Skip to content

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

class Parser:
    def __init__(self, demo_path: str, library_path: Optional[str] = None)

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(): Returns True if this is a professional match. Checks league_id > 0 OR radiant_team_id > 0 OR dire_team_id > 0. Use this instead of checking league_id directly, as some pro matches have league_id=0 but 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.

class DraftEvent(BaseModel):
    is_pick: bool
    team: int
    hero_id: int

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.

class MessagesResult(BaseModel):
    messages: List[MessageEvent] = []
    total_captured: int = 0

MessageEvent

A single message event from the replay.

class MessageEvent(BaseModel):
    type: str
    tick: int
    net_tick: int
    data: Dict[str, Any]

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