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), 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.
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}")
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)
timestamp: Optional[int] # Unix timestamp in milliseconds
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
timestamp: float # Game time in seconds
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
Example:
result = parser.parse_combat_log("match.dem", types=[0], heroes_only=True, max_entries=100)
for entry in result.entries:
if entry.type == 0: # DAMAGE
print(f"[{entry.timestamp:.1f}s] {entry.attacker_name} hit {entry.target_name} for {entry.value}")
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"
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
# 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)
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
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}")
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: