feat: Add Multi-Page Scanner to Skill Scanner plugin

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!
This commit is contained in:
LemonNexus 2026-02-15 00:35:50 +00:00
parent 46a76a91e8
commit 482ec9aea4
1 changed files with 307 additions and 0 deletions

View File

@ -160,6 +160,10 @@ class SkillScannerPlugin(BasePlugin):
self.skill_gains = [] self.skill_gains = []
self._load_data() 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 # Subscribe to skill gain events from core Log service
try: try:
from core.plugin_api import get_api from core.plugin_api import get_api
@ -262,6 +266,110 @@ class SkillScannerPlugin(BasePlugin):
splitter.addWidget(scan_group) 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 section
log_group = QGroupBox("Log Tracking (Core Service)") log_group = QGroupBox("Log Tracking (Core Service)")
log_layout = QVBoxLayout(log_group) 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, 0, QTableWidgetItem(name))
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-'))) self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0)))) 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")