""" Log file parser for Entropia Universe chat.log =============================================== Parses EU chat.log file to extract: - Loot events - Skill gains - Chat messages - System messages - Globals/HoFs """ import re import time import logging import threading from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Optional, Callable, Dict, List, Any, Pattern @dataclass class LogEvent: """Represents a parsed log event.""" timestamp: datetime type: str data: Dict[str, Any] raw: str class LogParser: """Parser for Entropia Universe chat.log file. Monitors the log file and emits events for interesting game actions. Example: parser = LogParser(Path("C:/.../chat.log")) @parser.on_event def handle(event_type, data): if event_type == 'loot': print(f"Loot: {data['item']}") parser.start() """ # Regex patterns for different event types PATTERNS = { # Loot: "2024-01-15 14:30:25 [System] [] [Player] You received Angel Scales x1 Value: 150 PED" 'loot': re.compile( r'You received\s+(.+?)\s+x(\d+)\s+Value:\s+([\d.]+)' ), # Skill gain: "Skill increase: Rifle (Gain: 0.5234)" 'skill': re.compile( r'Skill increase:\s+(.+?)\s+\(Gain:\s+([\d.]+)\)' ), # Global/HoF: "[Global] [Player] found something rare! Angel Scales (Uber)" 'global': re.compile( r'\[Global\].*?found something rare!\s+(.+?)\s+\((\d+)\s*PED\)' ), # HoF (Hall of Fame): "[Hall of Fame] [Player] found something extraordinary!" 'hof': re.compile( r'\[Hall of Fame\].*?found something extraordinary!\s+(.+?)\s+\((\d+)\s*PED\)' ), # Chat message: "[Trade] [PlayerName]: Message text" 'chat': re.compile( r'\[(\w+)\]\s+\[(.+?)\]:\s+(.*)' ), # Damage dealt: "You inflicted 45.2 points of damage" 'damage_dealt': re.compile( r'You inflicted\s+([\d.]+)\s+points of damage' ), # Damage received: "You took 12.3 points of damage" 'damage_received': re.compile( r'You took\s+([\d.]+)\s+points of damage' ), # Enemy killed: "You killed [Creature Name]" 'kill': re.compile( r'You killed\s+\[(.+?)\]' ), # Item crafted: "You have successfully crafted [Item Name]" 'craft': re.compile( r'You have successfully crafted\s+\[(.+?)\]' ), # Mission completed: "Mission "Mission Name" completed!" 'mission_complete': re.compile( r'Mission\s+"(.+?)"\s+completed!' ), # Experience gain: "You gained 0.2345 experience in your Rifle skill" 'experience': re.compile( r'You gained\s+([\d.]+)\s+experience in your\s+(.+?)\s+skill' ), # Item looted (alt format): "You looted [Item Name]" 'loot_alt': re.compile( r'You looted\s+\[(.+?)\]\s*(?:x(\d+))?' ), } def __init__( self, log_path: Path, poll_interval: float = 0.5, encoding: str = 'utf-8' ): """Initialize log parser. Args: log_path: Path to chat.log file poll_interval: Seconds between file checks encoding: File encoding """ self.log_path = Path(log_path) self.poll_interval = poll_interval self.encoding = encoding self._running = False self._thread: Optional[threading.Thread] = None self._file_position = 0 self._callbacks: List[Callable] = [] self._logger = logging.getLogger("LogParser") # Track last position to detect new content self._last_size = 0 self._last_modified = 0 def start(self) -> bool: """Start parsing log file. Returns: True if started successfully """ if self._running: return True if not self.log_path.exists(): self._logger.error(f"Log file not found: {self.log_path}") return False self._running = True # Start at end of file (only read new content) try: self._last_size = self.log_path.stat().st_size self._file_position = self._last_size except Exception as e: self._logger.error(f"Failed to get file size: {e}") return False self._thread = threading.Thread(target=self._parse_loop, daemon=True) self._thread.start() self._logger.info(f"Started parsing {self.log_path}") return True def stop(self) -> None: """Stop parsing log file.""" self._running = False if self._thread: self._thread.join(timeout=2.0) self._logger.info("Stopped parsing") def on_event(self, callback: Callable[[str, Dict[str, Any]], None]) -> Callable: """Register event callback. Args: callback: Function(event_type, data) called for each event Returns: The callback function (for use as decorator) """ self._callbacks.append(callback) return callback def _emit(self, event_type: str, data: Dict[str, Any]) -> None: """Emit event to all callbacks.""" for callback in self._callbacks: try: callback(event_type, data) except Exception as e: self._logger.error(f"Error in callback: {e}") def _parse_loop(self) -> None: """Main parsing loop.""" while self._running: try: self._check_file() time.sleep(self.poll_interval) except Exception as e: self._logger.error(f"Parse error: {e}") time.sleep(self.poll_interval) def _check_file(self) -> None: """Check log file for new content.""" try: stat = self.log_path.stat() current_size = stat.st_size current_modified = stat.st_mtime # Check if file has new content if current_size == self._last_size: return if current_size < self._last_size: # File was truncated or rotated, start from beginning self._file_position = 0 self._last_size = current_size self._last_modified = current_modified # Read new content with open(self.log_path, 'r', encoding=self.encoding, errors='ignore') as f: f.seek(self._file_position) new_lines = f.readlines() self._file_position = f.tell() # Parse each new line for line in new_lines: self._parse_line(line.strip()) except Exception as e: self._logger.error(f"Error checking file: {e}") def _parse_line(self, line: str) -> Optional[LogEvent]: """Parse a single log line.""" if not line: return None # Extract timestamp if present timestamp = datetime.now() ts_match = re.match(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', line) if ts_match: try: timestamp = datetime.strptime(ts_match.group(1), '%Y-%m-%d %H:%M:%S') except ValueError: pass # Try each pattern for event_type, pattern in self.PATTERNS.items(): match = pattern.search(line) if match: data = self._extract_data(event_type, match, line) event = LogEvent( timestamp=timestamp, type=event_type, data=data, raw=line ) self._emit(event_type, data) return event return None def _extract_data( self, event_type: str, match: re.Match, raw_line: str ) -> Dict[str, Any]: """Extract data from regex match.""" groups = match.groups() data = { 'raw': raw_line, 'timestamp': datetime.now().isoformat(), } if event_type == 'loot': data['item'] = groups[0].strip() data['quantity'] = int(groups[1]) if len(groups) > 1 else 1 data['value'] = float(groups[2]) if len(groups) > 2 else 0.0 elif event_type == 'skill': data['skill'] = groups[0].strip() data['gain'] = float(groups[1]) if len(groups) > 1 else 0.0 elif event_type in ('global', 'hof'): data['item'] = groups[0].strip() if groups else 'Unknown' data['value'] = float(groups[1]) if len(groups) > 1 else 0.0 data['is_hof'] = event_type == 'hof' elif event_type == 'chat': data['channel'] = groups[0] if groups else 'Unknown' data['player'] = groups[1] if len(groups) > 1 else 'Unknown' data['message'] = groups[2] if len(groups) > 2 else '' elif event_type == 'damage_dealt': data['damage'] = float(groups[0]) if groups else 0.0 elif event_type == 'damage_received': data['damage'] = float(groups[0]) if groups else 0.0 elif event_type == 'kill': data['creature'] = groups[0] if groups else 'Unknown' elif event_type == 'craft': data['item'] = groups[0] if groups else 'Unknown' elif event_type == 'mission_complete': data['mission'] = groups[0] if groups else 'Unknown' elif event_type == 'experience': data['amount'] = float(groups[0]) if groups else 0.0 data['skill'] = groups[1].strip() if len(groups) > 1 else 'Unknown' elif event_type == 'loot_alt': data['item'] = groups[0] if groups else 'Unknown' data['quantity'] = int(groups[1]) if len(groups) > 1 and groups[1] else 1 return data