您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extract player rankings data (name, level, experience) and export to CSV, JSON
// ==UserScript== // @name The West Rankings CSV Exporter // @namespace TW-Export-Player-CSV // @version 2.0 // @description Extract player rankings data (name, level, experience) and export to CSV, JSON // @author Frozah // @include https://*.the-west.*/game.php* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function (fn) { var script = document.createElement('script'); script.setAttribute('type', 'application/javascript'); script.textContent = '(' + fn + ')();'; document.body.appendChild(script); document.body.removeChild(script); })(function () { RankingExporter = { version: '2.0', name: 'Rankings CSV Exporter', author: 'Frozah', // Configuration with persistent storage config: { includeAllPages: true, maxRetries: 3, baseDelay: 1000, adaptiveDelay: true, exportFormat: 'csv', // csv, json, excel columns: ['rank', 'name', 'level', 'experience'], filters: { minLevel: null, maxLevel: null, minRank: null, maxRank: null }, autoSave: true, logLevel: 'info' // debug, info, warn, error }, // Runtime data playersData: [], currentPage: 1, totalPages: 16, isScrapingInProgress: false, progressBar: null, progressDialog: null, selectBox: null, retryCount: 0, startTime: null, // Performance tracking performance: { averagePageTime: 1000, pageLoadTimes: [], errors: [] }, // Initialize the script init: function() { this.loadConfig(); this.validateGameStructure(); this.createAdvancedSelectBox(); this.addMenuButton(); this.log('Script initialized successfully', 'info'); }, // Load persistent configuration loadConfig: function() { try { if (typeof GM_getValue !== 'undefined') { var savedConfig = GM_getValue('rankingExporterConfig', null); if (savedConfig) { this.config = Object.assign(this.config, JSON.parse(savedConfig)); } } } catch (e) { this.log('Error loading config: ' + e.message, 'warn'); } }, // Save configuration saveConfig: function() { try { if (typeof GM_setValue !== 'undefined') { GM_setValue('rankingExporterConfig', JSON.stringify(this.config)); } } catch (e) { this.log('Error saving config: ' + e.message, 'warn'); } }, // Validate game structure before operation validateGameStructure: function() { var requiredElements = [ '.ranking-experience', '.rl_pagebar_ranking', '.exp_playername', '.exp_level', '.exp_exp', '.exp_rank' ]; var missingElements = []; requiredElements.forEach(function(selector) { if ($(selector).length === 0) { missingElements.push(selector); } }); if (missingElements.length > 0) { this.log('Warning: Some game elements not found: ' + missingElements.join(', '), 'warn'); new UserMessage('Game structure may have changed. Some features might not work correctly.', UserMessage.TYPE_ERROR).show(); return false; } return true; }, // Enhanced logging system log: function(message, level) { level = level || 'info'; var levels = ['debug', 'info', 'warn', 'error']; var currentLevelIndex = levels.indexOf(this.config.logLevel); var messageLevelIndex = levels.indexOf(level); if (messageLevelIndex >= currentLevelIndex) { var timestamp = new Date().toISOString(); var logMessage = '[' + timestamp + '] [' + level.toUpperCase() + '] RankingExporter: ' + message; switch(level) { case 'error': console.error(logMessage); break; case 'warn': console.warn(logMessage); break; default: console.log(logMessage); } } }, // Create advanced select box with more options createAdvancedSelectBox: function() { var self = this; var listener = function(action) { switch(action) { case 'export_current': self.exportCurrentPage(); break; case 'export_all': self.exportAllPages(); break; case 'export_filtered': self.showFilterDialog(); break; case 'export_range': self.showRangeDialog(); break; case 'stop_scraping': self.stopScraping(); break; case 'settings': self.showSettingsDialog(); break; case 'resume_export': self.resumeExport(); break; } }; this.selectBox = new west.gui.Selectbox() .setWidth(250) .addListener(listener) .addItem('export_current', 'Export Current Page') .addItem('export_all', 'Export All Pages') .addItem('export_filtered', 'Export with Filters') .addItem('export_range', 'Export Range') .addItem('stop_scraping', 'Stop Scraping') .addItem('settings', 'Settings') .addItem('resume_export', 'Resume Export'); }, // Add enhanced menu button addMenuButton: function() { var self = this; // Remove existing button if any $('#RankingExportermenu').parent().remove(); var menuButton = $('<div id="RankingExportermenu" class="menulink" title="' + this.name + ' v' + this.version + '" />') .css({ 'background': 'linear-gradient(45deg, #8B4513, #A0522D)', 'width': '25px', 'height': '25px', 'cursor': 'pointer', 'border': '2px solid #654321', 'border-radius': '3px', 'margin': '2px', 'position': 'relative', 'box-shadow': '0 2px 4px rgba(0,0,0,0.3)', 'transition': 'all 0.2s ease' }) .on('mouseenter', function() { $(this).css({ 'background': 'linear-gradient(45deg, #A0522D, #CD853F)', 'transform': 'scale(1.1)' }); }) .on('mouseleave', function() { $(this).css({ 'background': 'linear-gradient(45deg, #8B4513, #A0522D)', 'transform': 'scale(1)' }); }) .click(function() { self.toggleSelectbox(); }); // Add CSV icon $('<div>').css({ 'position': 'absolute', 'top': '50%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'color': 'white', 'font-weight': 'bold', 'font-size': '10px', 'text-shadow': '1px 1px 1px rgba(0,0,0,0.7)' }).text('CSV').appendTo(menuButton); var div = $('<div class="ui_menucontainer" />') .append(menuButton) .append('<div class="menucontainer_bottom" />'); // Try to add to menu bar, with fallback var menuBar = $('#ui_menubar'); if (menuBar.length > 0) { menuBar.append(div); this.log('Menu button added successfully', 'debug'); } else { // Fallback: try to find any menu container var menuContainer = $('.ui_menucontainer').first().parent(); if (menuContainer.length > 0) { menuContainer.append(div); this.log('Menu button added to fallback location', 'debug'); } else { this.log('Could not find menu location', 'error'); // Create floating button as last resort this.createFloatingButton(); } } }, // Create floating button as fallback createFloatingButton: function() { var self = this; var floatingButton = $('<div id="RankingExporterFloat" />') .css({ 'position': 'fixed', 'top': '100px', 'right': '20px', 'width': '50px', 'height': '50px', 'background': 'linear-gradient(45deg, #8B4513, #A0522D)', 'border': '3px solid #654321', 'border-radius': '50%', 'cursor': 'pointer', 'z-index': '9999', 'box-shadow': '0 4px 8px rgba(0,0,0,0.4)', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'color': 'white', 'font-weight': 'bold', 'font-size': '12px', 'text-shadow': '1px 1px 1px rgba(0,0,0,0.7)', 'transition': 'all 0.2s ease' }) .text('CSV') .on('mouseenter', function() { $(this).css({ 'background': 'linear-gradient(45deg, #A0522D, #CD853F)', 'transform': 'scale(1.1)' }); }) .on('mouseleave', function() { $(this).css({ 'background': 'linear-gradient(45deg, #8B4513, #A0522D)', 'transform': 'scale(1)' }); }) .click(function() { self.toggleSelectbox(); }) .attr('title', this.name + ' v' + this.version); $('body').append(floatingButton); this.log('Floating button created as fallback', 'info'); }, // Show settings dialog showSettingsDialog: function() { var self = this; var content = $('<div></div>'); // Export format selection content.append('<h3>Export Settings</h3>'); var formatSelect = $('<select id="exportFormat">') .append('<option value="csv">CSV</option>') .append('<option value="json">JSON</option>') .val(this.config.exportFormat); content.append($('<p>Format: </p>').append(formatSelect)); // Performance settings content.append('<h3>Performance Settings</h3>'); var adaptiveDelay = $('<input type="checkbox" id="adaptiveDelay">') .prop('checked', this.config.adaptiveDelay); content.append($('<p>Adaptive delay: </p>').append(adaptiveDelay)); var baseDelay = $('<input type="number" id="baseDelay" min="500" max="5000" step="100">') .val(this.config.baseDelay); content.append($('<p>Base delay (ms): </p>').append(baseDelay)); // Auto-save settings var autoSave = $('<input type="checkbox" id="autoSave">') .prop('checked', this.config.autoSave); content.append($('<p>Auto-save progress: </p>').append(autoSave)); var dialog = new west.gui.Dialog("Settings", content); dialog.addButton("Save", function() { self.config.exportFormat = formatSelect.val(); self.config.adaptiveDelay = adaptiveDelay.is(':checked'); self.config.baseDelay = parseInt(baseDelay.val()); self.config.autoSave = autoSave.is(':checked'); self.saveConfig(); dialog.hide(); new UserMessage('Settings saved!', UserMessage.TYPE_SUCCESS).show(); }); dialog.addButton("Cancel", function() { dialog.hide(); }); dialog.show(); }, // Show filter dialog showFilterDialog: function() { var self = this; var content = $('<div></div>'); content.append('<h3>Filter Options</h3>'); var minLevel = $('<input type="number" id="minLevel" placeholder="Min Level">'); var maxLevel = $('<input type="number" id="maxLevel" placeholder="Max Level">'); var minRank = $('<input type="number" id="minRank" placeholder="Min Rank">'); var maxRank = $('<input type="number" id="maxRank" placeholder="Max Rank">'); content.append($('<p>Level range: </p>').append(minLevel).append(' - ').append(maxLevel)); content.append($('<p>Rank range: </p>').append(minRank).append(' - ').append(maxRank)); var dialog = new west.gui.Dialog("Export with Filters", content); dialog.addButton("Export", function() { self.config.filters.minLevel = minLevel.val() || null; self.config.filters.maxLevel = maxLevel.val() || null; self.config.filters.minRank = minRank.val() || null; self.config.filters.maxRank = maxRank.val() || null; dialog.hide(); self.exportAllPages(true); }); dialog.addButton("Cancel", function() { dialog.hide(); }); dialog.show(); }, // Enhanced data extraction with error handling extractCurrentPageData: function() { var self = this; var data = []; var startTime = Date.now(); try { // Check if we're actually on the rankings page if (!this.isOnRankingsPage()) { new UserMessage('Please navigate to the rankings page first', UserMessage.TYPE_ERROR).show(); return []; } var rows = $('.ranking-experience .tbody .tw2gui_scrollpane_clipper_contentpane .row'); this.log('Found ' + rows.length + ' rows on current page', 'debug'); rows.each(function(index) { try { var row = $(this); var playerData = self.extractPlayerData(row); if (playerData && self.applyFilters(playerData)) { data.push(playerData); } } catch (e) { self.log('Error extracting data from row ' + index + ': ' + e.message, 'warn'); } }); var extractionTime = Date.now() - startTime; this.log('Extracted ' + data.length + ' players in ' + extractionTime + 'ms', 'debug'); } catch (e) { this.log('Error in extractCurrentPageData: ' + e.message, 'error'); this.performance.errors.push({ timestamp: Date.now(), error: e.message, page: this.currentPage }); } return data; }, // Extract individual player data extractPlayerData: function(row) { var playerNameCell = row.find('.exp_playername a'); var playerName = ''; if (playerNameCell.length > 0) { playerName = playerNameCell.text().trim(); } else { playerName = row.find('.exp_playername').text().trim(); } var level = row.find('.exp_level').text().trim(); var experience = row.find('.exp_exp').text().trim(); var rank = row.find('.exp_rank').text().trim(); if (playerName && level && experience && rank) { return { rank: parseInt(rank) || rank, name: playerName, level: parseInt(level) || level, experience: experience, rawExperience: this.parseExperience(experience) }; } return null; }, // Parse experience string to number parseExperience: function(expString) { if (!expString) return 0; // Remove dots and commas for international number formats return parseInt(expString.replace(/[.,]/g, '')) || 0; }, // Apply filters to player data applyFilters: function(playerData) { var filters = this.config.filters; if (filters.minLevel && playerData.level < filters.minLevel) return false; if (filters.maxLevel && playerData.level > filters.maxLevel) return false; if (filters.minRank && playerData.rank < filters.minRank) return false; if (filters.maxRank && playerData.rank > filters.maxRank) return false; return true; }, // Enhanced export all pages with better error handling exportAllPages: function(useFilters) { if (this.isScrapingInProgress) { new UserMessage('Scraping already in progress...', UserMessage.TYPE_ERROR).show(); return; } // Check if we're on the rankings page if (!this.isOnRankingsPage()) { new UserMessage('Please navigate to the rankings page first', UserMessage.TYPE_ERROR).show(); return; } if (!this.validateGameStructure()) { return; } this.isScrapingInProgress = true; this.playersData = []; this.currentPage = 1; this.retryCount = 0; this.startTime = Date.now(); this.performance.pageLoadTimes = []; this.performance.errors = []; this.getTotalPages(); var filterText = useFilters ? ' with filters' : ''; new UserMessage('Starting scraping of ' + this.totalPages + ' pages' + filterText + '...', UserMessage.TYPE_HINT).show(); this.log('Starting export of ' + this.totalPages + ' pages', 'info'); this.createEnhancedProgressDialog(); this.scrapePage(1); }, // Enhanced progress dialog createEnhancedProgressDialog: function() { var self = this; var content = $('<div></div>'); content.append('<p id="scrapingStatus">Scraping in progress...</p>'); content.append('<p id="timeInfo">Estimated time remaining: Calculating...</p>'); this.progressBar = new west.gui.Progressbar(0, this.totalPages); content.append(this.progressBar.getMainDiv()); content.append('<div id="statsInfo" style="font-size: 12px; margin-top: 10px;"></div>'); this.progressDialog = new west.gui.Dialog("Enhanced Export Progress", content); this.progressDialog.addButton("Pause", function() { self.pauseScraping(); }); this.progressDialog.addButton("Cancel", function() { self.stopScraping(); self.progressDialog.hide(); }); this.progressDialog.show(); }, // Enhanced page scraping with retry logic scrapePage: function(pageNumber) { var self = this; var pageStartTime = Date.now(); if (!this.isScrapingInProgress || pageNumber > this.totalPages) { this.finalizeScraping(); return; } this.updateProgress(pageNumber, pageStartTime); this.log('Scraping page ' + pageNumber + '/' + this.totalPages, 'debug'); this.goToPageWithRetry(pageNumber, function(success) { if (!success) { self.log('Failed to navigate to page ' + pageNumber + ' after retries', 'error'); if (self.retryCount < self.config.maxRetries) { self.retryCount++; setTimeout(function() { self.scrapePage(pageNumber); }, self.getAdaptiveDelay() * 2); return; } else { new UserMessage('Too many failures, stopping export', UserMessage.TYPE_ERROR).show(); self.stopScraping(); return; } } self.retryCount = 0; var delay = self.getAdaptiveDelay(); setTimeout(function() { self.waitForPageLoad(function() { var pageData = self.extractCurrentPageData(); self.playersData = self.playersData.concat(pageData); var pageTime = Date.now() - pageStartTime; self.performance.pageLoadTimes.push(pageTime); self.updatePerformanceStats(); self.log('Page ' + pageNumber + ' completed: ' + pageData.length + ' players in ' + pageTime + 'ms', 'info'); // Auto-save progress if (self.config.autoSave && pageNumber % 5 === 0) { self.saveProgress(); } setTimeout(function() { self.scrapePage(pageNumber + 1); }, delay); }); }, Math.min(delay, 1000)); }); }, // Navigate to page with retry logic goToPageWithRetry: function(pageNumber, callback, attempt) { attempt = attempt || 1; var self = this; if (attempt > this.config.maxRetries) { callback(false); return; } this.log('Navigation attempt ' + attempt + ' to page ' + pageNumber, 'debug'); this.goToPage(pageNumber, function(success) { if (success) { callback(true); } else { self.log('Navigation attempt ' + attempt + ' failed, retrying...', 'warn'); setTimeout(function() { self.goToPageWithRetry(pageNumber, callback, attempt + 1); }, self.getAdaptiveDelay()); } }); }, // Enhanced navigation with success callback goToPage: function(pageNumber, callback) { var self = this; var success = false; try { // Method 1: Direct input field var pageInput = $('.rl_pagebar_ranking .tw2gui_textfield input[type="text"]'); if (pageInput.length > 0) { pageInput.val(pageNumber); pageInput.trigger('input').trigger('change'); var enterEvent = jQuery.Event('keypress'); enterEvent.which = 13; enterEvent.keyCode = 13; pageInput.trigger(enterEvent); success = true; setTimeout(function() { callback(success); }, 800); return; } // Method 2: Ajax call if (typeof Ajax !== 'undefined' && Ajax.remoteCall) { Ajax.remoteCall("ranking", "get_ranking_page", { page: pageNumber, type: "experience" }, function(response) { success = true; setTimeout(function() { callback(success); }, 500); }, function(error) { self.log('Ajax call failed: ' + error, 'warn'); callback(false); }); return; } // Method 3: Click pagination link var pageLink = $('.rl_pagebar_ranking .pagebar_page').filter(function() { return $(this).text() == pageNumber; }); if (pageLink.length > 0) { pageLink.click(); success = true; setTimeout(function() { callback(success); }, 800); return; } callback(false); } catch (e) { this.log('Error in goToPage: ' + e.message, 'error'); callback(false); } }, // Get adaptive delay based on performance getAdaptiveDelay: function() { if (!this.config.adaptiveDelay) { return this.config.baseDelay; } var avgTime = this.performance.averagePageTime; var errorRate = this.performance.errors.length / Math.max(this.performance.pageLoadTimes.length, 1); // Increase delay if there are errors or slow performance var adaptiveMultiplier = 1 + (errorRate * 2) + (avgTime > 2000 ? 0.5 : 0); return Math.min(this.config.baseDelay * adaptiveMultiplier, 5000); }, // Update performance statistics updatePerformanceStats: function() { if (this.performance.pageLoadTimes.length > 0) { var sum = this.performance.pageLoadTimes.reduce(function(a, b) { return a + b; }, 0); this.performance.averagePageTime = sum / this.performance.pageLoadTimes.length; } }, // Update progress dialog updateProgress: function(pageNumber, pageStartTime) { if (this.progressBar) { this.progressBar.setValue(pageNumber - 1); } if (this.progressDialog) { var elapsed = Date.now() - this.startTime; var avgPageTime = this.performance.averagePageTime; var remainingPages = this.totalPages - pageNumber + 1; var estimatedRemaining = (remainingPages * avgPageTime) / 1000; $('#scrapingStatus').text('Scraping page ' + pageNumber + '/' + this.totalPages + '...'); $('#timeInfo').text('Estimated time remaining: ' + Math.round(estimatedRemaining) + 's'); $('#statsInfo').html( 'Players collected: ' + this.playersData.length + '<br>' + 'Average page time: ' + Math.round(avgPageTime) + 'ms<br>' + 'Errors: ' + this.performance.errors.length ); } }, // Save progress to localStorage saveProgress: function() { try { var progressData = { playersData: this.playersData, currentPage: this.currentPage, totalPages: this.totalPages, timestamp: Date.now() }; if (typeof GM_setValue !== 'undefined') { GM_setValue('exportProgress', JSON.stringify(progressData)); } this.log('Progress saved at page ' + this.currentPage, 'debug'); } catch (e) { this.log('Error saving progress: ' + e.message, 'warn'); } }, // Resume export from saved progress resumeExport: function() { try { if (typeof GM_getValue !== 'undefined') { var progressData = GM_getValue('exportProgress', null); if (progressData) { progressData = JSON.parse(progressData); // Check if progress is recent (within 24 hours) if (Date.now() - progressData.timestamp < 24 * 60 * 60 * 1000) { this.playersData = progressData.playersData; this.currentPage = progressData.currentPage; this.totalPages = progressData.totalPages; new UserMessage('Resuming from page ' + this.currentPage + ' (' + this.playersData.length + ' players already collected)', UserMessage.TYPE_SUCCESS).show(); this.isScrapingInProgress = true; this.createEnhancedProgressDialog(); this.scrapePage(this.currentPage); return; } } } new UserMessage('No recent progress found to resume', UserMessage.TYPE_ERROR).show(); } catch (e) { this.log('Error resuming export: ' + e.message, 'error'); new UserMessage('Error resuming export', UserMessage.TYPE_ERROR).show(); } }, // Enhanced CSV generation with multiple formats generateExport: function(data, filename) { if (data.length === 0) { new UserMessage('No data to export', UserMessage.TYPE_ERROR).show(); return; } switch (this.config.exportFormat) { case 'json': this.generateJSON(data, filename.replace('.csv', '.json')); break; case 'csv': default: this.generateCSV(data, filename); break; } }, // Generate JSON export generateJSON: function(data, filename) { var exportData = { metadata: { exportDate: new Date().toISOString(), totalPlayers: data.length, version: this.version, filters: this.config.filters }, players: data }; var jsonContent = JSON.stringify(exportData, null, 2); this.downloadFile(jsonContent, filename, 'application/json'); }, // Enhanced CSV generation generateCSV: function(data, filename) { var csv = 'Rank,Player Name,Level,Experience,Raw Experience\n'; data.forEach(function(player) { var name = '"' + player.name.replace(/"/g, '""') + '"'; var experience = '"' + player.experience.replace(/"/g, '""') + '"'; csv += player.rank + ',' + name + ',' + player.level + ',' + experience + ',' + player.rawExperience + '\n'; }); this.downloadFile(csv, filename, 'text/csv'); }, // Enhanced file download downloadFile: function(content, filename, mimeType) { try { var blob = new Blob([content], { type: mimeType + ';charset=utf-8;' }); var link = document.createElement('a'); if (link.download !== undefined) { var url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); this.log('File downloaded: ' + filename, 'info'); } } catch (e) { this.log('Error downloading file: ' + e.message, 'error'); new UserMessage('Error downloading file', UserMessage.TYPE_ERROR).show(); } }, // Enhanced finalization with statistics finalizeScraping: function() { this.isScrapingInProgress = false; if (this.progressDialog) { this.progressDialog.hide(); } if (this.playersData.length === 0) { new UserMessage('No data collected', UserMessage.TYPE_ERROR).show(); return; } // Clean up progress save if (typeof GM_deleteValue !== 'undefined') { GM_deleteValue('exportProgress'); } // Sort and deduplicate this.playersData.sort(function(a, b) { return parseInt(a.rank) - parseInt(b.rank); }); var uniqueData = this.removeDuplicates(this.playersData); var totalTime = Date.now() - this.startTime; this.generateExport(uniqueData, 'rankings_enhanced_export.' + this.config.exportFormat); var stats = 'Export completed!\n' + 'Players: ' + uniqueData.length + '\n' + 'Time: ' + Math.round(totalTime / 1000) + 's\n' + 'Avg page time: ' + Math.round(this.performance.averagePageTime) + 'ms\n' + 'Errors: ' + this.performance.errors.length; new UserMessage(stats, UserMessage.TYPE_SUCCESS).show(); this.log('Export finalized: ' + uniqueData.length + ' players in ' + totalTime + 'ms', 'info'); }, // Remove duplicates more efficiently removeDuplicates: function(data) { var seen = {}; return data.filter(function(player) { var key = player.rank + '_' + player.name; if (seen[key]) { return false; } seen[key] = true; return true; }); }, // Pause scraping pauseScraping: function() { this.isScrapingInProgress = false; this.saveProgress(); new UserMessage('Scraping paused. Use "Resume Export" to continue.', UserMessage.TYPE_HINT).show(); if (this.progressDialog) { this.progressDialog.hide(); } }, // Show/hide select box toggleSelectbox: function() { var pos = $('div#RankingExportermenu').offset(); pos = { clientX: pos.left, clientY: pos.top }; this.selectBox.show(pos); }, // Export current page with enhanced features exportCurrentPage: function() { var data = this.extractCurrentPageData(); if (data.length === 0) { new UserMessage('No data found on this page', UserMessage.TYPE_ERROR).show(); return; } var filename = 'rankings_current_page_' + Date.now() + '.' + this.config.exportFormat; this.generateExport(data, filename); new UserMessage('Current page exported (' + data.length + ' players)', UserMessage.TYPE_SUCCESS).show(); }, // Show range dialog for partial exports showRangeDialog: function() { var self = this; var content = $('<div></div>'); content.append('<h3>Export Page Range</h3>'); content.append('<p>Select the range of pages to export:</p>'); var startPage = $('<input type="number" id="startPage" min="1" max="' + this.totalPages + '" value="1">'); var endPage = $('<input type="number" id="endPage" min="1" max="' + this.totalPages + '" value="' + Math.min(5, this.totalPages) + '">'); content.append($('<p>From page: </p>').append(startPage)); content.append($('<p>To page: </p>').append(endPage)); content.append('<p><small>Note: Large ranges may take several minutes</small></p>'); var dialog = new west.gui.Dialog("Export Page Range", content); dialog.addButton("Export", function() { var start = parseInt(startPage.val()) || 1; var end = parseInt(endPage.val()) || 1; if (start > end) { new UserMessage('Start page must be less than or equal to end page', UserMessage.TYPE_ERROR).show(); return; } if (start < 1 || end > self.totalPages) { new UserMessage('Page numbers must be between 1 and ' + self.totalPages, UserMessage.TYPE_ERROR).show(); return; } dialog.hide(); self.exportPageRange(start, end); }); dialog.addButton("Cancel", function() { dialog.hide(); }); dialog.show(); }, // Export specific page range exportPageRange: function(startPage, endPage) { if (this.isScrapingInProgress) { new UserMessage('Scraping already in progress...', UserMessage.TYPE_ERROR).show(); return; } this.isScrapingInProgress = true; this.playersData = []; this.currentPage = startPage; this.totalPages = endPage; this.retryCount = 0; this.startTime = Date.now(); new UserMessage('Starting range export: pages ' + startPage + '-' + endPage, UserMessage.TYPE_HINT).show(); this.log('Starting range export: pages ' + startPage + '-' + endPage, 'info'); this.createEnhancedProgressDialog(); this.scrapePage(startPage); }, // Enhanced total pages detection getTotalPages: function() { try { // Method 1: Look for pagination text var paginationText = $('.rl_pagebar_ranking .maxpages').text(); if (paginationText) { var match = paginationText.match(/\/\s*(\d+)/); if (match) { this.totalPages = parseInt(match[1]); this.log('Total pages detected (method 1): ' + this.totalPages, 'debug'); return; } } // Method 2: Count pagination links var pageLinks = $('.rl_pagebar_ranking .pagebar_page'); if (pageLinks.length > 0) { var lastPage = 1; pageLinks.each(function() { var pageNum = parseInt($(this).text()); if (!isNaN(pageNum) && pageNum > lastPage) { lastPage = pageNum; } }); this.totalPages = lastPage; this.log('Total pages detected (method 2): ' + this.totalPages, 'debug'); return; } // Method 3: Try to calculate from total players (if available) var totalPlayersText = $('.ranking-experience .ranking_header').text(); var totalPlayersMatch = totalPlayersText.match(/(\d+)\s*players?/i); if (totalPlayersMatch) { var totalPlayers = parseInt(totalPlayersMatch[1]); var playersPerPage = $('.ranking-experience .tbody .row').length || 25; this.totalPages = Math.ceil(totalPlayers / playersPerPage); this.log('Total pages calculated (method 3): ' + this.totalPages + ' (from ' + totalPlayers + ' players)', 'debug'); return; } // Default fallback this.totalPages = 16; this.log('Using default total pages: ' + this.totalPages, 'warn'); } catch (e) { this.log('Error detecting total pages: ' + e.message, 'error'); this.totalPages = 16; } }, // Enhanced page load waiting with timeout waitForPageLoad: function(callback) { var self = this; var attempts = 0; var maxAttempts = 20; // Increased for better reliability var startTime = Date.now(); var checkLoad = function() { attempts++; var rows = $('.ranking-experience .tbody .tw2gui_scrollpane_clipper_contentpane .row'); var loadTime = Date.now() - startTime; // Check if page is loaded or timeout reached if (rows.length > 0) { self.log('Page loaded successfully in ' + loadTime + 'ms (' + rows.length + ' rows)', 'debug'); callback(); } else if (attempts >= maxAttempts || loadTime > 10000) { self.log('Page load timeout after ' + loadTime + 'ms (' + attempts + ' attempts)', 'warn'); callback(); // Continue anyway } else { setTimeout(checkLoad, 500); } }; checkLoad(); }, // Stop scraping with cleanup stopScraping: function() { this.isScrapingInProgress = false; if (this.progressDialog) { this.progressDialog.hide(); } // Save partial data if any if (this.playersData.length > 0 && this.config.autoSave) { var partialFilename = 'rankings_partial_' + Date.now() + '.' + this.config.exportFormat; this.generateExport(this.playersData, partialFilename); new UserMessage('Scraping stopped. Partial data saved (' + this.playersData.length + ' players)', UserMessage.TYPE_HINT).show(); } else { new UserMessage('Scraping stopped', UserMessage.TYPE_ERROR).show(); } this.log('Scraping stopped by user', 'info'); }, // Check if on rankings page isOnRankingsPage: function() { return $('.ranking-experience').length > 0 && $('.rl_pagebar_ranking').length > 0; }, // Check if ranking elements exist (more flexible check) canAccessRankings: function() { // Check if we're in the game interface return $('#ui_menubar').length > 0 && ( $('.ranking-experience').length > 0 || $('a[href*="ranking"]').length > 0 || $('.menulink').length > 0 ); }, // Cleanup function cleanup: function() { this.isScrapingInProgress = false; if (this.progressDialog) { this.progressDialog.hide(); } // Clear any remaining timeouts if (this.scrapingTimeout) { clearTimeout(this.scrapingTimeout); } this.log('Cleanup completed', 'debug'); } }; // Initialize when document and game objects are ready $(document).ready(function() { try { var initInterval = setInterval(function() { if (typeof west !== 'undefined' && typeof west.gui !== 'undefined' && typeof west.gui.Selectbox !== 'undefined' && typeof west.gui.Dialog !== 'undefined' && typeof west.gui.Progressbar !== 'undefined' && typeof UserMessage !== 'undefined') { clearInterval(initInterval); // Initialize if we can access the game interface if (RankingExporter.canAccessRankings()) { RankingExporter.init(); console.log('Rankings Exporter Enhanced initialized'); } else { console.log('Game interface not detected, retrying...'); // Try again after a longer delay setTimeout(function() { if (RankingExporter.canAccessRankings()) { RankingExporter.init(); console.log('Rankings Exporter Enhanced initialized (delayed)'); } }, 5000); } } }, 1000); // Cleanup on page unload $(window).on('beforeunload', function() { if (RankingExporter.isScrapingInProgress) { RankingExporter.cleanup(); return 'Export in progress. Are you sure you want to leave?'; } }); } catch (e) { console.error('Error initializing Rankings Exporter Enhanced:', e); } }); // Global error handler window.onerror = function(msg, url, lineNo, columnNo, error) { if (msg.indexOf('RankingExporter') !== -1) { RankingExporter.log('Global error: ' + msg + ' at line ' + lineNo, 'error'); return true; } return false; }; // Debug info console.log('Rankings CSV Exporter Enhanced v' + RankingExporter.version + ' loaded'); console.log('Features: Adaptive delays, retry logic, multiple formats, filters, progress saving'); });