""" 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 ) from PyQt6.QtCore import Qt, QThread, pyqtSignal 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', '') lines = text.split('\n') for line in lines: line = line.strip() if not line: continue # Skip category headers and short lines if len(line) < 10: continue # Try pattern: SkillName Rank Points # More flexible pattern to handle merged text # Skill name can be 2-50 chars, rank from our list, points 1-6 digits match = re.search( rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', line, re.IGNORECASE ) if match: 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: skills[skill_name] = { 'rank': rank, 'points': points, 'scanned_at': datetime.now().isoformat() } print(f"[SkillScanner] Parsed: {skill_name} = {rank} ({points})") # Alternative parsing: try to find skill-rank-points triplets 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() # 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) # 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))))