Skip to content

Data Models

AI Summary

All parsed data is returned as Pydantic models for type safety and easy serialization. Models include: HeaderInfo (match metadata), GameInfo/DraftEvent/PlayerInfo (game data with draft, teams, players), UniversalParseResult/MessageEvent (messages), GameEventsResult/GameEventData (events), CombatLogResult/CombatLogEntry (combat), ModifiersResult/ModifierEntry (buffs), EntitiesResult/EntityData (entities), HeroSnapshot/EntityStateSnapshot (hero state with armor, damage, attributes at specific ticks), AttacksResult/AttackEvent (auto-attacks), EntityDeathsResult/EntityDeath (entity removals), WardsResult/WardEvent (ward placement, expiration, dewarding), StringTablesResult (tables), ParserInfo (state). Enums include RuneType (rune tracking), EntityType (hero, creep, summon, building), CombatLogType (45 combat log event types), DamageType (physical/magical/pure/hp_removal), Team (Radiant/Dire/Neutral), NeutralCampType (small/medium/hard/ancient camps for multi-camp farming detection), NeutralItemTier (tier unlock times), NeutralItem (100+ neutral items), ChatWheelMessage (voice line IDs), and GameActivity (animation/taunt detection). All models have .model_dump() for dict conversion and .model_dump_json() for JSON.


Utility Functions

Time Utilities

Function Description
TICKS_PER_SECOND Constant: 30.0 ticks per second
format_game_time(seconds) Format game time as "-0:40" or "3:07"
game_time_to_tick(game_time, game_start_tick) Convert game time (seconds) to tick
tick_to_game_time(tick, game_start_tick) Convert tick to game time (seconds)

Example:

from python_manta import format_game_time, TICKS_PER_SECOND

# Format game time
print(format_game_time(187))   # "3:07"
print(format_game_time(-40))   # "-0:40"

# Convert between ticks and game time
game_start_tick = 27000
tick = game_time_to_tick(300, game_start_tick)  # 5:00 -> tick 36000
time = tick_to_game_time(36000, game_start_tick)  # tick 36000 -> 300.0


Hero Name Utilities

Function Description
normalize_hero_name(name) Normalize hero names by replacing double underscores with single

Entity snapshots may use double underscores (npc_dota_hero_shadow__demon) while combat log uses single (npc_dota_hero_shadow_demon). This function ensures consistency for matching.

Example:

from python_manta import normalize_hero_name

# Normalize hero names for consistent matching
name = normalize_hero_name("npc_dota_hero_shadow__demon")
print(name)  # "npc_dota_hero_shadow_demon"

# Works with just the hero key too
key = normalize_hero_name("shadow__demon")
print(key)  # "shadow_demon"

# No change if already normalized
print(normalize_hero_name("shadow_demon"))  # "shadow_demon"


Enums

RuneType

Enum for Dota 2 power rune types with helper methods for combat log analysis.

class RuneType(str, Enum):
    DOUBLE_DAMAGE = "modifier_rune_doubledamage"
    HASTE = "modifier_rune_haste"
    ILLUSION = "modifier_rune_illusion"
    INVISIBILITY = "modifier_rune_invis"
    REGENERATION = "modifier_rune_regen"
    ARCANE = "modifier_rune_arcane"
    SHIELD = "modifier_rune_shield"
    WATER = "modifier_rune_water"

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Double Damage")
modifier_name str Combat log modifier name

Class Methods:

Method Returns Description
from_modifier(name) RuneType \| None Get RuneType from modifier name
is_rune_modifier(name) bool Check if modifier is a rune
all_modifiers() List[str] Get all rune modifier names

Example:

from python_manta import RuneType

# Check if a combat log entry is a rune pickup
if RuneType.is_rune_modifier(entry.inflictor_name):
    rune = RuneType.from_modifier(entry.inflictor_name)
    print(f"Picked up {rune.display_name}")

# Direct enum access
print(RuneType.HASTE.display_name)  # "Haste"
print(RuneType.HASTE.modifier_name)  # "modifier_rune_haste"

# Get all rune modifiers for filtering
rune_modifiers = RuneType.all_modifiers()


EntityType

Enum for classifying Dota 2 entity types from entity name strings. Useful for filtering combat log entries by attacker/target type.

class EntityType(str, Enum):
    HERO = "hero"
    LANE_CREEP = "lane_creep"
    NEUTRAL_CREEP = "neutral_creep"
    SUMMON = "summon"
    BUILDING = "building"
    WARD = "ward"
    COURIER = "courier"
    ROSHAN = "roshan"
    UNKNOWN = "unknown"

Properties:

Property Type Description
is_hero bool True if this is a hero
is_creep bool True if lane or neutral creep
is_unit bool True if controllable unit (not building/ward)
is_structure bool True if building or ward

Class Methods:

Method Returns Description
from_name(entity_name) EntityType Get EntityType from entity name string

Example:

from python_manta import MantaParser, EntityType

parser = MantaParser()
result = parser.parse_combat_log(demo_path, types=[0], max_entries=1000)

for entry in result.entries:
    attacker_type = EntityType.from_name(entry.attacker_name)
    target_type = EntityType.from_name(entry.target_name)

    # Self-buff detection
    is_self = entry.attacker_name == entry.target_name

    # Hero vs hero combat
    if attacker_type.is_hero and target_type.is_hero and not is_self:
        print(f"Hero combat: {entry.attacker_name} -> {entry.target_name}")

    # Hero farming creeps
    if attacker_type.is_hero and target_type.is_creep:
        print(f"Farming: {entry.attacker_name} hit {entry.target_name}")

    # Building damage
    if target_type == EntityType.BUILDING:
        print(f"Structure damage: {entry.target_name}")


CombatLogType

Enum for all 45+ combat log event types. Use this instead of magic numbers when filtering combat log entries.

class CombatLogType(int, Enum):
    DAMAGE = 0
    HEAL = 1
    MODIFIER_ADD = 2
    MODIFIER_REMOVE = 3
    DEATH = 4
    ABILITY = 5
    ITEM = 6
    LOCATION = 7
    GOLD = 8
    GAME_STATE = 9
    XP = 10
    PURCHASE = 11
    BUYBACK = 12
    ABILITY_TRIGGER = 13
    PLAYERSTATS = 14
    MULTIKILL = 15
    KILLSTREAK = 16
    TEAM_BUILDING_KILL = 17
    FIRST_BLOOD = 18
    MODIFIER_REFRESH = 19
    NEUTRAL_CAMP_STACK = 20
    PICKUP_RUNE = 21
    REVEALED_INVISIBLE = 22
    HERO_SAVED = 23
    MANA_RESTORED = 24
    HERO_LEVELUP = 25
    BOTTLE_HEAL_ALLY = 26
    ENDGAME_STATS = 27
    INTERRUPT_CHANNEL = 28
    ALLIED_GOLD = 29
    AEGIS_TAKEN = 30
    MANA_DAMAGE = 31
    PHYSICAL_DAMAGE_PREVENTED = 32
    UNIT_SUMMONED = 33
    ATTACK_EVADE = 34
    TREE_CUT = 35
    SUCCESSFUL_SCAN = 36
    END_KILLSTREAK = 37
    BLOODSTONE_CHARGE = 38
    CRITICAL_DAMAGE = 39
    SPELL_ABSORB = 40
    UNIT_TELEPORTED = 41
    KILL_EATER_EVENT = 42
    NEUTRAL_ITEM_EARNED = 43
    TELEPORT_INTERRUPTED = 44
    MODIFIER_STACK_EVENT = 45

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Purchase")
is_damage_related bool True if type is damage/heal related
is_modifier_related bool True if type is buff/debuff related
is_economy_related bool True if type is gold/XP/item related

Class Methods:

Method Returns Description
from_value(int) CombatLogType \| None Get CombatLogType from integer value

Example:

from python_manta import MantaParser, CombatLogType

parser = MantaParser()

# Use enum instead of magic numbers
result = parser.parse_combat_log(
    demo_path,
    types=[CombatLogType.PURCHASE, CombatLogType.ITEM],
    max_entries=100
)

for entry in result.entries:
    log_type = CombatLogType.from_value(entry.type)
    if log_type == CombatLogType.PURCHASE:
        print(f"{entry.target_name} bought {entry.value_name}")
    elif log_type == CombatLogType.ITEM:
        print(f"{entry.attacker_name} used {entry.inflictor_name}")

# Check type categories
if log_type.is_economy_related:
    print("Economy event")


DamageType

Enum for Dota 2 damage types.

class DamageType(int, Enum):
    PHYSICAL = 0    # Right-click attacks, physical spells
    MAGICAL = 1     # Most spells, reduced by magic resistance
    PURE = 2        # Ignores armor and magic resistance
    COMPOSITE = 3   # Legacy: removed from Dota 2, was reduced by both armor and magic resistance
    HP_REMOVAL = 4  # Blood Grenade, Heart stopper aura, etc.

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Physical")

Class Methods:

Method Returns Description
from_value(int) DamageType \| None Get DamageType from integer value

Example:

from python_manta import Parser, DamageType, CombatLogType

parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [CombatLogType.DAMAGE.value], "max_entries": 100})

for entry in result.combat_log.entries:
    dmg_type = DamageType.from_value(entry.damage_type)
    if dmg_type == DamageType.PURE:
        print(f"Pure damage: {entry.value} from {entry.inflictor_name}")
    elif dmg_type == DamageType.HP_REMOVAL:
        print(f"HP Removal: {entry.value} from {entry.inflictor_name}")


Team

Enum for Dota 2 team identifiers.

class Team(int, Enum):
    SPECTATOR = 0   # Spectator/observer
    UNASSIGNED = 1  # Not yet assigned
    RADIANT = 2     # Radiant team (bottom-left)
    DIRE = 3        # Dire team (top-right)
    NEUTRAL = 4     # Neutral creeps, Roshan, jungle camps

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Radiant")
is_playing bool True if Radiant or Dire
is_neutral bool True if neutral unit
opposite Team \| None The opposing team (None for non-playing)

Class Methods:

Method Returns Description
from_value(int) Team \| None Get Team from integer value

Example:

from python_manta import Parser, Team, CombatLogType

parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [CombatLogType.DEATH.value], "heroes_only": True})

for entry in result.combat_log.entries:
    attacker_team = Team.from_value(entry.attacker_team)
    target_team = Team.from_value(entry.target_team)

    if attacker_team and target_team and attacker_team != target_team:
        print(f"{attacker_team.display_name} killed {target_team.display_name} hero")

    # Check for neutral creep kills
    if target_team == Team.NEUTRAL:
        print(f"{attacker_team.display_name} killed neutral creep")

    # Use opposite property
    if attacker_team == Team.RADIANT:
        enemy = attacker_team.opposite  # Team.DIRE


NeutralCampType

Enum for neutral creep camp types. Used in combat log events (DEATH, MODIFIER_ADD, etc.) to identify which type of neutral camp a creep belongs to. Useful for detecting multi-camp farming.

class NeutralCampType(int, Enum):
    SMALL = 0   # Small camps: kobolds, harpies, ghosts, forest trolls, gnolls
    MEDIUM = 1  # Medium camps: wolves, ogres, mud golems
    HARD = 2    # Hard/Large camps: hellbears, dark trolls, wildkin, satyr hellcaller, centaurs
    ANCIENT = 3 # Ancient camps: dragons, thunderhides, prowlers, rock golems

SMALL (0) includes non-neutrals

The value 0 is also used for non-neutral units (lane creeps, wards). Filter by "neutral" in target_name to get only neutral creeps.

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Hard Camp")
is_ancient bool True if this is an ancient camp

Class Methods:

Method Returns Description
from_value(int) NeutralCampType Get NeutralCampType from integer value

Example - Multi-camp farming detection:

from python_manta import Parser, NeutralCampType
from collections import defaultdict

parser = Parser("match.dem")
result = parser.parse(combat_log={})

# Filter neutral creep deaths by heroes
neutral_deaths = [
    e for e in result.combat_log.entries
    if e.type_name == "DOTA_COMBATLOG_DEATH"
    and "npc_dota_neutral" in e.target_name
    and e.attacker_name.startswith("npc_dota_hero_")
]

# Group by time window (2 seconds) + attacker
def time_bucket(game_time):
    return int(game_time / 2.0)

kills_by_window = defaultdict(list)
for e in neutral_deaths:
    key = (time_bucket(e.game_time), e.attacker_name)
    kills_by_window[key].append(e)

# Detect multi-camp: different camp_types in same window
for (bucket, attacker), kills in kills_by_window.items():
    camp_types = set(NeutralCampType.from_value(e.neutral_camp_type) for e in kills)

    if len(camp_types) >= 2:
        hero = attacker.replace("npc_dota_hero_", "")
        types = [ct.display_name for ct in camp_types]
        print(f"{hero} multi-camp farming: {types}")

Example - Camp type breakdown:

from python_manta import Parser, NeutralCampType, Team
from collections import Counter

parser = Parser("match.dem")
result = parser.parse(combat_log={"types": [1]})  # DEATH events

# Count neutral kills by camp type
neutral_deaths = [
    e for e in result.combat_log.entries
    if "npc_dota_neutral" in e.target_name
]

by_type = Counter(NeutralCampType.from_value(e.neutral_camp_type) for e in neutral_deaths)
for camp_type, count in by_type.most_common():
    print(f"{camp_type.display_name}: {count} kills")

# Also available: neutral_camp_team (2=Radiant jungle, 3=Dire jungle)
radiant_jungle = sum(1 for e in neutral_deaths if e.neutral_camp_team == Team.RADIANT)
dire_jungle = sum(1 for e in neutral_deaths if e.neutral_camp_team == Team.DIRE)
print(f"Radiant jungle: {radiant_jungle}, Dire jungle: {dire_jungle}")


Item

Comprehensive enum for all ~200 purchasable Dota 2 items (shop items). Provides type-safe item identification with display names and categories.

class Item(str, Enum):
    # Consumables
    TANGO = "item_tango"
    CLARITY = "item_clarity"
    FAMANGO = "item_famango"  # Enchanted Mango alias

    # Weapons
    BATTLE_FURY = "item_bfury"
    DAEDALUS = "item_greater_crit"
    BUTTERFLY = "item_butterfly"

    # Artifacts
    BLINK_DAGGER = "item_blink"
    BLACK_KING_BAR = "item_black_king_bar"
    # ... 200+ items

Properties:

Property Type Description
item_name str Internal item name (e.g., "item_blink")
display_name str Human-readable name (e.g., "Blink Dagger")
category str | None Item category (consumable, weapon, armor, etc.)

Class Methods:

Method Returns Description
from_item_name(name) Item \| None Get Item from internal name
is_purchasable_item(name) bool Check if item name is a purchasable item
items_by_category(cat) List[Item] Get all items of a specific category
all_item_names() List[str] Get all purchasable item internal names

Example:

from python_manta import Item

# Get item from internal name
blink = Item.from_item_name("item_blink")
print(f"{blink.display_name}: {blink.category}")
# Output: Blink Dagger: artifact

# Get all weapons
weapons = Item.items_by_category("weapon")
print(f"Found {len(weapons)} weapon items")

# Check if item is purchasable
if Item.is_purchasable_item("item_famango"):
    mango = Item.from_item_name("item_famango")
    print(f"Display name: {mango.display_name}")  # "Enchanted Mango"


ItemCategory

Enum for item category classification.

class ItemCategory(str, Enum):
    CONSUMABLE = "consumable"
    ATTRIBUTE = "attribute"
    EQUIPMENT = "equipment"
    SECRET_SHOP = "secret_shop"
    SUPPORT = "support"
    MAGICAL = "magical"
    ARMOR = "armor"
    WEAPON = "weapon"
    ARTIFACT = "artifact"
    ACCESSORY = "accessory"

NeutralItemTier

Enum for neutral item tier classification. Tiers unlock at specific game times.

class NeutralItemTier(int, Enum):
    TIER_1 = 0  # Unlocks at 5:00
    TIER_2 = 1  # Unlocks at 15:00
    TIER_3 = 2  # Unlocks at 25:00
    TIER_4 = 3  # Unlocks at 35:00
    TIER_5 = 4  # Unlocks at 55:00

Properties:

Property Type Description
display_name str Human-readable name (e.g., "Tier 1")
unlock_time_minutes int Game time in minutes when tier unlocks

Class Methods:

Method Returns Description
from_value(int) NeutralItemTier \| None Get tier from integer value (0-4)

Example:

from python_manta import NeutralItemTier

tier = NeutralItemTier.TIER_3
print(f"{tier.display_name} unlocks at {tier.unlock_time_minutes} minutes")
# Output: Tier 3 unlocks at 25 minutes


NeutralItem

Comprehensive enum of all Dota 2 neutral items (100+ items), including both active items and retired/rotated items from previous patches. Useful for tracking neutral item pickups and usage.

class NeutralItem(str, Enum):
    # Tier 1 - Current (7.38+)
    CHIPPED_VEST = "item_chipped_vest"
    DORMANT_CURIO = "item_dormant_curio"
    KOBOLD_CUP = "item_kobold_cup"
    # ... 100+ items including retired ones

    # Tier 5 - Retired
    APEX = "item_apex"
    PIRATE_HAT = "item_pirate_hat"
    # etc.

Properties:

Property Type Description
item_name str Internal item name (e.g., "item_kobold_cup")
display_name str Human-readable name (e.g., "Kobold Cup")
tier int | None Item tier (0-4) or None for special items
tier_enum NeutralItemTier | None Tier as enum

Class Methods:

Method Returns Description
from_item_name(name) NeutralItem \| None Get NeutralItem from internal name
is_neutral_item(name) bool Check if item name is a neutral item
items_by_tier(tier) List[NeutralItem] Get all items of a specific tier
all_item_names() List[str] Get all neutral item internal names

Example:

from python_manta import MantaParser, NeutralItem, NeutralItemTier, CombatLogType

parser = MantaParser()

# Track neutral item usage in combat log
result = parser.parse_combat_log(demo_path, types=[CombatLogType.ITEM], max_entries=1000)

for entry in result.entries:
    if NeutralItem.is_neutral_item(entry.inflictor_name):
        item = NeutralItem.from_item_name(entry.inflictor_name)
        print(f"{entry.attacker_name} used {item.display_name} (Tier {item.tier + 1})")

# Get all Tier 1 items
tier1_items = NeutralItem.items_by_tier(0)
print(f"Tier 1 has {len(tier1_items)} items")

# Check unlock times
tier = NeutralItemTier.TIER_3
print(f"{tier.display_name} items unlock at {tier.unlock_time_minutes} minutes")

Tracking Neutral Item Drops:

# Use CDOTAUserMsg_FoundNeutralItem for neutral item pickups
result = parser.parse_universal(demo_path, "FoundNeutralItem", max_messages=100)

for msg in result.messages:
    player_id = msg.data.get('player_id')
    item_tier = msg.data.get('item_tier')  # 0-4
    tier = NeutralItemTier.from_value(item_tier)
    print(f"Player {player_id} found a {tier.display_name} neutral item")


ChatWheelMessage

Enum for Dota 2 chat wheel message IDs. Maps voice line IDs to human-readable text.

class ChatWheelMessage(int, Enum):
    # Standard phrases (0-232)
    OK = 0
    CAREFUL = 1
    GET_BACK = 2
    NEED_WARDS = 3
    STUN_NOW = 4
    HELP = 5
    PUSH_NOW = 6
    WELL_PLAYED = 7
    # ... many more standard phrases
    MY_BAD = 68
    SPACE_CREATED = 71
    BRUTAL_SAVAGE_REKT = 230
    # Dota Plus lines: 11000+
    # TI Battle Pass lines: 120000+
    # TI talent/team lines: 401000+

Properties:

Property Type Description
display_name str Human-readable message text

Class Methods:

Method Returns Description
from_id(id) ChatWheelMessage \| None Get enum from message ID
describe_id(id) str Get description for any ID (including unmapped)

Example:

from python_manta import MantaParser, ChatWheelMessage

parser = MantaParser()
game_info = parser.parse_game_info("match.dem")
players = {i: p.player_name for i, p in enumerate(game_info.players)}

result = parser.parse_universal("match.dem", "CDOTAUserMsg_ChatWheel", 100)

for msg in result.messages:
    player_id = msg.data.get('player_id', -1)
    player_name = players.get(player_id, f'Player {player_id}')
    msg_id = msg.data.get('chat_message_id', 0)

    # Use enum for known IDs, describe_id for all
    text = ChatWheelMessage.describe_id(msg_id)
    print(f"{player_name}: {text}")

# Output:
# Malr1ne: TI Battle Pass Voice Line #120009
# AMMAR_THE_F: > Space created
# Malr1ne: My bad


GameActivity

Enum for Dota 2 unit animation activity codes. Used in CDOTAUserMsg_TE_UnitAnimation messages to identify what animation a unit is playing. Useful for detecting taunts.

class GameActivity(int, Enum):
    # Basic states
    IDLE = 1500
    IDLE_RARE = 1501
    RUN = 1502
    ATTACK = 1503
    ATTACK2 = 1504
    DIE = 1506
    DISABLED = 1509
    # Ability casting
    CAST_ABILITY_1 = 1510
    CAST_ABILITY_2 = 1511
    # ... through CAST_ABILITY_6
    # Channeling
    CHANNEL_ABILITY_1 = 1520
    # ... through CHANNEL_ABILITY_6
    # Taunts
    KILLTAUNT = 1535
    TAUNT = 1536
    TAUNT_SNIPER = 1641
    TAUNT_SPECIAL = 1752
    CUSTOM_TOWER_TAUNT = 1756

Properties:

Property Type Description
display_name str Human-readable activity name
is_taunt bool True if this is a taunt animation
is_attack bool True if this is an attack animation
is_ability_cast bool True if this is an ability cast
is_channeling bool True if this is a channeling animation

Class Methods:

Method Returns Description
from_value(int) GameActivity \| None Get activity from integer value
get_taunt_activities() List[GameActivity] Get all taunt-related activities

Example:

from python_manta import MantaParser, GameActivity

parser = MantaParser()
result = parser.parse_universal("match.dem", "CDOTAUserMsg_TE_UnitAnimation", 10000)

# Find taunts
for msg in result.messages:
    activity_code = msg.data.get('activity', 0)
    activity = GameActivity.from_value(activity_code)

    if activity and activity.is_taunt:
        print(f"Taunt detected at tick {msg.tick}: {activity.display_name}")

# Check activity types
activity = GameActivity.ATTACK
print(activity.is_attack)      # True
print(activity.is_ability_cast) # False


Header Models

HeaderInfo

Match header metadata from the demo file.

class HeaderInfo(BaseModel):
    map_name: str              # Map name (e.g., "dota")
    server_name: str           # Server identifier
    client_name: str           # Client type
    game_directory: str        # Game directory path
    network_protocol: int      # Network protocol version
    demo_file_stamp: str       # Demo file signature
    build_num: int             # Game build number
    game: str                  # Game identifier
    server_start_tick: int     # Server start tick
    success: bool              # Parse success flag
    error: Optional[str]       # Error message if failed

Example:

header = parser.parse_header("match.dem")

# Access fields
print(header.map_name)
print(header.build_num)

# Convert to dict
data = header.model_dump()

# Convert to JSON
json_str = header.model_dump_json()


Game Info Models

GameInfo

Complete game information including draft, players, and teams.

class GameInfo(BaseModel):
    # Basic match info
    match_id: int              # Match ID
    game_mode: int             # Game mode ID
    game_winner: int           # Winner (2=Radiant, 3=Dire)
    league_id: int             # League ID (0 for pub matches)
    end_time: int              # End time (Unix timestamp)

    # Team info (pro matches only - 0/empty for pubs)
    radiant_team_id: int       # Radiant team ID
    dire_team_id: int          # Dire team ID
    radiant_team_tag: str      # Radiant team tag (e.g., "OG")
    dire_team_tag: str         # Dire team tag (e.g., "Secret")

    # Players
    players: List[PlayerInfo]  # All players in match

    # Draft
    picks_bans: List[DraftEvent]  # Draft sequence

    # Playback info
    playback_time: float       # Total playback time in seconds
    playback_ticks: int        # Total ticks
    playback_frames: int       # Total frames

    success: bool              # Parse success flag
    error: Optional[str]       # Error message if failed

DraftEvent

Single pick or ban event in the draft.

class DraftEvent(BaseModel):
    is_pick: bool    # True for pick, False for ban
    team: int        # 2 = Radiant, 3 = Dire
    hero_id: int     # Hero ID (see Dota 2 Wiki for mappings)

PlayerInfo

Player information from match metadata.

class PlayerInfo(BaseModel):
    hero_name: str           # Hero internal name (e.g., "npc_dota_hero_axe")
    player_name: str         # Player display name
    is_fake_client: bool     # True for bots
    steam_id: int            # Player Steam ID
    team: int                # Team (2=Radiant, 3=Dire)

Example:

game_info = parser.parse_game_info("match.dem")

# Basic match info
print(f"Match {game_info.match_id}")
# Convert to proper time format (H:MM:SS)
hours = int(game_info.playback_time // 3600)
mins = int((game_info.playback_time % 3600) // 60)
secs = int(game_info.playback_time % 60)
print(f"Duration: {hours}:{mins:02d}:{secs:02d}")
winner = "Radiant" if game_info.game_winner == 2 else "Dire"
print(f"Winner: {winner}")

# Team info (pro matches)
if game_info.league_id > 0:
    print(f"League: {game_info.league_id}")
    print(f"{game_info.radiant_team_tag} vs {game_info.dire_team_tag}")

# Players
for player in game_info.players:
    team = "Radiant" if player.team == 2 else "Dire"
    print(f"  {player.player_name} ({team}): {player.hero_name}")

# Draft
radiant_picks = [e for e in game_info.picks_bans if e.is_pick and e.team == 2]
dire_bans = [e for e in game_info.picks_bans if not e.is_pick and e.team == 3]

# Hero IDs: 1=Anti-Mage, 2=Axe, etc.
for pick in radiant_picks:
    print(f"Radiant picked hero {pick.hero_id}")


Universal Parse Models

UniversalParseResult

Result container for parse_universal().

class UniversalParseResult(BaseModel):
    messages: List[MessageEvent]  # Matched messages
    success: bool                 # Parse success flag
    error: Optional[str]          # Error message if failed
    count: int                    # Number of messages

MessageEvent

Single message from the replay.

class MessageEvent(BaseModel):
    type: str                    # Callback name (e.g., "CDOTAUserMsg_ChatMessage")
    tick: int                    # Game tick when message occurred
    net_tick: int                # Network tick
    data: Any                    # Message-specific data (dict)
    game_time: float             # Game time in seconds (negative before horn)
    game_time_str: str           # Formatted game time (e.g., "-0:40", "5:32")

Example:

result = parser.parse_universal("match.dem", "CDOTAUserMsg_ChatMessage", 100)

for msg in result.messages:
    # Access common fields
    print(f"Type: {msg.type}")
    print(f"Tick: {msg.tick}")

    # Access message-specific data
    player_id = msg.data.get('source_player_id')
    text = msg.data.get('message_text')


Game Events Models

GameEventsResult

Result container for parse_game_events().

class GameEventsResult(BaseModel):
    events: List[GameEventData]  # Parsed events
    event_types: List[str]       # Event type definitions (if capture_types=True)
    success: bool                # Parse success flag
    error: Optional[str]         # Error message if failed
    total_events: int            # Total events captured

GameEventData

Single game event with typed fields.

class GameEventData(BaseModel):
    name: str                     # Event name (e.g., "dota_combatlog")
    tick: int                     # Game tick
    net_tick: int                 # Network tick
    fields: Dict[str, Any]        # Event-specific fields

Example:

result = parser.parse_game_events("match.dem", event_filter="dota_player_kill", max_events=50)

for event in result.events:
    print(f"Event: {event.name}")
    print(f"Tick: {event.tick}")

    # Fields depend on event type
    for field_name, value in event.fields.items():
        print(f"  {field_name}: {value}")


Combat Log Models

CombatLogResult

Result container for parse_combat_log().

class CombatLogResult(BaseModel):
    entries: List[CombatLogEntry]  # Combat log entries
    success: bool                  # Parse success flag
    error: Optional[str]           # Error message if failed
    total_entries: int             # Total entries captured

CombatLogEntry

Single combat log entry with structured data.

class CombatLogEntry(BaseModel):
    tick: int                     # Game tick
    net_tick: int                 # Network tick
    type: int                     # Combat log type ID
    type_name: str                # Human-readable type name
    target_name: str              # Target unit name
    target_source_name: str       # Target source name
    attacker_name: str            # Attacker unit name
    damage_source_name: str       # Damage source name
    inflictor_name: str           # Ability/item that caused this
    is_attacker_illusion: bool    # Attacker is illusion
    is_attacker_hero: bool        # Attacker is a hero
    is_target_illusion: bool      # Target is illusion
    is_target_hero: bool          # Target is a hero
    is_visible_radiant: bool      # Visible to Radiant
    is_visible_dire: bool         # Visible to Dire
    value: int                    # Damage/heal value
    health: int                   # Target health after
    game_time: float              # Game time in seconds (negative before horn)
    game_time_str: str            # Formatted game time (e.g., "-0:40", "5:32")
    stun_duration: float          # Stun duration if applicable
    slow_duration: float          # Slow duration if applicable
    is_ability_toggle_on: bool    # Ability toggled on
    is_ability_toggle_off: bool   # Ability toggled off
    ability_level: int            # Ability level
    xp: int                       # XP reason/amount
    gold: int                     # Gold reason/amount
    last_hits: int                # Last hits at time
    attacker_team: int            # Attacker team ID
    target_team: int              # Target team ID
    attacker_hero_level: int      # Attacker's hero level (from entity state)
    target_hero_level: int        # Target's hero level (from entity state)

Hero Levels Now Available (v1.4.5.4+)

attacker_hero_level and target_hero_level are now populated from entity state during parsing. 100% of hero deaths have target_hero_level, 94%+ have attacker_hero_level.

Example:

result = parser.parse(combat_log={"types": [4], "heroes_only": True, "max_entries": 100})

for entry in result.combat_log.entries:
    if entry.type == 4:  # DEATH
        print(f"[{entry.game_time_str}] {entry.attacker_name} (lvl {entry.attacker_hero_level}) "
              f"killed {entry.target_name} (lvl {entry.target_hero_level})")


Attack Models

Attack models capture auto-attack projectiles from TE_Projectile messages. Unlike combat log which only records damage/kill events, attacks capture the actual attack action including tower attacks on creeps and neutral aggro.

AttacksResult

Result container for attacks parsing.

class AttacksResult(BaseModel):
    events: List[AttackEvent] = []  # Attack events
    total_events: int = 0           # Total events captured

AttackEvent

Single attack event (ranged projectile or melee hit).

class AttackEvent(BaseModel):
    # Timing
    tick: int                        # Game tick when attack was registered
    game_time: float = 0.0           # Game time in seconds
    game_time_str: str = ""          # Formatted game time (e.g., "15:34")
    launch_tick: int = 0             # Tick when projectile was launched (ranged only)

    # Entity identification
    source_index: int = 0            # Entity index of attacker
    target_index: int = 0            # Entity index of target
    source_handle: int = 0           # Raw entity handle (ranged only)
    target_handle: int = 0           # Raw entity handle (ranged only)
    attacker_name: str = ""          # Unit name (e.g., "npc_dota_hero_troll_warlord")
    target_name: str = ""            # Target unit name

    # Attack properties
    is_melee: bool = False           # True if melee attack (from combat log)
    projectile_speed: int = 0        # Projectile move speed (ranged only)
    dodgeable: bool = False          # Can be disjointed (ranged only)
    location_x: float = 0.0          # Attacker position X
    location_y: float = 0.0          # Attacker position Y

    # Melee-specific fields (from combat log DAMAGE events)
    damage: int = 0                  # Damage dealt (melee only)
    target_health: int = 0           # Target health AFTER attack (melee only)
    attacker_team: int = 0           # Attacker team: 2=Radiant, 3=Dire (melee only)
    target_team: int = 0             # Target team: 2=Radiant, 3=Dire (melee only)
    is_attacker_hero: bool = False   # Attacker is a hero (melee only)
    is_target_hero: bool = False     # Target is a hero (melee only)

Example:

from python_manta import Parser

parser = Parser("match.dem")
result = parser.parse(attacks={})

# Separate ranged and melee attacks
ranged = [a for a in result.attacks.events if not a.is_melee]
melee = [a for a in result.attacks.events if a.is_melee]
print(f"Ranged: {len(ranged)}, Melee: {len(melee)}")

# Hero vs hero melee combat (uses melee-specific fields)
hero_fights = [
    a for a in melee
    if a.is_attacker_hero and a.is_target_hero
]
for a in hero_fights[:5]:
    atk = a.attacker_name.replace("npc_dota_hero_", "")
    tgt = a.target_name.replace("npc_dota_hero_", "")
    print(f"[{a.game_time_str}] {atk} -> {tgt}: {a.damage} dmg, target HP: {a.target_health}")

# Ranged projectile analysis
fast_projectiles = [a for a in ranged if a.projectile_speed > 1000]
print(f"Fast projectiles (>1000 speed): {len(fast_projectiles)}")


Ward Models

Ward models capture the full lifecycle of observer and sentry wards: placement, expiration, and dewarding. See the Wards Reference for detailed usage.

WardsResult

Result container for ward lifecycle tracking.

class WardsResult(BaseModel):
    events: List[WardEvent] = []    # Ward events
    total_events: int = 0           # Total wards tracked

WardEvent

Single ward lifecycle event (placement through death/expiry).

class WardEvent(BaseModel):
    # Placement
    tick: int = 0                    # Tick when placed
    game_time: float = 0.0           # Game time in seconds
    game_time_str: str = ""          # Formatted time
    entity_id: int = 0               # Entity index
    ward_type: str = ""              # "observer" or "sentry"
    team: int = 0                    # 2=Radiant, 3=Dire
    x: float = 0.0                   # Placement X
    y: float = 0.0                   # Placement Y
    placed_by: str = ""              # Hero who placed (npc_dota_hero_*)

    # Death/Expiry
    death_tick: int = 0              # Tick when removed (0 if alive)
    death_game_time: float = 0.0     # Game time when removed
    death_game_time_str: str = ""    # Formatted death time
    was_killed: bool = False         # True = dewarded, False = expired
    killed_by: str = ""              # Hero who dewarded
    killer_team: int = 0             # Killer's team
    gold_bounty: int = 0            # Gold from deward (typically 50)

Example:

from python_manta import Parser

parser = Parser("match.dem")
result = parser.parse(wards={})

# Deward analysis
dewards = [w for w in result.wards.events if w.was_killed]
for w in dewards:
    killer = w.killed_by.replace("npc_dota_hero_", "")
    print(f"[{w.death_game_time_str}] {killer} dewarded {w.ward_type} +{w.gold_bounty}g")

# Vision control by team
radiant_obs = [w for w in result.wards.events if w.team == 2 and w.ward_type == "observer"]
dire_obs = [w for w in result.wards.events if w.team == 3 and w.ward_type == "observer"]
print(f"Observer wards: Radiant {len(radiant_obs)}, Dire {len(dire_obs)}")


Modifier Models

ModifiersResult

Result container for parse_modifiers().

class ModifiersResult(BaseModel):
    modifiers: List[ModifierEntry]  # Modifier entries
    success: bool                   # Parse success flag
    error: Optional[str]            # Error message if failed
    total_modifiers: int            # Total modifiers captured

ModifierEntry

Single modifier/buff entry.

class ModifierEntry(BaseModel):
    tick: int               # Game tick
    net_tick: int           # Network tick
    parent: int             # Entity handle of unit with modifier
    caster: int             # Entity handle of caster
    ability: int            # Ability that created modifier
    modifier_class: int     # Modifier class ID
    serial_num: int         # Serial number
    index: int              # Modifier index
    creation_time: float    # When modifier was created
    duration: float         # Duration in seconds (-1 = permanent)
    stack_count: int        # Number of stacks
    is_aura: bool           # Whether it's an aura
    is_debuff: bool         # Whether it's a debuff

Example:

result = parser.parse_modifiers("match.dem", max_modifiers=100)

for mod in result.modifiers:
    duration_str = f"{mod.duration}s" if mod.duration >= 0 else "permanent"
    print(f"Entity {mod.parent}: duration={duration_str}, stacks={mod.stack_count}")


Entity Models

EntitiesResult

Result container for query_entities().

class EntitiesResult(BaseModel):
    entities: List[EntityData]  # Entity data
    success: bool               # Parse success flag
    error: Optional[str]        # Error message if failed
    total_entities: int         # Total entities returned
    tick: int                   # Tick when captured
    net_tick: int               # Network tick when captured

EntityData

Single entity with properties.

class EntityData(BaseModel):
    index: int                    # Entity index
    serial: int                   # Entity serial number
    class_name: str               # Entity class name
    properties: Dict[str, Any]    # Entity properties

Common Hero Properties:

Property Type Description
m_iHealth int Current health
m_iMaxHealth int Maximum health
m_flMana float Current mana
m_flMaxMana float Maximum mana
m_vecOrigin list Position [x, y, z]
m_iCurrentLevel int Hero level
m_iTotalEarnedGold int Total gold earned
m_iKills int Kills
m_iDeaths int Deaths
m_iAssists int Assists

Example:

result = parser.query_entities("match.dem", class_filter="Hero", max_entities=10)

for entity in result.entities:
    print(f"\n{entity.class_name} (index={entity.index})")
    print(f"  Health: {entity.properties.get('m_iHealth')}/{entity.properties.get('m_iMaxHealth')}")
    print(f"  Level: {entity.properties.get('m_iCurrentLevel')}")


Snapshot Models

AbilitySnapshot

State of a single hero ability at a specific tick.

class AbilitySnapshot(BaseModel):
    slot: int = 0                 # Ability slot index (0-5 for regular abilities)
    name: str = ""                # Full ability class name (e.g., "CDOTA_Ability_Juggernaut_BladeFury")
    level: int = 0                # Current ability level (0-4 typically)
    cooldown: float = 0.0         # Current cooldown remaining
    max_cooldown: float = 0.0     # Maximum cooldown length
    mana_cost: int = 0            # Mana cost
    charges: int = 0              # Current charges (for charge-based abilities)
    is_ultimate: bool = False     # True if slot 5 (ultimate)

    # Properties
    short_name: str               # Name without "CDOTA_Ability_" prefix
    is_maxed: bool                # True if at max level (4 for regular, 3 for ultimate)
    is_on_cooldown: bool          # True if cooldown > 0

TalentChoice

Represents a talent selection at levels 10, 15, 20, or 25.

class TalentChoice(BaseModel):
    tier: int = 0                 # Talent tier (10, 15, 20, or 25)
    slot: int = 0                 # Raw ability slot index
    is_left: bool = True          # True if left talent was chosen
    name: str = ""                # Talent ability name

    # Properties
    side: str                     # "left" or "right"

ItemSnapshot

State of a single item in hero inventory at a specific tick.

class ItemSnapshot(BaseModel):
    slot: int = 0                 # Inventory slot (0-16)
    name: str = ""                # Item class name (e.g., "item_blink")
    charges: int = 0              # Current charges (wand, wards, etc.)
    cooldown: float = 0.0         # Remaining cooldown
    max_cooldown: float = 0.0     # Maximum cooldown length

    # Properties
    short_name: str               # Name without "item_" prefix (e.g., "blink")
    display_name: str             # Human-readable name (e.g., "Battle Fury", "Poor Man's Shield")
    is_main_inventory: bool       # True if slot 0-5
    is_backpack: bool             # True if slot 6-8
    is_tp_slot: bool              # True if slot 9
    is_stash: bool                # True if slot 10-15
    is_neutral_slot: bool         # True if slot 16
    is_on_cooldown: bool          # True if cooldown > 0
    # Purchasable item enum
    item_enum: Item | None        # Item enum if purchasable item
    is_purchasable_item: bool     # True if item is a purchasable (shop) item
    # Neutral item enum
    is_neutral_item: bool         # True if item is a neutral item (any slot)
    neutral_item_enum: NeutralItem | None  # NeutralItem enum if applicable

Inventory Slot Layout:

Slot Location
0-5 Main Inventory
6-8 Backpack
9 TP Slot
10-15 Stash
16 Neutral Item

HeroSnapshot

Hero state captured at a specific tick via parser.snapshot().

class HeroSnapshot(BaseModel):
    hero_name: str = ""           # Hero class name (e.g., "CDOTA_Unit_Hero_Axe")
    hero_id: int = 0              # Hero ID
    player_id: int = 0            # Player slot (0-9)
    team: int = 0                 # Team (2=Radiant, 3=Dire)
    x: float = 0.0                # World X position
    y: float = 0.0                # World Y position
    z: float = 0.0                # World Z position
    health: int = 0               # Current health
    max_health: int = 0           # Maximum health
    mana: float = 0.0             # Current mana
    max_mana: float = 0.0         # Maximum mana
    level: int = 0                # Hero level (1-30)
    is_alive: bool = True         # Alive status
    is_illusion: bool = False     # True if illusion
    is_clone: bool = False        # True if clone (Meepo, Arc Warden)
    # Combat stats
    armor: float = 0.0            # Physical armor value
    magic_resistance: float = 0.0 # Magic resistance percentage
    damage_min: int = 0           # Minimum attack damage
    damage_max: int = 0           # Maximum attack damage
    attack_range: int = 0         # Attack range
    # Attributes
    strength: float = 0.0         # Total strength (base + bonuses)
    agility: float = 0.0          # Total agility (base + bonuses)
    intellect: float = 0.0        # Total intellect (base + bonuses)
    # Abilities and talents
    abilities: List[AbilitySnapshot] = []  # All hero abilities with levels
    talents: List[TalentChoice] = []       # Chosen talents
    ability_points: int = 0                # Unspent ability points
    # Inventory (slots 0-5: main, 6-8: backpack, 9: TP, 10-15: stash, 16: neutral)
    inventory: List[ItemSnapshot] = []     # All items in inventory
    # Vision (live values, modified by items/spells/abilities)
    day_vision_range: int = 0     # Day vision range (default 1800, NS has 800)
    night_vision_range: int = 0   # Night vision range (default 800, Slark has 1800, NS has 1800)

    # Properties and methods
    has_ultimate: bool            # True if ultimate ability has been learned
    talents_chosen: int           # Number of talents selected (0-4)
    get_ability(name) -> Optional[AbilitySnapshot]  # Find ability by name (partial match)
    get_talent_at_tier(tier) -> Optional[TalentChoice]  # Get talent at tier (10/15/20/25)
    # Inventory helpers
    main_inventory: List[ItemSnapshot]    # Items in slots 0-5
    backpack: List[ItemSnapshot]          # Items in slots 6-8
    stash: List[ItemSnapshot]             # Items in slots 10-15
    neutral_item: Optional[ItemSnapshot]  # Item in slot 16
    tp_scroll: Optional[ItemSnapshot]     # Item in slot 9
    get_item(name) -> Optional[ItemSnapshot]  # Find item by name (partial match)
    has_item(name) -> bool                # Check if hero has item

EntityStateSnapshot

Result container for parser.snapshot().

class EntityStateSnapshot(BaseModel):
    tick: int = 0                   # Tick when snapshot was captured
    game_time: float = 0.0          # Game time in seconds
    heroes: List[HeroSnapshot] = [] # All hero states
    success: bool = True            # Parse success flag
    error: Optional[str] = None     # Error message if failed
    # Day/night cycle
    is_night: bool = False              # True during nighttime
    is_nightstalker_night: bool = False # True during Night Stalker ultimate darkness
    is_temporary_night: bool = False    # True during temporary night (e.g., NS ult)
    is_temporary_day: bool = False      # True during temporary day
    net_time_of_day: int = 0            # Raw time-of-day value (0-65535, <20000 = night)

Example - Basic Hero State:

from python_manta import Parser

parser = Parser("match.dem")
index = parser.build_index(interval_ticks=1800)

# Get hero state at 10 minutes
target_tick = index.game_started + (10 * 60 * 30)
snap = parser.snapshot(target_tick=target_tick)

for hero in snap.heroes:
    name = hero.hero_name.replace("CDOTA_Unit_Hero_", "")
    print(f"{name}: Lvl {hero.level}, HP {hero.health}/{hero.max_health}")
    print(f"  Armor: {hero.armor:.1f}, Magic Resist: {hero.magic_resistance:.0f}%")
    print(f"  Damage: {hero.damage_min}-{hero.damage_max}")
    print(f"  STR: {hero.strength:.1f}, AGI: {hero.agility:.1f}, INT: {hero.intellect:.1f}")

Example - Abilities and Talents:

from python_manta import Parser

parser = Parser("match.dem")
snap = parser.snapshot(target_tick=60000)  # ~33 minutes

for hero in snap.heroes:
    print(f"{hero.hero_name} (Level {hero.level})")

    # Show all abilities with levels
    for ability in hero.abilities:
        status = "[MAX]" if ability.is_maxed else ""
        cd = f" (CD: {ability.cooldown:.1f}s)" if ability.is_on_cooldown else ""
        print(f"  {ability.short_name}: Level {ability.level}{status}{cd}")

    # Show talent choices
    print(f"  Talents: {hero.talents_chosen}/4")
    for talent in hero.talents:
        print(f"    Level {talent.tier}: {talent.side}")

    # Find specific ability by name
    omnislash = hero.get_ability("Omnislash")
    if omnislash and omnislash.level > 0:
        print(f"  Has Omnislash level {omnislash.level}")

    # Check if ultimate is learned
    if hero.has_ultimate:
        print("  Ultimate learned!")

    # Get talent at specific tier
    lvl20_talent = hero.get_talent_at_tier(20)
    if lvl20_talent:
        print(f"  Level 20 talent: {lvl20_talent.side}")

Example - Inventory:

from python_manta import Parser

parser = Parser("match.dem")
snap = parser.snapshot(target_tick=60000)  # ~17 minutes

for hero in snap.heroes:
    print(f"{hero.hero_name}")

    # Main inventory
    for item in hero.main_inventory:
        charges = f" x{item.charges}" if item.charges else ""
        print(f"  [{item.slot}] {item.short_name}{charges}")

    # Neutral item
    if hero.neutral_item:
        print(f"  Neutral: {hero.neutral_item.short_name}")

    # Find specific item
    bkb = hero.get_item("black_king_bar")
    if bkb:
        print(f"  Has BKB in slot {bkb.slot}")

    # Check item presence
    if hero.has_item("blink"):
        print("  Has Blink Dagger!")


String Table Models

StringTablesResult

Result container for get_string_tables().

class StringTablesResult(BaseModel):
    tables: Dict[str, List[StringTableData]]  # Table name -> entries
    table_names: List[str]                    # List of table names
    success: bool                             # Parse success flag
    error: Optional[str]                      # Error message if failed
    total_entries: int                        # Total entries

StringTableData

Single string table entry.

class StringTableData(BaseModel):
    table_name: str         # Table name
    index: int              # Entry index
    key: str                # Entry key
    value: Optional[str]    # Entry value (may be binary/base64)

Parser Info Model

ParserInfo

Parser state information.

class ParserInfo(BaseModel):
    game_build: int           # Game build number
    tick: int                 # Final parser tick
    net_tick: int             # Final network tick
    string_tables: List[str]  # List of string table names
    entity_count: int         # Number of entities
    success: bool             # Parse success flag
    error: Optional[str]      # Error message if failed

Example:

info = parser.get_parser_info("match.dem")

print(f"Game lasted {info.tick} ticks")
print(f"Final entity count: {info.entity_count}")
print(f"String tables: {', '.join(info.string_tables)}")


Serialization

All models support Pydantic serialization:

# To dictionary
data = model.model_dump()

# To JSON string
json_str = model.model_dump_json()

# From dictionary
model = HeaderInfo.model_validate(data)

# From JSON
model = HeaderInfo.model_validate_json(json_str)