Source code for gnomish_army_knife.database.meta
"""
A module implementing an arena-match metadata interface.
"""
# built-in
from pathlib import Path
from typing import Any, Optional, cast
import uuid
# third-party
from vcorelib.io.types import JsonObject
from vcorelib.logging import LoggerMixin
from vcorelib.math import to_nanos
from vcorelib.math.time import nano_str
# internal
from gnomish_army_knife.database.event import CombatLogEvent
from gnomish_army_knife.enums.events import LogEvent
# https://wago.tools/db2/Map?filter%5BInstanceType%5D=4&page=1
ARENAS = {
559: "Nagrand Arena (old)",
562: "zzOldBlade's Edge Arena",
572: "Ruins of Lordaeron",
617: "Dalaran Sewers",
618: "The Ring of Valor",
980: "Tol'Viron Arena",
1134: "The Tiger's Peak",
1170: "Shado-Pan Showdown",
1504: "Black Rook Hold Arena",
1505: "Nagrand Arena",
1552: "Ashamane's Fall",
1672: "Blade's Edge Arena",
1825: "Hook Point",
1911: "Mugambala",
2167: "The Robodrome",
2373: "Empyrean Domain",
2509: "Maldraxxus Coliseum",
2511: "Enigma Arena",
2547: "Enigma Crucible",
2563: "Nokhudon Proving Grounds",
2759: "Cage of Carnage",
}
[docs]
class ArenaMatchMetadata(LoggerMixin):
"""A class implementing an arena-match metadata interface."""
def __init__(self) -> None:
"""Initialize this instance."""
super().__init__()
self.seen_match_start: bool = False
self.seen_match_end: bool = False
self.data: JsonObject = {}
self.reset()
@property
def summary(self) -> str:
"""A summary string for this match."""
return (
f"({'WIN' if self.data['is_win'] else 'LOSS'}"
f" in {self.data['duration']}s) "
f"{self.data['match_type']} on {self.data['instance']},"
f" {self.data['unknown']}?"
)
def _match_start(self, event: CombatLogEvent) -> None:
"""Handle the match start event."""
# MATCH_START: instanceID, unk, matchType, teamId
# MATCH_START,1505,0,Skirmish,0
# another example
# atinylittleshell/wow-combat-log-parser/src/actions/ArenaMatchStart.ts
# These should always be the same for i.e. solo shuffle rounds.
if not self.seen_match_start:
self.data["instance_id"] = int(event.data[0])
self.data["instance"] = ARENAS.get(
cast(int, self.data["instance_id"]), event.data[0]
)
self.data["unknown"] = event.data[1]
self.data["match_type"] = event.data[2]
self.data["team_id"] = int(event.data[3])
self.seen_match_start = True
def _match_end(self, event: CombatLogEvent) -> None:
"""Handle the match end event."""
# MATCH_END: winningTeam, duration, newRatingTeam1, newRatingTeam2
# MATCH_END,0,58,0,0
self.data["winning_team_id"] = int(event.data[0])
self.data["is_win"] = (
self.data["team_id"] == self.data["winning_team_id"]
)
self.data["duration_s"] = int(event.data[1])
self.data["duration"] = nano_str(
to_nanos(cast(int, self.data["duration_s"])), is_time=True
)
self.data["new_rating_team_1"] = int(event.data[2])
self.data["new_rating_team_2"] = int(event.data[3])
self.data["summary"] = self.summary
self.seen_match_end = True
[docs]
def handle(self, event: CombatLogEvent) -> bool:
"""
Attempt to glean new metadata from this event. Returns whether or not
the end of a match has been captured.
"""
result = False
events: dict[str, Any] = self.data["events"] # type: ignore
keep_metadata = False
match event.name:
case LogEvent.MATCH_START:
self._match_start(event)
keep_metadata = True
case LogEvent.COMBATANT_INFO:
# No special actions taken for this event currently.
if self.seen_match_start:
keep_metadata = True
case LogEvent.MATCH_END:
if self.seen_match_start:
self._match_end(event)
result = True
keep_metadata = True
if keep_metadata:
if event.name in events:
if not isinstance(events[event.name], list):
events[event.name] = [events[event.name]]
events[event.name].append(event.as_json())
else:
events[event.name] = event.as_json()
self.data["metadata_event_count"] += 1 # type: ignore
return result
[docs]
def reset(self) -> None:
"""Reset metadata state."""
self.seen_match_start = False
self.seen_match_end = False
self.data = {"events": {}, "metadata_event_count": 0}
[docs]
def file_path(self, base: Path, suffix: str = ".txt") -> Optional[Path]:
"""
Get a file path this match could be written to. This path should be
unique for any match that occurs on live servers.
"""
result = None
if self.seen_match_start and self.seen_match_end:
result = Path(
cast(str, self.data["match_type"]),
cast(str, self.data["instance"]),
)
name = f"{uuid.uuid4()}{suffix}"
while result.joinpath(name).is_file(): # pragma: nocover
name = f"{uuid.uuid4()}{suffix}"
result = base.joinpath(result, name)
return result