fix: Add Arch Master rank and Reset button to Skill Scanner

FIXES:
1. Added 'Arch Master' to the list of multi-word ranks
   - Multi-word ranks are now matched first to prevent partial matches
   - Changed rank matching order: ['Arch Master', 'Grand Master'] + single ranks
   - This fixes 'Laser Weaponry Technology Arch' being parsed incorrectly
   - Now correctly parses as: 'Laser Weaponry Technology', 'Arch Master', 8805

2. Added 'Reset Data' button to Skill Scanner plugin
   - Red button next to 'Scan Skills Window'
   - Shows confirmation dialog before clearing
   - Clears: skills_data, skill_gains, and the UI tables
   - Also clears the data file (skill_tracker.json)

3. Clean skill names by removing 'Skill' prefix
   - OCR sometimes reads 'Skill Laser Weaponry Technology'
   - Now strips 'Skill' or 'SKILL' prefix from skill names

4. Updated both Skill Scanner and Game Reader Test plugins
   - Both now use the same improved parsing logic
   - Both handle multi-word ranks correctly
This commit is contained in:
LemonNexus 2026-02-15 00:23:13 +00:00
parent bf42c2a1b7
commit 1538508b63
2 changed files with 72 additions and 21 deletions

View File

@ -674,14 +674,16 @@ class GameReaderTestPlugin(BasePlugin):
"""Parse skills from OCR text.""" """Parse skills from OCR text."""
skills = {} skills = {}
# Ranks in Entropia Universe # Ranks in Entropia Universe - multi-word first for proper matching
RANKS = [ SINGLE_RANKS = [
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
'Skilled', 'Expert', 'Professional', 'Master', 'Grand Master', 'Skilled', 'Expert', 'Professional', 'Master',
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
] ]
rank_pattern = '|'.join(RANKS) MULTI_RANKS = ['Arch Master', 'Grand Master']
ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
rank_pattern = '|'.join(ALL_RANKS)
# Clean text # Clean text
text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
@ -705,6 +707,9 @@ class GameReaderTestPlugin(BasePlugin):
rank = match.group(2) rank = match.group(2)
points = int(match.group(3)) points = int(match.group(3))
# Clean skill name - remove "Skill" prefix
skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
if points > 0 and skill_name: if points > 0 and skill_name:
skills[skill_name] = {'rank': rank, 'points': points} skills[skill_name] = {'rank': rank, 'points': points}

View File

@ -54,26 +54,26 @@ class SkillOCRThread(QThread):
"""Parse skill data from OCR text with improved handling for 3-column layout.""" """Parse skill data from OCR text with improved handling for 3-column layout."""
skills = {} skills = {}
# Ranks in Entropia Universe (in order) # Ranks in Entropia Universe (including multi-word ranks)
RANKS = [ # Single word ranks
SINGLE_RANKS = [
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
'Skilled', 'Expert', 'Professional', 'Master', 'Grand Master', 'Skilled', 'Expert', 'Professional', 'Master',
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
] ]
rank_pattern = '|'.join(RANKS) # 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 # Clean up the text - remove common headers and junk
text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '') text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
text = text.replace('Attributes', '').replace('COMBAT', '').replace('Design', '')
text = text.replace('Construction', '').replace('Defense', '').replace('General', '')
text = text.replace('Handgun', '').replace('Heavy Melee Weapons', '')
text = text.replace('Information', '').replace('Inflict Melee Damage', '')
text = text.replace('Inflict Ranged Damage', '').replace('Light Melee Weapons', '')
text = text.replace('Longblades', '').replace('Medical', '').replace('Mining', '')
text = text.replace('Science', '').replace('Social', '').replace('Beauty', '')
text = text.replace('Mindforce', '')
lines = text.split('\n') lines = text.split('\n')
@ -88,6 +88,7 @@ class SkillOCRThread(QThread):
# Try pattern: SkillName Rank Points # Try pattern: SkillName Rank Points
# More flexible pattern to handle merged text # 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( match = re.search(
rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
line, re.IGNORECASE line, re.IGNORECASE
@ -98,11 +99,12 @@ class SkillOCRThread(QThread):
rank = match.group(2) rank = match.group(2)
points = int(match.group(3)) points = int(match.group(3))
# Clean up skill name # 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() skill_name = skill_name.strip()
# Validate - points should be reasonable (not too small) # Validate - points should be reasonable (not too small)
if points > 0: if points > 0 and skill_name:
skills[skill_name] = { skills[skill_name] = {
'rank': rank, 'rank': rank,
'points': points, 'points': points,
@ -112,7 +114,7 @@ class SkillOCRThread(QThread):
# Alternative parsing: try to find skill-rank-points triplets # Alternative parsing: try to find skill-rank-points triplets
if not skills: if not skills:
skills = self._parse_skills_alternative(text, RANKS) skills = self._parse_skills_alternative(text, ALL_RANKS)
return skills return skills
@ -123,13 +125,16 @@ class SkillOCRThread(QThread):
# Find all rank positions in the text # Find all rank positions in the text
for rank in ranks: for rank in ranks:
# Look for pattern: [text] [Rank] [number] # Look for pattern: [text] [Rank] [number]
pattern = rf'([A-Z][a-z]{{2,}}(?:\s+[A-Z][a-z]{{2,}}){{0,3}})\s+{rank}\s+(\d{{1,6}})' 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) matches = re.finditer(pattern, text, re.IGNORECASE)
for match in matches: for match in matches:
skill_name = match.group(1).strip() skill_name = match.group(1).strip()
points = int(match.group(2)) 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: if points > 0 and len(skill_name) > 2:
skills[skill_name] = { skills[skill_name] = {
'rank': rank, 'rank': rank,
@ -216,6 +221,9 @@ class SkillScannerPlugin(BasePlugin):
scan_group = QGroupBox("OCR Scan (Core Service)") scan_group = QGroupBox("OCR Scan (Core Service)")
scan_layout = QVBoxLayout(scan_group) scan_layout = QVBoxLayout(scan_group)
# Buttons row
buttons_layout = QHBoxLayout()
scan_btn = QPushButton("Scan Skills Window") scan_btn = QPushButton("Scan Skills Window")
scan_btn.setStyleSheet(""" scan_btn.setStyleSheet("""
QPushButton { QPushButton {
@ -228,7 +236,23 @@ class SkillScannerPlugin(BasePlugin):
} }
""") """)
scan_btn.clicked.connect(self._scan_skills) scan_btn.clicked.connect(self._scan_skills)
scan_layout.addWidget(scan_btn) 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 = QLabel("Ready to scan")
self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);") self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
@ -312,8 +336,30 @@ class SkillScannerPlugin(BasePlugin):
self._refresh_skills_table() self._refresh_skills_table()
self.scan_progress.setText(f"Found {len(skills_data)} skills") 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): def _on_scan_error(self, error):
self.scan_progress.setText(f"Error: {error}") self.scan_progress.setText(f"Error: {error}")
self.scan_progress.setText(f"Error: {error}")
def _refresh_skills_table(self): def _refresh_skills_table(self):
self.skills_table.setRowCount(len(self.skills_data)) self.skills_table.setRowCount(len(self.skills_data))