From 6d8f7102449d054781a608281203dd5adf9ab9c2 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Tue, 10 Feb 2026 15:13:21 +0000 Subject: [PATCH] fix: add comma to loot regex patterns for items like 'Dominax Original Garter, Adjusted (L)' - Add comma (,) to character class in PATTERN_LOOT_EN and PATTERN_LOOT_SV - Add comma (,) to PATTERN_LOOT_NO_VALUE_EN and PATTERN_LOOT_NO_VALUE_SV - Fixes items with commas in their names not being tracked --- core/log_watcher.py | 166 ++++++++++++++++++++++---------------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/core/log_watcher.py b/core/log_watcher.py index c20db24..3f1393d 100644 --- a/core/log_watcher.py +++ b/core/log_watcher.py @@ -43,53 +43,53 @@ class HuntingSessionStats: total_shrapnel_ped: Decimal = Decimal('0.0') total_universal_ammo_ped: Decimal = Decimal('0.0') total_other_loot_ped: Decimal = Decimal('0.0') # Non-shrapnel, non-UA loot - + # Cost tracking weapon_cost_ped: Decimal = Decimal('0.0') armor_cost_ped: Decimal = Decimal('0.0') healing_cost_ped: Decimal = Decimal('0.0') plates_cost_ped: Decimal = Decimal('0.0') total_cost_ped: Decimal = Decimal('0.0') - + # Combat tracking damage_dealt: Decimal = Decimal('0.0') damage_taken: Decimal = Decimal('0.0') healing_done: Decimal = Decimal('0.0') shots_fired: int = 0 kills: int = 0 - + # Special events globals_count: int = 0 hofs_count: int = 0 personal_globals: List[Dict[str, Any]] = field(default_factory=list) - + # Calculated metrics @property def net_profit_ped(self) -> Decimal: """Calculate net profit (excluding shrapnel from loot).""" return self.total_other_loot_ped - self.total_cost_ped - + @property def return_percentage(self) -> Decimal: """Calculate return percentage (loot/cost * 100).""" if self.total_cost_ped > 0: return (self.total_other_loot_ped / self.total_cost_ped) * Decimal('100') return Decimal('0.0') - + @property def cost_per_kill(self) -> Decimal: """Calculate cost per kill.""" if self.kills > 0: return self.total_cost_ped / self.kills return Decimal('0.0') - + @property def dpp(self) -> Decimal: """Calculate Damage Per PED (efficiency metric).""" if self.total_cost_ped > 0: return self.damage_dealt / self.total_cost_ped return Decimal('0.0') - + @property def damage_per_kill(self) -> Decimal: """Calculate average damage per kill.""" @@ -117,29 +117,29 @@ class LogWatcher: # 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*\[?\]?\s*' - r'You\s+received\s+\[?([\w\s\-()]+?)\]?\s+x\s*\((\d+)\)\s*' + r'You\s+received\s+\[?([\w\s\-(),]+?)\]?\s+x\s*\((\d+)\)\s*' r'Value:\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) 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'Du\s+fick\s+([\w\s\-(),]+?)\s+x\s*\((\d+)\)\s*' r'Värde:\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) - + # LOOT PATTERN WITHOUT VALUE (some items don't show value) # English: "You received Animal Thyroid Oil x 5" PATTERN_LOOT_NO_VALUE_EN = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]:?\s*\[?\]?\s*' - r'You\s+received\s+\[?([\w\s\-()]+?)\]?\s+x\s*(\d+)', + r'You\s+received\s+\[?([\w\s\-(),]+?)\]?\s+x\s*(\d+)', re.IGNORECASE ) - + PATTERN_LOOT_NO_VALUE_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+)', + r'Du\s+fick\s+([\w\s\-(),]+?)\s+x\s*(\d+)', re.IGNORECASE ) @@ -151,7 +151,7 @@ class LogWatcher: r'You\s+killed\s+\[?([\w\s\-()]+?)\]?', re.IGNORECASE ) - + PATTERN_KILL_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' r'Du\s+dödade\s+\[?([\w\s\-()]+?)\]?', @@ -201,7 +201,7 @@ class LogWatcher: r'(?:for|with\s+a\s+value\s+of)\s+(\d+(?:\.\d+)?)\s+PED', re.IGNORECASE ) - + 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', @@ -232,7 +232,7 @@ class LogWatcher: r'You\s+have\s+advanced\s+to\s+level\s+(\d+)\s+in\s+([\w\s]+)', re.IGNORECASE ) - + PATTERN_LEVEL_UP_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' r'Du\s+har\s+avancerat\s+till\s+nivå\s+(\d+)\s+i\s+([\w\s]+)', @@ -322,7 +322,7 @@ class LogWatcher: r'(You\s+Evaded|You\s+dodged|The\s+target\s+Evaded\s+your\s+attack|The\s+target\s+Dodged|The\s+attack\s+missed\s+you)', re.IGNORECASE ) - + PATTERN_EVADE_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' r'(Du\s+undvek|Målet\s+undvek\s+din\s+attack|Attacken\s+missade\s+dig)', @@ -337,13 +337,13 @@ class LogWatcher: r'Your\s+([\w\s\-()]+?)\s+has\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC', re.IGNORECASE ) - + PATTERN_DECAY_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' r'Din\s+([\w\s\-()]+?)\s+har\s+decayed\s+(\d+(?:\.\d+)?)\s+PEC', re.IGNORECASE ) - + PATTERN_WEAPON_BROKEN_SV = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+\[?\]?\s*' r'Din\s+([\w\s\-()]+?)\s+har\s+nått\s+minimalt\s+skick', @@ -365,7 +365,7 @@ class LogWatcher: r'Överföring\s+slutförd.*?((\d+(?:\.\d+)?))\s+PED', re.IGNORECASE ) - + PATTERN_PED_TRANSFER_EN = re.compile( r'^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[System\]\s+' r'Transfer\s+complete.*?((\d+(?:\.\d+)?))\s+PED', @@ -394,7 +394,7 @@ class LogWatcher: r'You\s+received\s+(\d+(?:\.\d+)?)\s+PED\s+from\s+your\s+teammates', re.IGNORECASE ) - + PATTERN_TEAM_SHARE_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+(\d+(?:\.\d+)?)\s+PED\s+från\s+dina\s+lagkamrater', @@ -529,7 +529,7 @@ class LogWatcher: def _is_shrapnel(self, item_name: str) -> bool: """Check if item is Shrapnel.""" return item_name.strip().lower() == 'shrapnel' - + def _is_universal_ammo(self, item_name: str) -> bool: """Check if item is Universal Ammo.""" name = item_name.strip().lower() @@ -539,7 +539,7 @@ class LogWatcher: """Categorize loot item and return LootItem.""" is_shrapnel = self._is_shrapnel(item_name) is_ua = self._is_universal_ammo(item_name) - + return LootItem( name=item_name.strip(), quantity=1, # Will be set by caller @@ -558,12 +558,12 @@ class LogWatcher: return None # Try each pattern in priority order - + # KILL - Swedish match = self.PATTERN_KILL_SV.match(line) if match: return self._create_kill_event(match, line, 'swedish') - + # KILL - English match = self.PATTERN_KILL_EN.match(line) if match: @@ -578,12 +578,12 @@ class LogWatcher: match = self.PATTERN_LOOT_EN.match(line) if match: return self._create_loot_event_en(match, line) - + # LOOT WITHOUT VALUE - Swedish match = self.PATTERN_LOOT_NO_VALUE_SV.match(line) if match: return self._create_loot_event_no_value(match, line, 'swedish') - + # LOOT WITHOUT VALUE - English match = self.PATTERN_LOOT_NO_VALUE_EN.match(line) if match: @@ -608,7 +608,7 @@ class LogWatcher: match = self.PATTERN_PERSONAL_GLOBAL_EN.match(line) if match: return self._create_personal_global_event(match, line, 'english') - + # HOF - English match = self.PATTERN_HOF_EN.match(line) if match: @@ -665,7 +665,7 @@ class LogWatcher: 'language': 'english' } ) - + # LEVEL UP - Swedish match = self.PATTERN_LEVEL_UP_SV.match(line) if match: @@ -797,7 +797,7 @@ class LogWatcher: raw_line=line, data={'type': match.group(2), 'language': 'english'} ) - + # EVADE/DODGE/MISS - Swedish match = self.PATTERN_EVADE_SV.match(line) if match: @@ -821,7 +821,7 @@ class LogWatcher: 'language': 'english' } ) - + # DECAY - Swedish match = self.PATTERN_DECAY_SV.match(line) if match: @@ -877,7 +877,7 @@ class LogWatcher: 'language': 'english' } ) - + # TEAM SHARE - English match = self.PATTERN_TEAM_SHARE_EN.match(line) if match: @@ -890,7 +890,7 @@ class LogWatcher: 'language': 'english' } ) - + # TEAM SHARE - Swedish match = self.PATTERN_TEAM_SHARE_SV.match(line) if match: @@ -911,10 +911,10 @@ class LogWatcher: item_name = match.group(2).strip() quantity = int(match.group(3)) value_ped = Decimal(match.group(4)) - + loot_item = self._categorize_loot(item_name, value_ped) loot_item.quantity = quantity - + return LogEvent( timestamp=self._parse_timestamp(match.group(1)), event_type='loot', @@ -934,10 +934,10 @@ class LogWatcher: item_name = match.group(2).strip() quantity = int(match.group(3)) value_ped = Decimal(match.group(4)) if match.group(4) else Decimal('0') - + loot_item = self._categorize_loot(item_name, value_ped) loot_item.quantity = quantity - + return LogEvent( timestamp=self._parse_timestamp(match.group(1)), event_type='loot', @@ -951,15 +951,15 @@ class LogWatcher: 'language': 'english' } ) - + def _create_loot_event_no_value(self, match: re.Match, line: str, language: str) -> LogEvent: """Create loot event without value.""" item_name = match.group(2).strip() quantity = int(match.group(3)) - + loot_item = self._categorize_loot(item_name, Decimal('0')) loot_item.quantity = quantity - + return LogEvent( timestamp=self._parse_timestamp(match.group(1)), event_type='loot', @@ -973,7 +973,7 @@ class LogWatcher: 'language': language } ) - + def _create_kill_event(self, match: re.Match, line: str, language: str) -> LogEvent: """Create kill event.""" return LogEvent( @@ -1027,7 +1027,7 @@ class LogWatcher: 'language': language } ) - + def _create_hof_event(self, match: re.Match, line: str, language: str) -> LogEvent: """Create Hall of Fame event.""" return LogEvent( @@ -1118,40 +1118,40 @@ class LogWatcher: class HuntingSessionTracker: """ Tracks hunting session statistics from LogWatcher events. - + This class accumulates all hunting-related data and provides real-time metrics like profit/loss, return percentage, etc. """ - + def __init__(self): self.stats = HuntingSessionStats() self._session_start: Optional[datetime] = None self._session_active = False - + # Callbacks for real-time updates self._on_stats_update: Optional[Callable] = None - + def start_session(self): """Start a new hunting session.""" self._session_start = datetime.now() self._session_active = True self.stats = HuntingSessionStats() logger.info("Hunting session started") - + def end_session(self) -> HuntingSessionStats: """End the current hunting session and return final stats.""" self._session_active = False logger.info("Hunting session ended") return self.stats - + def is_active(self) -> bool: """Check if session is active.""" return self._session_active - + def set_stats_callback(self, callback: Callable[[HuntingSessionStats], None]): """Set callback for real-time stats updates.""" self._on_stats_update = callback - + def _notify_update(self): """Notify listeners of stats update.""" if self._on_stats_update: @@ -1159,71 +1159,71 @@ class HuntingSessionTracker: self._on_stats_update(self.stats) except Exception as e: logger.error(f"Stats callback error: {e}") - + def on_loot(self, event: LogEvent): """Process loot event.""" if not self._session_active: return - + data = event.data value_ped = data.get('value_ped', Decimal('0.0')) is_shrapnel = data.get('is_shrapnel', False) is_ua = data.get('is_universal_ammo', False) - + self.stats.total_loot_ped += value_ped - + if is_shrapnel: self.stats.total_shrapnel_ped += value_ped elif is_ua: self.stats.total_universal_ammo_ped += value_ped else: self.stats.total_other_loot_ped += value_ped - + self._notify_update() - + def on_kill(self, event: LogEvent): """Process kill event.""" if not self._session_active: return - + self.stats.kills += 1 self._notify_update() - + def on_damage_dealt(self, event: LogEvent): """Process damage dealt event.""" if not self._session_active: return - + damage = event.data.get('damage', Decimal('0.0')) self.stats.damage_dealt += damage self.stats.shots_fired += 1 # Each damage event = 1 shot self._notify_update() - + def on_damage_taken(self, event: LogEvent): """Process damage taken event.""" if not self._session_active: return - + damage = event.data.get('damage', Decimal('0.0')) self.stats.damage_taken += damage self._notify_update() - + def on_heal(self, event: LogEvent): """Process heal event.""" if not self._session_active: return - + heal_amount = event.data.get('heal_amount', Decimal('0.0')) self.stats.healing_done += heal_amount self._notify_update() - + def on_global(self, event: LogEvent): """Process global event.""" if not self._session_active: return - + self.stats.globals_count += 1 - + # Store personal global details if event.event_type == 'personal_global': self.stats.personal_globals.append({ @@ -1231,16 +1231,16 @@ class HuntingSessionTracker: 'creature': event.data.get('creature', 'Unknown'), 'value_ped': event.data.get('value_ped', Decimal('0.0')) }) - + self._notify_update() - + def on_hof(self, event: LogEvent): """Process Hall of Fame event.""" if not self._session_active: return - + self.stats.hofs_count += 1 - + # Store HoF details if 'creature' in event.data: self.stats.personal_globals.append({ @@ -1249,53 +1249,53 @@ class HuntingSessionTracker: 'value_ped': event.data.get('value_ped', Decimal('0.0')), 'is_hof': True }) - + self._notify_update() - + def on_decay(self, event: LogEvent): """Process decay event.""" if not self._session_active: return - + # Convert PEC to PED amount_pec = event.data.get('amount_pec', Decimal('0.0')) amount_ped = amount_pec / Decimal('100') - + self.stats.weapon_cost_ped += amount_ped self.stats.total_cost_ped += amount_ped self._notify_update() - + def add_weapon_cost(self, cost_ped: Decimal): """Manually add weapon cost (for calculated decay).""" if not self._session_active: return - + self.stats.weapon_cost_ped += cost_ped self.stats.total_cost_ped += cost_ped self._notify_update() - + def add_armor_cost(self, cost_ped: Decimal): """Manually add armor cost.""" if not self._session_active: return - + self.stats.armor_cost_ped += cost_ped self.stats.total_cost_ped += cost_ped self._notify_update() - + def add_healing_cost(self, cost_ped: Decimal): """Manually add healing cost.""" if not self._session_active: return - + self.stats.healing_cost_ped += cost_ped self.stats.total_cost_ped += cost_ped self._notify_update() - + def get_stats(self) -> HuntingSessionStats: """Get current stats.""" return self.stats - + def get_summary(self) -> Dict[str, Any]: """Get session summary as dictionary.""" return {