EU-Utility/plugins/skill_scanner/plugin.py

827 lines
31 KiB
Python

"""
EU-Utility - Skill Scanner Plugin
Uses core OCR and Log services via PluginAPI.
"""
import re
import json
from datetime import datetime
from pathlib import Path
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
QFrame, QGroupBox, QTextEdit, QSplitter, QComboBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from plugins.base_plugin import BasePlugin
class SkillOCRThread(QThread):
"""OCR scan using core service."""
scan_complete = pyqtSignal(dict)
scan_error = pyqtSignal(str)
progress_update = pyqtSignal(str)
def __init__(self, ocr_service):
super().__init__()
self.ocr_service = ocr_service
def run(self):
"""Perform OCR using core service."""
try:
self.progress_update.emit("Capturing screen...")
# Use core OCR service
result = self.ocr_service.recognize()
if 'error' in result and result['error']:
self.scan_error.emit(result['error'])
return
self.progress_update.emit("Parsing skills...")
# Parse skills from text
skills_data = self._parse_skills(result.get('text', ''))
self.scan_complete.emit(skills_data)
except Exception as e:
self.scan_error.emit(str(e))
def _parse_skills(self, text):
"""Parse skill data from OCR text with improved handling for 3-column layout."""
skills = {}
# Ranks in Entropia Universe (including multi-word ranks)
# Single word ranks
SINGLE_RANKS = [
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
'Skilled', 'Expert', 'Professional', 'Master',
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
]
# Multi-word ranks (must be checked first - longer matches first)
MULTI_RANKS = [
'Arch Master', 'Grand Master'
]
# Combine: multi-word first (so they match before single word), then single
ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
rank_pattern = '|'.join(ALL_RANKS)
# Clean up the text - remove common headers and junk
text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
# Remove category names that appear as standalone words
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, ' ')
# Remove extra whitespace
text = ' '.join(text.split())
# Find all skills in the text using finditer
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))
# Clean up skill name - remove common words that might be prepended
skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
skill_name = skill_name.strip()
# Validate - points should be reasonable (not too small)
if points > 0 and skill_name and len(skill_name) > 2:
skills[skill_name] = {
'rank': rank,
'points': points,
'scanned_at': datetime.now().isoformat()
}
print(f"[SkillScanner] Parsed: {skill_name} = {rank} ({points})")
# If no skills found with primary method, try alternative
if not skills:
skills = self._parse_skills_alternative(text, ALL_RANKS)
return skills
def _parse_skills_alternative(self, text, ranks):
"""Alternative parser for when text is heavily merged."""
skills = {}
# Find all rank positions in the text
for rank in ranks:
# Look for pattern: [text] [Rank] [number]
pattern = rf'([A-Z][a-z]{{2,}}(?:\s+[A-Z][a-z]{{2,}}){{0,3}})\s+{re.escape(rank)}\s+(\d{{1,6}})'
matches = re.finditer(pattern, text, re.IGNORECASE)
for match in matches:
skill_name = match.group(1).strip()
points = int(match.group(2))
# Clean skill name
skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
if points > 0 and len(skill_name) > 2:
skills[skill_name] = {
'rank': rank,
'points': points,
'scanned_at': datetime.now().isoformat()
}
return skills
class SkillScannerPlugin(BasePlugin):
"""Scan skills using core OCR and track gains via core Log service."""
name = "Skill Scanner"
version = "2.1.0"
author = "ImpulsiveFPS"
description = "Uses core OCR and Log services"
hotkey = "ctrl+shift+s"
def initialize(self):
"""Setup skill scanner."""
self.data_file = Path("data/skill_tracker.json")
self.data_file.parent.mkdir(parents=True, exist_ok=True)
# Load saved data
self.skills_data = {}
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
api = get_api()
# Check if log service available
log_service = api.services.get('log')
if log_service:
print(f"[SkillScanner] Connected to core Log service")
except Exception as e:
print(f"[SkillScanner] Could not connect to Log service: {e}")
def _load_data(self):
"""Load saved skill data."""
if self.data_file.exists():
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
self.skills_data = data.get('skills', {})
self.skill_gains = data.get('gains', [])
except:
pass
def _save_data(self):
"""Save skill data."""
with open(self.data_file, 'w') as f:
json.dump({
'skills': self.skills_data,
'gains': self.skill_gains
}, f, indent=2)
def get_ui(self):
"""Create skill scanner UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Header
header = QLabel("Skill Tracker")
header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
layout.addWidget(header)
# Info about core services
info = self._get_service_status()
info_label = QLabel(info)
info_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
layout.addWidget(info_label)
# Splitter
splitter = QSplitter(Qt.Orientation.Vertical)
# Scan section
scan_group = QGroupBox("OCR Scan (Core Service)")
scan_layout = QVBoxLayout(scan_group)
# Buttons row
buttons_layout = QHBoxLayout()
scan_btn = QPushButton("Scan Skills Window")
scan_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
color: white;
padding: 12px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
scan_btn.clicked.connect(self._scan_skills)
buttons_layout.addWidget(scan_btn)
reset_btn = QPushButton("Reset Data")
reset_btn.setStyleSheet("""
QPushButton {
background-color: #ff4757;
color: white;
padding: 12px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
reset_btn.clicked.connect(self._reset_data)
buttons_layout.addWidget(reset_btn)
scan_layout.addLayout(buttons_layout)
self.scan_progress = QLabel("Ready to scan")
self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
scan_layout.addWidget(self.scan_progress)
self.skills_table = QTableWidget()
self.skills_table.setColumnCount(3)
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
self.skills_table.horizontalHeader().setStretchLastSection(True)
scan_layout.addWidget(self.skills_table)
splitter.addWidget(scan_group)
# Multi-Page Scanning section
multi_page_group = QGroupBox("Multi-Page Scanner")
multi_page_layout = QVBoxLayout(multi_page_group)
# Mode selection
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("Mode:"))
self.scan_mode_combo = QComboBox()
self.scan_mode_combo.addItems(["Smart Auto + Hotkey Fallback", "Manual Hotkey Only", "Manual Click Only"])
self.scan_mode_combo.currentIndexChanged.connect(self._on_scan_mode_changed)
mode_layout.addWidget(self.scan_mode_combo)
mode_layout.addStretch()
multi_page_layout.addLayout(mode_layout)
# Instructions
self.instructions_label = QLabel(
"🤖 SMART MODE:\n"
"1. Position Skills window\n"
"2. Click 'Start Smart Scan'\n"
"3. Navigate pages in EU - auto-detect will scan for you\n"
"4. If auto fails, use hotkey F12 to scan manually\n"
"5. Click 'Save All' when done"
)
self.instructions_label.setStyleSheet("color: #888; font-size: 11px;")
multi_page_layout.addWidget(self.instructions_label)
# Hotkey info
self.hotkey_info = QLabel("Hotkey: F12 = Scan Current Page")
self.hotkey_info.setStyleSheet("color: #4ecdc4; font-weight: bold;")
multi_page_layout.addWidget(self.hotkey_info)
# 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("▶️ Start Smart Scan")
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._start_smart_scan)
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)
log_status = QLabel("Skill gains tracked from chat log")
log_status.setStyleSheet("color: #4ecdc4;")
log_layout.addWidget(log_status)
self.gains_text = QTextEdit()
self.gains_text.setReadOnly(True)
self.gains_text.setMaximumHeight(150)
self.gains_text.setPlaceholderText("Recent skill gains from core Log service...")
log_layout.addWidget(self.gains_text)
self.total_gains_label = QLabel(f"Total gains: {len(self.skill_gains)}")
self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);")
log_layout.addWidget(self.total_gains_label)
splitter.addWidget(log_group)
layout.addWidget(splitter)
self._refresh_skills_table()
return widget
def _get_service_status(self) -> str:
"""Get status of core services."""
try:
from core.ocr_service import get_ocr_service
from core.log_reader import get_log_reader
ocr = get_ocr_service()
log = get_log_reader()
ocr_status = "" if ocr.is_available() else ""
log_status = "" if log.is_available() else ""
return f"Core Services - OCR: {ocr_status} Log: {log_status}"
except:
return "Core Services - status unknown"
def _scan_skills(self):
"""Start OCR scan using core service."""
try:
from core.ocr_service import get_ocr_service
ocr_service = get_ocr_service()
if not ocr_service.is_available():
self.scan_progress.setText("Error: OCR service not available")
return
self.scanner = SkillOCRThread(ocr_service)
self.scanner.scan_complete.connect(self._on_scan_complete)
self.scanner.scan_error.connect(self._on_scan_error)
self.scanner.progress_update.connect(self._on_scan_progress)
self.scanner.start()
except Exception as e:
self.scan_progress.setText(f"Error: {e}")
def _on_scan_progress(self, message):
self.scan_progress.setText(message)
def _on_scan_complete(self, skills_data):
self.skills_data.update(skills_data)
self._save_data()
self._refresh_skills_table()
self.scan_progress.setText(f"Found {len(skills_data)} skills")
def _reset_data(self):
"""Reset all skill data."""
from PyQt6.QtWidgets import QMessageBox
reply = QMessageBox.question(
None,
"Reset Skill Data",
"Are you sure you want to clear all scanned skill data?\n\nThis cannot be undone.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.skills_data = {}
self.skill_gains = []
self._save_data()
self._refresh_skills_table()
self.gains_text.clear()
self.total_gains_label.setText("Total gains: 0")
self.scan_progress.setText("Data cleared")
def _on_scan_error(self, error):
self.scan_progress.setText(f"Error: {error}")
self.scan_progress.setText(f"Error: {error}")
def _refresh_skills_table(self):
self.skills_table.setRowCount(len(self.skills_data))
for i, (name, data) in enumerate(sorted(self.skills_data.items())):
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.auto_scan_active = False
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")
# Unregister hotkey if active
self._unregister_hotkey()
def _on_scan_mode_changed(self, index):
"""Handle scan mode change."""
modes = [
"🤖 SMART MODE:\n1. Position Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done",
"⌨️ HOTKEY MODE:\n1. Position Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page",
"🖱️ MANUAL MODE:\n1. Position Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat"
]
self.instructions_label.setText(modes[index])
def _start_smart_scan(self):
"""Start smart auto-scan with hotkey fallback."""
mode = self.scan_mode_combo.currentIndex()
if mode == 0: # Smart Auto + Hotkey
self._start_auto_scan_with_hotkey()
elif mode == 1: # Hotkey only
self._register_hotkey()
self._update_multi_page_status("Hotkey F12 ready! Navigate to first page and press F12", success=True)
else: # Manual click
self._scan_page_for_multi()
def _start_auto_scan_with_hotkey(self):
"""Start auto-detection with fallback to hotkey."""
self.auto_scan_active = True
self.auto_scan_failures = 0
self.last_page_number = None
# Register F12 hotkey as fallback
self._register_hotkey()
# Start monitoring
self._update_multi_page_status("🤖 Auto-detect started! Navigate to page 1...", success=True)
# Start auto-detection timer
self.auto_scan_timer = QTimer()
self.auto_scan_timer.timeout.connect(self._check_for_page_change)
self.auto_scan_timer.start(500) # Check every 500ms
def _register_hotkey(self):
"""Register F12 hotkey for manual scan."""
try:
import keyboard
keyboard.on_press_key('f12', lambda e: self._hotkey_scan())
self.hotkey_registered = True
except Exception as e:
print(f"[SkillScanner] Could not register hotkey: {e}")
self.hotkey_registered = False
def _unregister_hotkey(self):
"""Unregister hotkey."""
try:
if hasattr(self, 'hotkey_registered') and self.hotkey_registered:
import keyboard
keyboard.unhook_all()
self.hotkey_registered = False
except:
pass
# Stop auto-scan timer
if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
self.auto_scan_timer.stop()
self.auto_scan_active = False
def _hotkey_scan(self):
"""Scan triggered by F12 hotkey."""
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self, "_scan_page_for_multi",
Qt.ConnectionType.QueuedConnection
)
def _check_for_page_change(self):
"""Auto-detect page changes by monitoring page number area."""
if not self.auto_scan_active:
return
try:
from PIL import ImageGrab
import pytesseract
# Capture page number area (bottom center of skills window)
# This is approximate - may need adjustment
screen = ImageGrab.grab()
width, height = screen.size
# Try to capture the page number area (bottom center, small region)
# EU skills window shows page like "1/12" at bottom
page_area = (width // 2 - 50, height - 100, width // 2 + 50, height - 50)
page_img = ImageGrab.grab(bbox=page_area)
# OCR just the page number
page_text = pytesseract.image_to_string(page_img, config='--psm 7 -c tessedit_char_whitelist=0123456789/')
# Extract current page number
import re
match = re.search(r'(\d+)/(\d+)', page_text)
if match:
current_page = int(match.group(1))
total_pages = int(match.group(2))
# If page changed, trigger scan
if self.last_page_number is not None and current_page != self.last_page_number:
self._update_multi_page_status(f"📄 Page change detected: {current_page}/{total_pages}", success=True)
self._scan_page_for_multi()
self.last_page_number = current_page
else:
# Failed to detect page number
self.auto_scan_failures += 1
if self.auto_scan_failures >= 10: # After 5 seconds of failures
self._fallback_to_hotkey()
except Exception as e:
self.auto_scan_failures += 1
if self.auto_scan_failures >= 10:
self._fallback_to_hotkey()
def _fallback_to_hotkey(self):
"""Fallback to hotkey mode when auto-detection fails."""
if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
self.auto_scan_timer.stop()
self.auto_scan_active = False
# Keep hotkey registered
self._update_multi_page_status(
"⚠️ Auto-detect unreliable. Use F12 hotkey to scan each page manually!",
error=True
)
# Play alert sound
self._play_beep()