From 482ec9aea4d5f2f4f74381c03c834d59c8433d69 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 15 Feb 2026 00:35:50 +0000 Subject: [PATCH] feat: Add Multi-Page Scanner to Skill Scanner plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEW FEATURE - Multi-Page Scanner: WORKFLOW: 1. User positions Skills window to show skills 2. User clicks 'Scan Current Page' 3. App scans, shows checkmark ✅, plays BEEP sound 4. Status shows: 'Page X scanned! Click Next Page in game →' 5. User manually clicks Next Page in EU 6. User clicks 'Scan Current Page' again 7. Repeat until all pages scanned 8. User clicks 'Save All' to store combined results FEATURES: - ✅ Checkmark icon and green text on successful scan - 🔊 Beep sound (Windows MessageBeep) to notify user - 📊 Live counters: Pages scanned, Total skills - 🗑 Clear Session button to start over - 💾 Save All button merges session into main data - 📝 Session table shows all skills collected so far UI ELEMENTS: - Instructions panel explaining the workflow - Status label with color-coded feedback - Pages: X counter - Skills: X counter - Three buttons: Scan Page, Save All, Clear Session - Session table showing accumulated skills TECHNICAL: - current_scan_session dict accumulates skills across pages - pages_scanned counter tracks progress - Thread-safe UI updates via QMetaObject.invokeMethod - Windows beep via winsound module (with fallback) This gives users full control while guiding them through multi-page scanning without any auto-clicking! --- plugins/skill_scanner/plugin.py | 307 ++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/plugins/skill_scanner/plugin.py b/plugins/skill_scanner/plugin.py index d3affb2..b1f9fff 100644 --- a/plugins/skill_scanner/plugin.py +++ b/plugins/skill_scanner/plugin.py @@ -160,6 +160,10 @@ class SkillScannerPlugin(BasePlugin): self.skill_gains = [] self._load_data() + # Multi-page scanning state + self.current_scan_session = {} # Skills collected in current multi-page scan + self.pages_scanned = 0 + # Subscribe to skill gain events from core Log service try: from core.plugin_api import get_api @@ -262,6 +266,110 @@ class SkillScannerPlugin(BasePlugin): splitter.addWidget(scan_group) + # Multi-Page Scanning section + multi_page_group = QGroupBox("Multi-Page Scanner") + multi_page_layout = QVBoxLayout(multi_page_group) + + # Instructions + instructions = QLabel( + "1. Position your Skills window to show skills\n" + "2. Click 'Scan Page' below\n" + "3. When you hear the beep, click Next Page in game\n" + "4. Repeat until all pages are scanned\n" + "5. Click 'Save All' to store combined results" + ) + instructions.setStyleSheet("color: #888; font-size: 11px;") + multi_page_layout.addWidget(instructions) + + # Status row + status_layout = QHBoxLayout() + + self.multi_page_status = QLabel("⏳ Ready to scan page 1") + self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;") + status_layout.addWidget(self.multi_page_status) + + self.pages_scanned_label = QLabel("Pages: 0") + self.pages_scanned_label.setStyleSheet("color: #4ecdc4;") + status_layout.addWidget(self.pages_scanned_label) + + self.total_skills_label = QLabel("Skills: 0") + self.total_skills_label.setStyleSheet("color: #4ecdc4;") + status_layout.addWidget(self.total_skills_label) + + status_layout.addStretch() + multi_page_layout.addLayout(status_layout) + + # Buttons + mp_buttons_layout = QHBoxLayout() + + self.scan_page_btn = QPushButton("📷 Scan Current Page") + self.scan_page_btn.setStyleSheet(""" + QPushButton { + background-color: #4ecdc4; + color: #141f23; + padding: 12px; + border: none; + border-radius: 4px; + font-weight: bold; + font-size: 13px; + } + QPushButton:hover { + background-color: #3dbdb4; + } + """) + self.scan_page_btn.clicked.connect(self._scan_page_for_multi) + mp_buttons_layout.addWidget(self.scan_page_btn) + + save_all_btn = QPushButton("💾 Save All Scanned") + save_all_btn.setStyleSheet(""" + QPushButton { + background-color: #ff8c42; + color: white; + padding: 12px; + border: none; + border-radius: 4px; + font-weight: bold; + font-size: 13px; + } + """) + save_all_btn.clicked.connect(self._save_multi_page_scan) + mp_buttons_layout.addWidget(save_all_btn) + + clear_session_btn = QPushButton("🗑 Clear Session") + clear_session_btn.setStyleSheet(""" + QPushButton { + background-color: #666; + color: white; + padding: 12px; + border: none; + border-radius: 4px; + } + """) + clear_session_btn.clicked.connect(self._clear_multi_page_session) + mp_buttons_layout.addWidget(clear_session_btn) + + multi_page_layout.addLayout(mp_buttons_layout) + + # Current session table + self.session_table = QTableWidget() + self.session_table.setColumnCount(3) + self.session_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"]) + self.session_table.horizontalHeader().setStretchLastSection(True) + self.session_table.setMaximumHeight(200) + self.session_table.setStyleSheet(""" + QTableWidget { + background-color: #0d1117; + border: 1px solid #333; + } + QTableWidget::item { + padding: 4px; + color: #c9d1d9; + } + """) + multi_page_layout.addWidget(self.session_table) + + splitter.addWidget(multi_page_group) + # Log section log_group = QGroupBox("Log Tracking (Core Service)") log_layout = QVBoxLayout(log_group) @@ -363,3 +471,202 @@ class SkillScannerPlugin(BasePlugin): self.skills_table.setItem(i, 0, QTableWidgetItem(name)) self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-'))) self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0)))) + + def _scan_page_for_multi(self): + """Scan current page and add to multi-page session.""" + from PyQt6.QtCore import QTimer + + self.multi_page_status.setText("📷 Scanning...") + self.multi_page_status.setStyleSheet("color: #ffd93d;") + self.scan_page_btn.setEnabled(False) + + # Run scan in thread + from threading import Thread + + def do_scan(): + try: + from core.ocr_service import get_ocr_service + import re + from datetime import datetime + + ocr_service = get_ocr_service() + if not ocr_service.is_available(): + self._update_multi_page_status("Error: OCR not available", error=True) + return + + # Capture and OCR + result = ocr_service.recognize() + text = result.get('text', '') + + # Parse skills + skills = self._parse_skills_from_text(text) + + # Add to session + for skill_name, data in skills.items(): + self.current_scan_session[skill_name] = data + + self.pages_scanned += 1 + + # Update UI + from PyQt6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self, "_update_session_table", + Qt.ConnectionType.QueuedConnection, + Q_ARG(object, self.current_scan_session) + ) + + # Show success with checkmark and beep + self._update_multi_page_status( + f"✅ Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game →", + success=True + ) + + # Play beep sound + self._play_beep() + + except Exception as e: + self._update_multi_page_status(f"Error: {str(e)}", error=True) + finally: + from PyQt6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.scan_page_btn, + "setEnabled", + Qt.ConnectionType.QueuedConnection, + Q_ARG(bool, True) + ) + + thread = Thread(target=do_scan) + thread.daemon = True + thread.start() + + def _parse_skills_from_text(self, text): + """Parse skills from OCR text.""" + skills = {} + + # Ranks in Entropia Universe - multi-word first for proper matching + SINGLE_RANKS = [ + 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', + 'Skilled', 'Expert', 'Professional', 'Master', + 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', + 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' + ] + MULTI_RANKS = ['Arch Master', 'Grand Master'] + ALL_RANKS = MULTI_RANKS + SINGLE_RANKS + rank_pattern = '|'.join(ALL_RANKS) + + # Clean text + text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') + text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '') + + # Remove category names + for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction', + 'Defense', 'General', 'Handgun', 'Heavy Melee Weapons', + 'Heavy Weapons', 'Information', 'Inflict Melee Damage', + 'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades', + 'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']: + text = text.replace(category, ' ') + + text = ' '.join(text.split()) + + # Find all skills + import re + for match in re.finditer( + rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', + text, re.IGNORECASE + ): + skill_name = match.group(1).strip() + rank = match.group(2) + points = int(match.group(3)) + + skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE) + skill_name = skill_name.strip() + + if points > 0 and skill_name and len(skill_name) > 2: + skills[skill_name] = {'rank': rank, 'points': points} + + return skills + + def _update_multi_page_status(self, message, success=False, error=False): + """Update multi-page status label.""" + from PyQt6.QtCore import QMetaObject, Qt, Q_ARG + + color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42" + + QMetaObject.invokeMethod( + self.multi_page_status, + "setText", + Qt.ConnectionType.QueuedConnection, + Q_ARG(str, message) + ) + QMetaObject.invokeMethod( + self.multi_page_status, + "setStyleSheet", + Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"color: {color}; font-size: 14px;") + ) + + # Update counters + QMetaObject.invokeMethod( + self.pages_scanned_label, + "setText", + Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Pages: {self.pages_scanned}") + ) + QMetaObject.invokeMethod( + self.total_skills_label, + "setText", + Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Skills: {len(self.current_scan_session)}") + ) + + def _play_beep(self): + """Play a beep sound to notify user.""" + try: + import winsound + winsound.MessageBeep(winsound.MB_OK) + except: + # Fallback - try to use system beep + try: + print('\a') # ASCII bell character + except: + pass + + def _update_session_table(self, skills): + """Update the session table with current scan data.""" + self.session_table.setRowCount(len(skills)) + for i, (name, data) in enumerate(sorted(skills.items())): + self.session_table.setItem(i, 0, QTableWidgetItem(name)) + self.session_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-'))) + self.session_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0)))) + + def _save_multi_page_scan(self): + """Save all scanned skills from multi-page session.""" + if not self.current_scan_session: + from PyQt6.QtWidgets import QMessageBox + QMessageBox.information(None, "No Data", "No skills scanned yet. Scan some pages first!") + return + + # Merge with existing data + self.skills_data.update(self.current_scan_session) + self._save_data() + self._refresh_skills_table() + + from PyQt6.QtWidgets import QMessageBox + QMessageBox.information( + None, + "Scan Complete", + f"Saved {len(self.current_scan_session)} skills from {self.pages_scanned} pages!" + ) + + # Clear session after saving + self._clear_multi_page_session() + + def _clear_multi_page_session(self): + """Clear the current multi-page scanning session.""" + self.current_scan_session = {} + self.pages_scanned = 0 + self.session_table.setRowCount(0) + self.multi_page_status.setText("⏳ Ready to scan page 1") + self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;") + self.pages_scanned_label.setText("Pages: 0") + self.total_skills_label.setText("Skills: 0")