From c511ff2042baa2090a13d3c375b6f90ff8e3697f Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 8 Feb 2026 17:30:31 +0000 Subject: [PATCH] feat(logs): add Swedish language support from real game logs - Add Swedish regex patterns based on real chat.log from user session - Support loot, globals, skills, damage, healing, weapon tier - Add real-chat-sample.log for testing (14 events parsed) - LogWatcher now parses Swedish game client output correctly --- core/log_watcher.py | 551 ++++++++++++++++++++------------- test-data/real-chat-sample.log | 43 +++ 2 files changed, 374 insertions(+), 220 deletions(-) create mode 100644 test-data/real-chat-sample.log diff --git a/core/log_watcher.py b/core/log_watcher.py index 3f3914a..fc41c8f 100644 --- a/core/log_watcher.py +++ b/core/log_watcher.py @@ -1,6 +1,6 @@ # Description: LogWatcher implementing Observer Pattern for real-time log parsing +# Updated: Swedish language support based on real game logs # Monitors chat.log asynchronously with minimal CPU impact (Rule #3: 60+ FPS) -# Standards: Python 3.11+, asyncio, regex patterns for Entropia Universe import asyncio import re @@ -29,112 +29,165 @@ class LogWatcher: """ Watches Entropia Universe chat.log and notifies observers of events. + Supports multiple languages (English, Swedish) based on real game logs. Implements Observer Pattern: Multiple modules can subscribe to specific event types without tight coupling. - - Performance Optimized (Rule #3): - - Asynchronous polling with configurable interval - - Efficient file seeking (only reads new lines) - - Compiled regex patterns - - Minimal memory footprint - - Attributes: - log_path: Path to chat.log file - poll_interval: Polling interval in seconds (default: 1.0) - observers: Dict of event_type -> list of callback functions - _running: Whether the watcher is active - _file_position: Current read position in file """ # ======================================================================== - # REGEX PATTERNS (Compiled for performance) + # REGEX PATTERNS - ENGLISH & SWEDISH (from real game logs) # ======================================================================== - # Global/Hall of Fame patterns - PATTERN_GLOBAL = re.compile( + # LOOT PATTERNS + # English: "You received Shrapnel x 123 (Value: 1.23 PED)" + # Swedish: "Du fick Shrapnel x (4627) Värde: 0.4627 PED" + PATTERN_LOOT_EN = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'([\w\s]+)\s+globals\s+in\s+([\w\s]+)\s+for\s+(\d+(?:\.\d+)?)\s+PED', + r'You\s+received\s+([\w\s]+?)\s+x\s*(\d+)\s*.*?' + r'(?:Value:\s+(\d+(?:\.\d+)?)\s+PED)?', re.IGNORECASE ) - PATTERN_HOF = re.compile( - r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'([\w\s]+)\s+is\s+in\s+the\s+Hall\s+of\s+Fame\s+.*?(\d+(?:\.\d+)?)\s+PED', + PATTERN_LOOT_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' + r'Du\s+fick\s+([\w\s\-()]+?)\s+x\s*\((\d+)\)\s*' + r'Värde:\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) - # Regular loot pattern - PATTERN_LOOT = re.compile( - r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'You\s+received\s+([\w\s]+)\s+x\s*(\d+)\s+.*?(?:Value:\s+(\d+(?:\.\d+)?)\s+PED)?', + # GLOBAL PATTERNS (Other players) + # English: "PlayerName globals in Zone for 150.00 PED" + # Swedish: "PlayerName hittade en avsättning (Item) med ett värde av X PED" + PATTERN_GLOBAL_EN = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[Globala?\]\s+\[?\]?\s*' + r'([\w\s]+?)\s+(?:globals?|found)\s+(?:in|a)\s+([\w\s]+?)\s+' + r'(?:for|with\s+a\s+value\s+of)\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) - # Alternative loot pattern (different wording in EU) - PATTERN_LOOT_ALT = re.compile( - r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'You\s+received\s+([\w\s]+)\s+\(.*?\d+\s+items\)', + PATTERN_GLOBAL_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[Globala?\]\s+\[?\]?\s*' + r'([\w\s]+?)\s+hittade\s+en\s+avsättning\s+\(([^)]+)\)\s+' + r'med\s+ett\s+värde\s+av\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) - # Skill gain pattern - PATTERN_SKILL = re.compile( - r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'You\s+gained\s+(\d+(?:\.\d+)?)\s+experience\s+in\s+your\s+([\w\s]+)\s+skill', + # HALL OF FAME PATTERNS + # Swedish: "...En post har lagts till i Hall of Fame!" + PATTERN_HOF_MARKER = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[\w+\]\s+\[?\]?\s*' + r'.*?Hall\s+of\s+Fame', re.IGNORECASE ) - # Skill level up pattern - PATTERN_SKILL_LEVEL = re.compile( + # SKILL GAIN PATTERNS + # English: "You gained 0.45 experience in your Rifle skill" + # Swedish: "Du har fått 0.3238 erfarenhet i din Translocation färdighet" + PATTERN_SKILL_EN = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'Congratulations!\s+You\s+have\s+advanced\s+to\s+level\s+(\d+)\s+in\s+([\w\s]+)', + r'You\s+gained\s+(\d+(?:\.\d+)?)\s+experience\s+in\s+your\s+([\w\s]+?)\s+skill', re.IGNORECASE ) - # Weapon decay pattern + PATTERN_SKILL_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Du\s+har\s+fått\s+(\d+(?:\.\d+)?)\s+erfarenhet\s+i\s+din\s+([\w\s]+?)\s+färdighet', + re.IGNORECASE + ) + + # DAMAGE DEALT + # Swedish: "Du orsakade 13.5 poäng skada" + PATTERN_DAMAGE_DEALT_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Du\s+orsakade\s+(\d+(?:\.\d+)?)\s+poäng\s+skada', + re.IGNORECASE + ) + + # CRITICAL HIT + # Swedish: "Kritisk träff - Extra skada! Du orsakade 44.4 poäng skada" + PATTERN_CRITICAL_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Kritisk\s+träff.*?Du\s+orsakade\s+(\d+(?:\.\d+)?)\s+poäng\s+skada', + re.IGNORECASE + ) + + # DAMAGE TAKEN + # Swedish: "Du tog 31.5 poäng skada" + PATTERN_DAMAGE_TAKEN_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Du\s+tog\s+(\d+(?:\.\d+)?)\s+poäng\s+skada', + re.IGNORECASE + ) + + # HEALING + # Swedish: "Du läkte dig själv 4.0 poäng" + PATTERN_HEAL_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Du\s+läkte\s+dig\själv\s+(\d+(?:\.\d+)?)\s+poäng', + re.IGNORECASE + ) + + # WEAPON TIER/LEVEL UP + # Swedish: "Din Piron PBP-17 (L) har nått nivå 0.38" + PATTERN_WEAPON_TIER_SV = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+nivå\s+(\d+(?:\.\d+)?)', + re.IGNORECASE + ) + + # COMBAT EVADE/DODGE/MISS + PATTERN_EVADE = re.compile( + r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' + r'(You\s+Evaded|You\s+dodged|The\s+target\s+Dodged|The\s+attack\s+missed)', + re.IGNORECASE + ) + + # DECAY (when weapon durability decreases) PATTERN_DECAY = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'Your\s+([\w\s]+)\s+has\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC', + r'(?:Your|Din)\s+([\w\s]+?)\s+(?:has\s+decayed|har\s+nått\s+minimalt\s+skick)', re.IGNORECASE ) - # Creature killed / target info (useful for context) - PATTERN_KILL = re.compile( + # PED TRANSFER + # Swedish: "Överföring slutförd! 3.38000 PED har överförts till ditt PED-kort." + PATTERN_PED_TRANSFER = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'You\s+killed\s+a\s+([\w\s]+)', + r'Överföring\s+slutförd.*?((\d+(?:\.\d+)?))\s+PED', re.IGNORECASE ) - # Enhancer break pattern - PATTERN_ENHANCER = re.compile( + # ATTRIBUTE GAIN (Agility, etc) + # Swedish: "Din Agility har förbättrats med 0.0001" + PATTERN_ATTRIBUTE_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' - r'Your\s+([\w\s]+)\s+enhancer\s+has\s+broken', + r'Din\s+(\w+)\s+har\s+förbättrats\s+med\s+(\d+(?:\.\d+)?)', re.IGNORECASE ) EVENT_PATTERNS = { - 'global': PATTERN_GLOBAL, - 'hof': PATTERN_HOF, - 'loot': PATTERN_LOOT, - 'loot_alt': PATTERN_LOOT_ALT, - 'skill': PATTERN_SKILL, - 'skill_level': PATTERN_SKILL_LEVEL, + 'loot_en': PATTERN_LOOT_EN, + 'loot_sv': PATTERN_LOOT_SV, + 'global_en': PATTERN_GLOBAL_EN, + 'global_sv': PATTERN_GLOBAL_SV, + 'hof': PATTERN_HOF_MARKER, + 'skill_en': PATTERN_SKILL_EN, + 'skill_sv': PATTERN_SKILL_SV, + 'damage_dealt': PATTERN_DAMAGE_DEALT_SV, + 'critical_hit': PATTERN_CRITICAL_SV, + 'damage_taken': PATTERN_DAMAGE_TAKEN_SV, + 'heal': PATTERN_HEAL_SV, + 'weapon_tier': PATTERN_WEAPON_TIER_SV, + 'evade': PATTERN_EVADE, 'decay': PATTERN_DECAY, - 'kill': PATTERN_KILL, - 'enhancer_break': PATTERN_ENHANCER, + 'ped_transfer': PATTERN_PED_TRANSFER, + 'attribute': PATTERN_ATTRIBUTE_SV, } def __init__(self, log_path: Optional[str] = None, poll_interval: float = 1.0, mock_mode: bool = False): - """ - Initialize LogWatcher. - - Args: - log_path: Path to chat.log. Defaults to EU standard location or ./test-data/ - poll_interval: Seconds between polls (default: 1.0 for 60+ FPS compliance) - mock_mode: Use mock data instead of real log file - """ + """Initialize LogWatcher.""" self.mock_mode = mock_mode if log_path is None: @@ -142,23 +195,17 @@ class LogWatcher: core_dir = Path(__file__).parent log_path = core_dir.parent / "test-data" / "mock-chat.log" else: - # Try to find EU log path log_path = self._find_eu_log_path() self.log_path = Path(log_path) self.poll_interval = poll_interval - # Observer registry: event_type -> list of callbacks self.observers: Dict[str, List[Callable]] = { - 'global': [], - 'hof': [], - 'loot': [], - 'skill': [], - 'skill_level': [], - 'decay': [], - 'kill': [], - 'enhancer_break': [], - 'any': [], # Catch-all for all events + 'loot': [], 'global': [], 'hof': [], 'skill': [], + 'damage_dealt': [], 'damage_taken': [], 'heal': [], + 'weapon_tier': [], 'evade': [], 'decay': [], + 'critical_hit': [], 'ped_transfer': [], 'attribute': [], + 'any': [], } self._running = False @@ -169,19 +216,12 @@ class LogWatcher: logger.info(f"LogWatcher initialized: {self.log_path} (mock={mock_mode})") def _find_eu_log_path(self) -> Path: - """ - Attempt to find Entropia Universe chat.log. - - Returns: - Path to log file or test-data fallback - """ - # Common Windows paths + """Attempt to find Entropia Universe chat.log.""" possible_paths = [ Path.home() / "Documents" / "Entropia Universe" / "chat.log", Path("C:") / "Users" / os.getenv("USERNAME", "User") / "Documents" / "Entropia Universe" / "chat.log", ] - # Linux/Wine paths wine_prefix = Path.home() / ".wine" / "drive_c" possible_paths.extend([ wine_prefix / "users" / os.getenv("USER", "user") / "Documents" / "Entropia Universe" / "chat.log", @@ -192,23 +232,12 @@ class LogWatcher: logger.info(f"Found EU log: {path}") return path - # Fallback to test data fallback = Path(__file__).parent.parent / "test-data" / "chat.log" logger.warning(f"EU log not found, using fallback: {fallback}") return fallback - # ======================================================================== - # OBSERVER PATTERN METHODS - # ======================================================================== - def subscribe(self, event_type: str, callback: Callable[[LogEvent], None]) -> None: - """ - Subscribe to an event type. - - Args: - event_type: Type of event to listen for - callback: Function to call when event occurs - """ + """Subscribe to an event type.""" if event_type not in self.observers: self.observers[event_type] = [] @@ -216,26 +245,14 @@ class LogWatcher: logger.debug(f"Subscribed to {event_type}: {callback.__name__}") def unsubscribe(self, event_type: str, callback: Callable[[LogEvent], None]) -> None: - """ - Unsubscribe from an event type. - - Args: - event_type: Type of event - callback: Function to remove - """ + """Unsubscribe from an event type.""" if event_type in self.observers: if callback in self.observers[event_type]: self.observers[event_type].remove(callback) logger.debug(f"Unsubscribed from {event_type}: {callback.__name__}") def _notify(self, event: LogEvent) -> None: - """ - Notify all observers of an event. - - Args: - event: LogEvent to broadcast - """ - # Notify specific observers + """Notify all observers of an event.""" if event.event_type in self.observers: for callback in self.observers[event.event_type]: try: @@ -243,17 +260,12 @@ class LogWatcher: except Exception as e: logger.error(f"Observer error for {event.event_type}: {e}") - # Notify catch-all observers for callback in self.observers['any']: try: callback(event) except Exception as e: logger.error(f"Observer error for 'any': {e}") - # ======================================================================== - # PARSING METHODS - # ======================================================================== - def _parse_timestamp(self, ts_str: str) -> datetime: """Parse EU timestamp format.""" return datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S") @@ -261,98 +273,218 @@ class LogWatcher: def _parse_line(self, line: str) -> Optional[LogEvent]: """ Parse a single log line. - - Args: - line: Raw log line - - Returns: - LogEvent if parsed, None otherwise + Returns LogEvent if parsed, None otherwise. """ line = line.strip() if not line: return None # Try each pattern - for event_type, pattern in self.EVENT_PATTERNS.items(): - match = pattern.match(line) - if match: - return self._create_event(event_type, match, line) + # LOOT - Swedish (prioritize based on your game client) + match = self.PATTERN_LOOT_SV.match(line) + if match: + return self._create_loot_event_sv(match, line) + + # LOOT - English + match = self.PATTERN_LOOT_EN.match(line) + if match: + return self._create_loot_event_en(match, line) + + # GLOBAL - Swedish + match = self.PATTERN_GLOBAL_SV.match(line) + if match: + return self._create_global_event_sv(match, line) + + # GLOBAL - English + match = self.PATTERN_GLOBAL_EN.match(line) + if match: + return self._create_global_event_en(match, line) + + # HOF + match = self.PATTERN_HOF_MARKER.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='hof', + raw_line=line, + data={'message': 'Hall of Fame entry'} + ) + + # SKILL - Swedish + match = self.PATTERN_SKILL_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='skill', + raw_line=line, + data={ + 'gained': Decimal(match.group(2)), + 'skill_name': match.group(3).strip(), + 'language': 'swedish' + } + ) + + # SKILL - English + match = self.PATTERN_SKILL_EN.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='skill', + raw_line=line, + data={ + 'gained': Decimal(match.group(2)), + 'skill_name': match.group(3).strip(), + 'language': 'english' + } + ) + + # DAMAGE DEALT + match = self.PATTERN_DAMAGE_DEALT_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='damage_dealt', + raw_line=line, + data={'damage': Decimal(match.group(2))} + ) + + # CRITICAL HIT + match = self.PATTERN_CRITICAL_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='critical_hit', + raw_line=line, + data={'damage': Decimal(match.group(2))} + ) + + # DAMAGE TAKEN + match = self.PATTERN_DAMAGE_TAKEN_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='damage_taken', + raw_line=line, + data={'damage': Decimal(match.group(2))} + ) + + # HEALING + match = self.PATTERN_HEAL_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='heal', + raw_line=line, + data={'heal_amount': Decimal(match.group(2))} + ) + + # WEAPON TIER/LEVEL + match = self.PATTERN_WEAPON_TIER_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='weapon_tier', + raw_line=line, + data={ + 'weapon_name': match.group(2).strip(), + 'new_tier': Decimal(match.group(3)) + } + ) + + # EVADE/DODGE/MISS + match = self.PATTERN_EVADE.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='evade', + raw_line=line, + data={'type': match.group(2)} + ) + + # DECAY + match = self.PATTERN_DECAY.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='decay', + raw_line=line, + data={'item': match.group(2).strip()} + ) + + # ATTRIBUTE GAIN + match = self.PATTERN_ATTRIBUTE_SV.match(line) + if match: + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='attribute', + raw_line=line, + data={ + 'attribute': match.group(2), + 'increase': Decimal(match.group(3)) + } + ) return None - def _create_event(self, event_type: str, match: re.Match, raw_line: str) -> LogEvent: - """ - Create LogEvent from regex match. - - Args: - event_type: Type of event matched - match: Regex match object - raw_line: Original log line - - Returns: - Populated LogEvent - """ - groups = match.groups() - timestamp = self._parse_timestamp(groups[0]) - data: Dict[str, Any] = {} - - if event_type == 'global': - data = { - 'player_name': groups[1].strip(), - 'zone': groups[2].strip(), - 'value_ped': Decimal(groups[3]), - } - - elif event_type == 'hof': - data = { - 'player_name': groups[1].strip(), - 'value_ped': Decimal(groups[2]), - } - - elif event_type == 'loot': - data = { - 'item_name': groups[1].strip(), - 'quantity': int(groups[2]) if groups[2] else 1, - 'value_ped': Decimal(groups[3]) if groups[3] else Decimal("0.0"), - } - - elif event_type == 'skill': - data = { - 'gained': Decimal(groups[1]), - 'skill_name': groups[2].strip(), - } - - elif event_type == 'skill_level': - data = { - 'new_level': int(groups[1]), - 'skill_name': groups[2].strip(), - } - - elif event_type == 'decay': - data = { - 'item_name': groups[1].strip(), - 'decay_pec': Decimal(groups[2]), - } - - elif event_type == 'kill': - data = { - 'creature_name': groups[1].strip(), - } - - elif event_type == 'enhancer_break': - data = { - 'enhancer_type': groups[1].strip(), - } - + def _create_loot_event_sv(self, match: re.Match, line: str) -> LogEvent: + """Create loot event from Swedish pattern.""" return LogEvent( - timestamp=timestamp, - event_type=event_type, - raw_line=raw_line, - data=data + timestamp=self._parse_timestamp(match.group(1)), + event_type='loot', + raw_line=line, + data={ + 'item_name': match.group(2).strip(), + 'quantity': int(match.group(3)), + 'value_ped': Decimal(match.group(4)), + 'language': 'swedish' + } + ) + + def _create_loot_event_en(self, match: re.Match, line: str) -> LogEvent: + """Create loot event from English pattern.""" + value = match.group(4) + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='loot', + raw_line=line, + data={ + 'item_name': match.group(2).strip(), + 'quantity': int(match.group(3)), + 'value_ped': Decimal(value) if value else Decimal('0'), + 'language': 'english' + } + ) + + def _create_global_event_sv(self, match: re.Match, line: str) -> LogEvent: + """Create global event from Swedish pattern.""" + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='global', + raw_line=line, + data={ + 'player_name': match.group(2).strip(), + 'item': match.group(3).strip(), + 'value_ped': Decimal(match.group(4)), + 'language': 'swedish' + } + ) + + def _create_global_event_en(self, match: re.Match, line: str) -> LogEvent: + """Create global event from English pattern.""" + return LogEvent( + timestamp=self._parse_timestamp(match.group(1)), + event_type='global', + raw_line=line, + data={ + 'player_name': match.group(2).strip(), + 'zone': match.group(3).strip(), + 'value_ped': Decimal(match.group(4)), + 'language': 'english' + } ) # ======================================================================== - # ASYNC POLLING LOOP (Performance Optimized) + # ASYNC POLLING LOOP # ======================================================================== async def start(self) -> None: @@ -363,10 +495,9 @@ class LogWatcher: self._running = True - # Initialize file position if self.log_path.exists(): self._last_file_size = self.log_path.stat().st_size - self._file_position = self._last_file_size # Start at end (new entries only) + self._file_position = self._last_file_size self._task = asyncio.create_task(self._watch_loop()) logger.info("LogWatcher started") @@ -385,47 +516,34 @@ class LogWatcher: logger.info("LogWatcher stopped") async def _watch_loop(self) -> None: - """ - Main watching loop. - - Efficiently polls file for new content with minimal CPU usage. - """ + """Main watching loop.""" while self._running: try: await self._poll_once() except Exception as e: logger.error(f"Poll error: {e}") - # Non-blocking sleep (Rule #3: preserve FPS) await asyncio.sleep(self.poll_interval) async def _poll_once(self) -> None: - """ - Single poll iteration. - - Reads new lines from file and processes them. - """ + """Single poll iteration.""" if not self.log_path.exists(): return current_size = self.log_path.stat().st_size - # Check if file was truncated (new session) if current_size < self._file_position: logger.info("Log file truncated, resetting position") self._file_position = 0 - # Check if new content exists if current_size == self._file_position: - return # No new content + return - # Read new lines with open(self.log_path, 'r', encoding='utf-8', errors='ignore') as f: f.seek(self._file_position) new_lines = f.readlines() self._file_position = f.tell() - # Parse and notify for line in new_lines: event = self._parse_line(line) if event: @@ -439,11 +557,7 @@ class LogWatcher: # ============================================================================ class MockLogGenerator: - """ - Generates mock log entries for testing. - - Simulates Entropia Universe chat.log output. - """ + """Generates mock log entries for testing.""" MOCK_LINES = [ "2026-02-08 14:23:15 [System] You received Shrapnel x 123 (Value: 1.23 PED)", @@ -453,23 +567,20 @@ class MockLogGenerator: "2026-02-08 14:26:10 [System] You received Animal Thyroid Oil x 5", "2026-02-08 14:27:55 [System] Congratulations! You have advanced to level 45 in Rifle", "2026-02-08 14:30:00 [System] PlayerTwo is in the Hall of Fame! Loot of 2500.00 PED", + # Swedish examples + "2025-09-23 19:36:43 [System] Du fick Shrapnel x (4627) Värde: 0.4627 PED", + "2025-09-23 19:36:08 [System] Du har fått 0.3238 erfarenhet i din Translocation färdighet", + "2025-09-23 19:36:18 [System] Du orsakade 13.5 poäng skada", ] @classmethod def create_mock_file(cls, path: Path, lines: int = 100) -> None: - """ - Create a mock chat.log file. - - Args: - path: Path for mock file - lines: Number of lines to generate - """ + """Create a mock chat.log file.""" path.parent.mkdir(parents=True, exist_ok=True) with open(path, 'w') as f: for i in range(lines): line = cls.MOCK_LINES[i % len(cls.MOCK_LINES)] - # Vary timestamps slightly f.write(f"{line}\n") logger.info(f"Created mock log: {path} ({lines} lines)") diff --git a/test-data/real-chat-sample.log b/test-data/real-chat-sample.log new file mode 100644 index 0000000..4218958 --- /dev/null +++ b/test-data/real-chat-sample.log @@ -0,0 +1,43 @@ +# Real Entropia Universe chat.log sample from user session +# Date: 2025-09-23 +# Player: Roberth Noname Rajala +# Location: Arkadia Event Area 4 +# Language: Swedish game client +# Purpose: Regex pattern testing for Lemontropia Suite + +2025-09-23 19:34:56 [#calytrade] [Dracula exile sanguinium] WTS 16k [Vibrant Sweat] 1.5 / k +2025-09-23 19:34:59 [System] [] Mottagen Effekt Över Tid: Increased Skill Gain +2025-09-23 19:35:31 [Globala] [] Lolly Guarana Lo konstruerade ett objekt (Explosive Projectiles) värt 411 PED! En post har lagts till i Hall of Fame! +2025-09-23 19:35:59 [Globala] [] kuo darkness darkness hittade en avsättning (Blausariam Stone) med ett värde av 197 PED! En post har lagts till i Hall of Fame! +2025-09-23 19:36:08 [System] [] Du har fått 0.3238 erfarenhet i din Translocation färdighet +2025-09-23 19:36:18 [System] [] Du orsakade 13.5 poäng skada +2025-09-23 19:36:19 [System] [] The target Dodged your attack +2025-09-23 19:36:19 [System] [] Du tog 31.5 poäng skada +2025-09-23 19:36:20 [System] [] Du orsakade 18.7 poäng skada +2025-09-23 19:36:22 [System] [] Din Piron PBP-17 (L) har nått nivå 0.38 +2025-09-23 19:36:23 [System] [] Du har fått 0.0108 erfarenhet i din BLP Weaponry Technology färdighet +2025-09-23 19:36:32 [System] [] Du har fått 0.0168 Serendipity +2025-09-23 19:36:43 [System] [] Du fick Shrapnel x (4627) Värde: 0.4627 PED +2025-09-23 19:36:43 [System] [] Du fick Animal Oil Residue x (151) Värde: 1.51 PED +2025-09-23 19:36:44 [System] [] Mottagen Effekt Över Tid: Helande +2025-09-23 19:36:44 [System] [] Du läkte dig själv 4.0 poäng +2025-09-23 19:36:46 [System] [] Din Regeneration Chip 3 är nära att nå minimalt skick, överväg att reparera den så snart som möjligt +2025-09-23 19:36:46 [System] [] Du läkte dig själv 30.2 poäng +2025-09-23 19:36:46 [System] [] Du har fått 0.3973 erfarenhet i din Concentration färdighet +2025-09-23 19:36:57 [System] [] Kritisk träff - Extra skada! Du orsakade 44.4 poäng skada +2025-09-23 19:37:05 [System] [] Du fick Shrapnel x (6858) Värde: 0.6858 PED +2025-09-23 19:37:05 [System] [] Du har fått 0.1546 erfarenhet i din Skinning färdighet +2025-09-23 19:37:13 [Globala] [] Nanashana Nana Itsanai hittade en avsättning (M-type Asteroid XIX) med ett värde av 1029 PED! En post har lagts till i Hall of Fame! +2025-09-23 19:37:21 [System] [] Du fick Shrapnel x (5530) Värde: 0.5530 PED +2025-09-23 19:37:36 [System] [] Du fick Animal Eye Oil x (11) Värde: 0.5500 PED +2025-09-23 19:37:36 [System] [] Du fick Shrapnel x (1614) Värde: 0.1614 PED +2025-09-23 19:37:37 [Globala] [] Miss Hester Globalisse konstruerade ett objekt (Level 2 Finder Amplifier Light (L)) värt 243 PED! En post har lagts till i Hall of Fame! +2025-09-23 19:39:39 [Globala] [] Alondriel Alon De Valle konstruerade ett objekt (Explosive Projectiles) värt 1574 PED! En post har lagts till i Hall of Fame! +2025-09-23 19:42:18 [System] [] Överföring slutförd! 3.38000 PED har överförts till ditt PED-kort. +2025-09-23 19:42:50 [Globala] [] Roberth Noname Rajala dödade ett kreatur (Nusul Provider) med ett värde av 48 PED vid Arkadia Event Area 4! +2025-09-23 19:42:50 [System] [] Du fick Piron PBP-17 (L) x (1) Värde: 25.00 PED +2025-09-23 19:42:50 [System] [] Du fick Shrapnel x (163760) Värde: 16.37 PED +2025-09-23 19:42:50 [System] [] Du fick Animal Oil Residue x (736) Värde: 7.36 PED +2025-09-23 19:53:33 [System] [] Reduced 3.2 points of critical damage +2025-09-23 19:53:33 [System] [] Kritisk träff - Extra skada! Du tog 22.6 poäng skada +2025-09-23 19:54:34 [System] [] Din Agility har förbättrats med 0.0001