"""Arma 3 RPT log parser.""" from __future__ import annotations import re from datetime import datetime from pathlib import Path from typing import Callable class RPTParser: """Parses Arma 3 .rpt log files.""" # Pattern: "HH:MM:SS ..." or "[HH:MM:SS] ..." with optional date prefix _timestamp_re = re.compile( r"^\s*(?:(\d{2}/\d{2}/\d{4})\s+)?" r"(?:\[)?(\d{2}:\d{2}:\d{2})(?:\])?\s*" r"(?:\[?(\w+)\]?\s*)?(.*)$" ) def parse_line(self, line: str) -> dict | None: """Parse one RPT log line.""" if not line or not line.strip(): return None match = self._timestamp_re.match(line) if not match: # Non-timestamped line — treat as info stripped = line.strip() if not stripped: return None return { "timestamp": datetime.utcnow().isoformat(), "level": "info", "message": stripped, } date_str, time_str, level_str, message = match.groups() # Map Arma 3 log levels level = "info" if level_str: level_lower = level_str.lower() if level_lower in ("error", "fault"): level = "error" elif level_lower in ("warning", "warn"): level = "warning" # Build ISO timestamp try: if date_str: dt = datetime.strptime(f"{date_str} {time_str}", "%m/%d/%Y %H:%M:%S") else: dt = datetime.strptime(time_str, "%H:%M:%S") dt = dt.replace(year=datetime.utcnow().year, month=datetime.utcnow().month, day=datetime.utcnow().day) timestamp = dt.isoformat() except ValueError: timestamp = datetime.utcnow().isoformat() return { "timestamp": timestamp, "level": level, "message": (message or "").strip(), } def get_log_file_resolver(self, server_id: int) -> Callable[[Path], Path | None]: """Return a callable that finds the current RPT log file.""" def resolver(server_dir: Path) -> Path | None: # Arma 3 stores logs in server_dir/server/*.rpt profile_dir = server_dir / "server" if not profile_dir.exists(): return None rpt_files = sorted(profile_dir.glob("*.rpt"), key=lambda p: p.stat().st_mtime, reverse=True) if rpt_files: return rpt_files[0] # Fallback: check for arma3server_x64_*.rpt pattern rpt_files = sorted(profile_dir.glob("arma3server*.rpt"), key=lambda p: p.stat().st_mtime, reverse=True) return rpt_files[0] if rpt_files else None return resolver