Parser¶
AI Summary
Parser is the main class for parsing Dota 2 replays. Create an instance with Parser("match.dem") and call parse() with collectors like header=True, game_info=True, messages={...}. The parser supports single-pass collection of multiple data types. All methods return Pydantic models for type safety.
Constructor¶
Creates a new parser instance for a specific demo file.
Parameters:
- demo_path: Path to the .dem replay file
- library_path (optional): Path to the shared library. If not provided, uses the bundled library.
Example:
from python_manta import Parser
# Create parser for a demo file
parser = Parser("match.dem")
# Use custom library path
parser = Parser("match.dem", library_path="/path/to/libmanta_wrapper.so")
Main Method¶
parse¶
def parse(
self,
header: bool = False,
game_info: bool = False,
combat_log: Optional[Dict[str, Any]] = None,
entities: Optional[Dict[str, Any]] = None,
game_events: Optional[Dict[str, Any]] = None,
modifiers: Optional[Dict[str, Any]] = None,
string_tables: Optional[Dict[str, Any]] = None,
messages: Optional[Dict[str, Any]] = None,
parser_info: bool = False,
attacks: Optional[Dict[str, Any]] = None,
entity_deaths: Optional[Dict[str, Any]] = None,
wards: Optional[Dict[str, Any]] = None,
) -> ParseResult
Parses the demo file with the specified collectors enabled.
Parameters:
- header: Enable header collector (match metadata)
- game_info: Enable game info collector (draft, players, teams)
- combat_log: Combat log config ({"types": [0, 4], "max_entries": 100, "heroes_only": True})
- entities: Entity snapshots config ({"interval_ticks": 1800, "max_snapshots": 50})
- game_events: Game events config ({"event_filter": "kill", "max_events": 100})
- modifiers: Modifiers config ({"max_modifiers": 1000, "auras_only": True})
- string_tables: String tables config ({"table_names": ["userinfo"]})
- messages: Messages config ({"filter": "ChatMessage", "max_messages": 100})
- parser_info: Enable parser info collector
- attacks: Attacks config ({"max_events": 1000})
- entity_deaths: Entity deaths config ({"heroes_only": True})
- wards: Wards config ({"max_events": 0}) — tracks observer/sentry ward lifecycle
Returns: ParseResult
Example:
# Parse header only
result = parser.parse(header=True)
print(f"Map: {result.header.map_name}")
# Parse multiple collectors in single pass
result = parser.parse(header=True, game_info=True)
print(f"Map: {result.header.map_name}")
print(f"Match ID: {result.game_info.match_id}")
# Parse with message filtering
result = parser.parse(messages={
"filter": "CDOTAUserMsg_ChatMessage",
"max_messages": 100
})
for msg in result.messages.messages:
print(f"[{msg.tick}] {msg.data}")
Data Models¶
ParseResult¶
The result returned by parse().
class ParseResult(BaseModel):
success: bool
header: Optional[HeaderInfo] = None
game_info: Optional[GameInfo] = None
combat_log: Optional[CombatLogResult] = None
entities: Optional[EntityParseResult] = None
game_events: Optional[GameEventsResult] = None
modifiers: Optional[ModifiersResult] = None
string_tables: Optional[StringTablesResult] = None
messages: Optional[MessagesResult] = None
parser_info: Optional[ParserInfo] = None
attacks: Optional[AttacksResult] = None
entity_deaths: Optional[EntityDeathsResult] = None
wards: Optional[WardsResult] = None
Fields:
- success: Whether parsing completed successfully
- header: Header data if header=True was specified
- game_info: Game info if game_info=True was specified
- combat_log: Combat log data if combat_log=... was specified
- entities: Entity snapshots if entities=... was specified
- game_events: Game events if game_events=... was specified
- modifiers: Modifier data if modifiers=... was specified
- string_tables: String table data if string_tables=... was specified
- messages: Messages if messages=... was specified
- parser_info: Parser state if parser_info=True was specified
- attacks: Attack events if attacks=... was specified
- entity_deaths: Entity death events if entity_deaths=... was specified
- wards: Ward lifecycle events if wards=... was specified
HeaderInfo¶
Match metadata from the demo file header.
class HeaderInfo(BaseModel):
demo_file_stamp: str
network_protocol: int
server_name: str
client_name: str
map_name: str
game_directory: str
fullpackets_version: int
allow_clientside_entities: bool
allow_clientside_particles: bool
addons: str
demo_version_name: str
demo_version_guid: str
build_num: int
game: int
server_start_tick: int
Example:
result = parser.parse(header=True)
header = result.header
print(f"Map: {header.map_name}")
print(f"Server: {header.server_name}")
print(f"Build: {header.build_num}")
GameInfo¶
Complete game information including draft, players, and teams.
class GameInfo(BaseModel):
match_id: int = 0
game_mode: int = 0
game_winner: int = 0 # 2=Radiant, 3=Dire
league_id: int = 0 # Can be 0 even for pro matches!
end_time: int = 0 # Unix timestamp
radiant_team_id: int = 0
dire_team_id: int = 0
radiant_team_tag: str = ""
dire_team_tag: str = ""
players: List[PlayerInfo] = []
picks_bans: List[DraftEvent] = []
playback_time: float = 0.0 # Match duration in seconds
playback_ticks: int = 0
playback_frames: int = 0
def is_pro_match(self) -> bool:
"""Check if this is a pro/league match."""
...
Helper Methods:
is_pro_match(): ReturnsTrueif this is a professional match. Checksleague_id > 0ORradiant_team_id > 0ORdire_team_id > 0. Use this instead of checkingleague_iddirectly, as some pro matches haveleague_id=0but valid team IDs.
league_id can be 0 for pro matches
Don't rely on league_id > 0 to detect pro matches. Some professional matches have league_id=0 but valid radiant_team_id and dire_team_id. Always use is_pro_match() instead.
Example:
result = parser.parse(game_info=True)
game_info = result.game_info
print(f"Match ID: {game_info.match_id}")
print(f"Winner: {'Radiant' if game_info.game_winner == 2 else 'Dire'}")
# Players (10 players: 5 Radiant + 5 Dire)
for player in game_info.players:
team = "Radiant" if player.team == 2 else "Dire"
print(f" {player.player_name} ({team}): {player.hero_name}")
# Draft
for event in game_info.picks_bans:
action = "PICK" if event.is_pick else "BAN"
team = "Radiant" if event.team == 2 else "Dire"
print(f"{team} {action}: Hero {event.hero_id}")
# Pro match info (use is_pro_match() helper - works even when league_id is 0)
if game_info.is_pro_match():
print(f"{game_info.radiant_team_tag} vs {game_info.dire_team_tag}")
DraftEvent¶
A single pick or ban event.
Fields:
- is_pick: True for picks, False for bans
- team: 2 for Radiant, 3 for Dire
- hero_id: Dota 2 hero ID
PlayerInfo¶
Information about a player in the match.
class PlayerInfo(BaseModel):
player_name: str = ""
hero_name: str = "" # npc_dota_hero_* format
is_fake_client: bool = False
steam_id: int = 0
team: int = 0 # 2=Radiant, 3=Dire
Fields:
- player_name: Player's display name
- hero_name: Hero in npc_dota_hero_* format (e.g., "npc_dota_hero_axe")
- is_fake_client: True for bots
- steam_id: Player's Steam ID (64-bit)
- team: 2 for Radiant, 3 for Dire
Example:
result = parser.parse(game_info=True)
# List all players with their heroes
for player in result.game_info.players:
team = "Radiant" if player.team == 2 else "Dire"
print(f"{player.player_name} ({team}): {player.hero_name}")
print(f" Steam ID: {player.steam_id}")
MessagesResult¶
Container for parsed messages.
MessageEvent¶
A single message event from the replay.
Fields:
- type: Message type name (e.g., "CDOTAUserMsg_ChatMessage")
- tick: Game tick when message occurred
- net_tick: Network tick
- data: Message payload as dictionary
Message Filtering¶
The messages parameter accepts a dictionary with these options:
| Key | Type | Description |
|---|---|---|
filter |
str | Substring match for message types |
max_messages |
int | Maximum messages to capture (0 = unlimited) |
Filter Examples:
# Chat messages only
result = parser.parse(messages={"filter": "CDOTAUserMsg_ChatMessage"})
# All ping-related messages
result = parser.parse(messages={"filter": "Ping"})
# First 50 messages of any type
result = parser.parse(messages={"max_messages": 50})
# All messages (use with caution - can be large)
result = parser.parse(messages=True)
Warning
Some message types generate thousands of entries per match. Always use max_messages to prevent memory issues.
Complete Example¶
from python_manta import Parser, Hero
# Create parser
parser = Parser("match.dem")
# Parse everything in single pass
result = parser.parse(
header=True,
game_info=True,
messages={"filter": "CDOTAUserMsg_ChatMessage", "max_messages": 50}
)
# Access header
print(f"Map: {result.header.map_name}")
print(f"Server: {result.header.server_name}")
# Access draft with Hero enum for readable names
for event in result.game_info.picks_bans:
if event.is_pick:
hero_name = Hero(event.hero_id).name
team = "Radiant" if event.team == 2 else "Dire"
print(f"{team} picked {hero_name}")
# Access messages
for msg in result.messages.messages:
print(f"[{msg.tick}] {msg.type}: {msg.data}")