feat: Constrain skill scanner to only Entropia game window
BUG: OCR was reading text from Discord, EU-Utility UI, and other windows. FIX: 1. Added find_entropia_window() - Uses win32gui + psutil on Windows to find the game window by process name 'Entropia.exe' and window title containing 'Entropia Universe'. Returns (left, top, width, height). 2. Added capture_entropia_region() - Captures only the game window region, falls back to full screen if window not found. 3. Added is_valid_skill_text() - Filters out non-game text patterns: - Discord, Event Bus, Game Reader, Test, Page Scanner, HOTKEY MODE - UI elements like 'Skill Tracker', 'Calculator', 'Nexus Search' - Debug text like '[SkillScanner]', 'Parsed:', 'Cleared' - Process names like 'Entropia.exe', 'Client (64 bit)', 'Arkadia' - Lines with >10 words (skills aren't that long) 4. Added recognize_image() method to OCRService for convenience. 5. Modified SkillOCRThread.run() to: - Capture only Entropia window - Filter text before parsing - Use _parse_skills_filtered() which validates each line 6. Added _parse_skills_filtered() method that: - Splits text by lines - Only keeps lines containing a valid rank - Validates each line with is_valid_skill_text() - Logs filtered lines for debugging RESULT: - Scanner now ONLY reads from the game window - Invalid text (Discord, UI, debug) is filtered out - Much cleaner skill parsing results Note: Window title varies by location (e.g., '[Arkadia]', '[Calypso]') but process name is always 'Entropia.exe'.
This commit is contained in:
parent
05f8c06312
commit
0155eb0be0
|
|
@ -201,6 +201,19 @@ class OCRService:
|
|||
'results': []
|
||||
}
|
||||
|
||||
def recognize_image(self, image) -> Dict[str, Any]:
|
||||
"""
|
||||
Perform OCR on a PIL Image.
|
||||
Convenience alias for recognize(image=image).
|
||||
|
||||
Args:
|
||||
image: PIL Image to OCR
|
||||
|
||||
Returns:
|
||||
Dict with 'text', 'confidence', 'results', 'image_size'
|
||||
"""
|
||||
return self.recognize(image=image)
|
||||
|
||||
def _ocr_easyocr(self, image) -> Dict[str, Any]:
|
||||
"""OCR using EasyOCR."""
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -19,6 +19,153 @@ from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|||
from plugins.base_plugin import BasePlugin
|
||||
|
||||
|
||||
def find_entropia_window():
|
||||
"""
|
||||
Find the Entropia Universe game window.
|
||||
Returns (left, top, width, height) or None if not found.
|
||||
"""
|
||||
try:
|
||||
import platform
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
import win32gui
|
||||
import win32process
|
||||
import psutil
|
||||
|
||||
def callback(hwnd, windows):
|
||||
if not win32gui.IsWindowVisible(hwnd):
|
||||
return True
|
||||
|
||||
# Get window title
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
|
||||
# Check if it's an Entropia window (title contains "Entropia Universe")
|
||||
if 'Entropia Universe' in title:
|
||||
try:
|
||||
# Get process ID
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
process = psutil.Process(pid)
|
||||
|
||||
# Verify process name contains "Entropia"
|
||||
if 'entropia' in process.name().lower():
|
||||
rect = win32gui.GetWindowRect(hwnd)
|
||||
left, top, right, bottom = rect
|
||||
windows.append((left, top, right - left, bottom - top, title))
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
windows = []
|
||||
win32gui.EnumWindows(callback, windows)
|
||||
|
||||
if windows:
|
||||
# Return the largest window (most likely the main game)
|
||||
windows.sort(key=lambda w: w[2] * w[3], reverse=True)
|
||||
left, top, width, height, title = windows[0]
|
||||
print(f"[SkillScanner] Found Entropia window: '{title}' at ({left}, {top}, {width}, {height})")
|
||||
return (left, top, width, height)
|
||||
|
||||
elif platform.system() == 'Linux':
|
||||
# Try using xdotool or wmctrl
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['xdotool', 'search', '--name', 'Entropia Universe'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
window_id = result.stdout.strip().split('\n')[0]
|
||||
# Get window geometry
|
||||
geo_result = subprocess.run(
|
||||
['xdotool', 'getwindowgeometry', window_id],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if geo_result.returncode == 0:
|
||||
# Parse: "Position: 100,200 (screen: 0)" and "Geometry: 1920x1080"
|
||||
pos_match = re.search(r'Position: (\d+),(\d+)', geo_result.stdout)
|
||||
geo_match = re.search(r'Geometry: (\d+)x(\d+)', geo_result.stdout)
|
||||
if pos_match and geo_match:
|
||||
left = int(pos_match.group(1))
|
||||
top = int(pos_match.group(2))
|
||||
width = int(geo_match.group(1))
|
||||
height = int(geo_match.group(2))
|
||||
print(f"[SkillScanner] Found Entropia window at ({left}, {top}, {width}, {height})")
|
||||
return (left, top, width, height)
|
||||
except Exception as e:
|
||||
print(f"[SkillScanner] Linux window detection failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SkillScanner] Window detection error: {e}")
|
||||
|
||||
print("[SkillScanner] Could not find Entropia window - will use full screen")
|
||||
return None
|
||||
|
||||
|
||||
def capture_entropia_region(region=None):
|
||||
"""
|
||||
Capture screen region of Entropia window.
|
||||
If region is None, tries to find the window automatically.
|
||||
Returns PIL Image or None.
|
||||
"""
|
||||
try:
|
||||
from PIL import ImageGrab
|
||||
|
||||
if region is None:
|
||||
region = find_entropia_window()
|
||||
|
||||
if region:
|
||||
left, top, width, height = region
|
||||
# Add some padding to ensure we capture everything
|
||||
# Don't go below 0
|
||||
left = max(0, left)
|
||||
top = max(0, top)
|
||||
screenshot = ImageGrab.grab(bbox=(left, top, left + width, top + height))
|
||||
print(f"[SkillScanner] Captured Entropia window region: {width}x{height}")
|
||||
return screenshot
|
||||
else:
|
||||
# Fallback to full screen
|
||||
screenshot = ImageGrab.grab()
|
||||
print("[SkillScanner] Captured full screen (window not found)")
|
||||
return screenshot
|
||||
|
||||
except Exception as e:
|
||||
print(f"[SkillScanner] Capture error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def is_valid_skill_text(text):
|
||||
"""
|
||||
Filter out non-game text from OCR results.
|
||||
Returns True if text looks like it could be from the game.
|
||||
"""
|
||||
# List of patterns that indicate NON-game text (UI, Discord, etc.)
|
||||
invalid_patterns = [
|
||||
'Discord', 'Presence', 'Event Bus', 'Example', 'Game Reader',
|
||||
'Test', 'Page Scanner', 'HOTKEY MODE', 'Skill Tracker',
|
||||
'Navigate', 'window', 'UI', 'Plugin', 'Settings',
|
||||
'Click', 'Button', 'Menu', 'Panel', 'Tab', 'Loading...',
|
||||
'Calculator', 'Nexus Search', 'Dashboard', 'Analytics',
|
||||
'Multi-Page', 'Scanner', 'Auto-detect', 'F12',
|
||||
'Cleared', 'Parsed:', '[SkillScanner]', 'INFO', 'DEBUG',
|
||||
'Loading', 'Initializing', 'Connecting', 'Error:', 'Warning:',
|
||||
'Entropia.exe', 'Client (64 bit)', 'Arkadia', 'Calypso',
|
||||
]
|
||||
|
||||
# Check for invalid patterns
|
||||
text_upper = text.upper()
|
||||
for pattern in invalid_patterns:
|
||||
if pattern.upper() in text_upper:
|
||||
return False
|
||||
|
||||
# Check for reasonable skill name length (not too long, not too short)
|
||||
words = text.split()
|
||||
if len(words) > 10: # Skills don't have 10+ words
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SkillOCRThread(QThread):
|
||||
"""OCR scan using core service."""
|
||||
scan_complete = pyqtSignal(dict)
|
||||
|
|
@ -30,21 +177,37 @@ class SkillOCRThread(QThread):
|
|||
self.ocr_service = ocr_service
|
||||
|
||||
def run(self):
|
||||
"""Perform OCR using core service."""
|
||||
"""Perform OCR using core service - only on Entropia window."""
|
||||
try:
|
||||
self.progress_update.emit("Capturing screen...")
|
||||
self.progress_update.emit("Finding Entropia window...")
|
||||
|
||||
# Use core OCR service
|
||||
result = self.ocr_service.recognize()
|
||||
# Capture only the Entropia game window
|
||||
screenshot = capture_entropia_region()
|
||||
|
||||
if screenshot is None:
|
||||
self.scan_error.emit("Could not capture game window")
|
||||
return
|
||||
|
||||
self.progress_update.emit("Running OCR on game window...")
|
||||
|
||||
# Use core OCR service with the captured image
|
||||
result = self.ocr_service.recognize_image(screenshot)
|
||||
|
||||
if 'error' in result and result['error']:
|
||||
self.scan_error.emit(result['error'])
|
||||
return
|
||||
|
||||
self.progress_update.emit("Parsing skills...")
|
||||
self.progress_update.emit("Filtering and parsing skills...")
|
||||
|
||||
# Filter out non-game text and parse skills
|
||||
raw_text = result.get('text', '')
|
||||
skills_data = self._parse_skills_filtered(raw_text)
|
||||
|
||||
if not skills_data:
|
||||
self.progress_update.emit("No valid skills found. Make sure Skills window is open.")
|
||||
else:
|
||||
self.progress_update.emit(f"Found {len(skills_data)} skills")
|
||||
|
||||
# Parse skills from text
|
||||
skills_data = self._parse_skills(result.get('text', ''))
|
||||
self.scan_complete.emit(skills_data)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -114,6 +277,50 @@ class SkillOCRThread(QThread):
|
|||
|
||||
return skills
|
||||
|
||||
def _parse_skills_filtered(self, text):
|
||||
"""
|
||||
Parse skills with filtering to remove non-game text.
|
||||
Only returns skills that pass validity checks.
|
||||
"""
|
||||
# First, split text into lines and filter each line
|
||||
lines = text.split('\n')
|
||||
valid_lines = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Check if line contains a rank (required for skill lines)
|
||||
has_rank = any(rank in line for rank in [
|
||||
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
|
||||
'Skilled', 'Expert', 'Professional', 'Master',
|
||||
'Arch Master', 'Grand Master', # Multi-word first
|
||||
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
|
||||
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
|
||||
])
|
||||
|
||||
if not has_rank:
|
||||
continue # Skip lines without ranks
|
||||
|
||||
# Check for invalid patterns
|
||||
if not is_valid_skill_text(line):
|
||||
print(f"[SkillScanner] Filtered out: '{line[:50]}...'")
|
||||
continue
|
||||
|
||||
valid_lines.append(line)
|
||||
|
||||
# Join valid lines and parse
|
||||
filtered_text = '\n'.join(valid_lines)
|
||||
|
||||
if not filtered_text.strip():
|
||||
print("[SkillScanner] No valid game text found after filtering")
|
||||
return {}
|
||||
|
||||
print(f"[SkillScanner] Filtered {len(lines)} lines to {len(valid_lines)} valid lines")
|
||||
|
||||
return self._parse_skills(filtered_text)
|
||||
|
||||
def _parse_skills_alternative(self, text, ranks):
|
||||
"""Alternative parser for when text is heavily merged."""
|
||||
skills = {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue