EU-Utility/premium/eu_integration/log_parser.py

324 lines
10 KiB
Python

"""
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