Skip to content

Examples

AI Summary

Complete working examples for common Dota 2 replay analysis tasks. All examples use TI 2025 Grand Finals Game 5 (XG vs Falcons, Match ID: 8461956309) as the reference replay. Includes: match summary generator (header + draft + final stats), kill timeline builder, item build tracker (via combat log type 11), item usage tracker (combat log type 6), Lotus Orb detection (ability triggers type 13), neutral item tracker (pickups via FoundNeutralItem, usage via combat log, buffs via modifiers), ward placement analyzer, damage report generator, rune tracking, and defensive item analysis (Outworld Staff vs Omnislash). Each example demonstrates combining multiple API methods for real analysis workflows.


Example Match: TI 2025 Grand Finals Game 5

All examples in this documentation use The International 2025 Grand Finals Game 5 as the reference replay:

Property Value
Match ID 8461956309
Event The International 2025 Grand Finals
Teams Xtreme Gaming (XG) vs Team Falcons (FLCN)
Winner Falcons (Dire)
Duration 1:17:54 (4674 seconds)
Server Valve TI14 Server (Hamburg)

XG (Radiant): - Ame: Juggernaut - Xm: Shadow Fiend - Xxs: Earthshaker - XinQ: Shadow Demon - xNova: Pugna

Falcons (Dire): - Sneyking: Naga Siren - skiter: Medusa - Malr1ne: Pangolier - AMMAR_THE_F: Magnus - Cr1t-: Disruptor


Match Summary

Extract a complete match summary with draft, players, and final scores.

from python_manta import MantaParser, Hero

def generate_match_summary(demo_path: str):
    parser = MantaParser()

    # Get header
    header = parser.parse_header(demo_path)
    print(f"Match Summary")
    print(f"{'='*50}")
    print(f"Map: {header.map_name}")
    print(f"Build: {header.build_num}")
    print(f"Server: {header.server_name}")

    # Get game info (draft)
    game_info = parser.parse_game_info(demo_path)
    print(f"\nDraft:")
    print("-" * 30)

    radiant_picks = []
    dire_picks = []
    for event in game_info.picks_bans:
        if event.is_pick:
            hero = Hero.from_id(event.hero_id)
            hero_name = hero.display_name if hero else f"Unknown({event.hero_id})"
            if event.team == 2:
                radiant_picks.append(hero_name)
            else:
                dire_picks.append(hero_name)

    print(f"Radiant picks: {radiant_picks}")
    print(f"Dire picks: {dire_picks}")

    # Get final hero stats
    heroes = parser.query_entities(
        demo_path,
        class_filter="Hero",
        property_filter=[
            "m_iCurrentLevel", "m_iKills", "m_iDeaths", "m_iAssists",
            "m_iTotalEarnedGold", "m_iLastHits", "m_iTeamNum"
        ],
        max_entities=10
    )

    print(f"\nFinal Scoreboard:")
    print("-" * 70)
    print(f"{'Hero':<35} {'Lvl':>4} {'K':>3} {'D':>3} {'A':>3} {'LH':>5} {'Gold':>7}")
    print("-" * 70)

    radiant = []
    dire = []
    for hero in heroes.entities:
        p = hero.properties
        team = p.get("m_iTeamNum", 0)
        stats = (hero.class_name, p.get("m_iCurrentLevel", 0), p.get("m_iKills", 0),
                 p.get("m_iDeaths", 0), p.get("m_iAssists", 0), p.get("m_iLastHits", 0),
                 p.get("m_iTotalEarnedGold", 0))

        if team == 2:
            radiant.append(stats)
        elif team == 3:
            dire.append(stats)

    print("RADIANT:")
    for name, lvl, k, d, a, lh, gold in radiant:
        print(f"  {name:<33} {lvl:>4} {k:>3} {d:>3} {a:>3} {lh:>5} {gold:>7,}")

    print("\nDIRE:")
    for name, lvl, k, d, a, lh, gold in dire:
        print(f"  {name:<33} {lvl:>4} {k:>3} {d:>3} {a:>3} {lh:>5} {gold:>7,}")

# Usage
generate_match_summary("match.dem")

Expected Output (TI 2025 Grand Finals Game 5):

Match Summary
==================================================
Map: start
Build: 10512
Server: Valve TI14 Server (srcds427-fra2.Hamburg.5)

Match ID: 8461956309
Duration: 1:17:54
Winner: Dire

Teams:
  Radiant: XG (ID: 8261500)
  Dire: FLCN (ID: 9247354)

Draft:
  Radiant picks: ['Earthshaker', 'Shadow Demon', 'Shadow Fiend', 'Pugna', 'Juggernaut']
  Dire picks: ['Naga Siren', 'Pangolier', 'Disruptor', 'Medusa', 'Magnus']

Players:
  [Radiant] Ame: juggernaut
  [Radiant] Xm: nevermore
  [Radiant] Xxs: earthshaker
  [Radiant] XinQ: shadow_demon
  [Radiant] xNova: pugna
  [Dire] Sneyking: naga_siren
  [Dire] skiter: medusa
  [Dire] Malr1ne: pangolier
  [Dire] AMMAR_THE_F: magnataur
  [Dire] Cr1t-: disruptor


Kill Timeline

Build a chronological kill feed with timestamps using combat log.

from python_manta import MantaParser, CombatLogType

def build_kill_timeline(demo_path: str):
    parser = MantaParser()

    # Use combat log DEATH type for kills
    result = parser.parse_combat_log(
        demo_path,
        types=[CombatLogType.DEATH],
        max_entries=5000
    )

    # Filter for hero deaths only
    hero_deaths = [e for e in result.entries if "npc_dota_hero_" in e.target_name]

    print("Kill Timeline")
    print("=" * 60)

    for entry in hero_deaths:
        mins = int(entry.game_time // 60)
        secs = int(entry.game_time % 60)
        victim = entry.target_name.replace("npc_dota_hero_", "")
        killer = entry.attacker_name.replace("npc_dota_hero_", "")
        print(f"[{mins:02d}:{secs:02d}] {killer} killed {victim}")

    print(f"\nTotal hero deaths: {len(hero_deaths)}")

# Usage
build_kill_timeline("match.dem")

Expected Output (TI 2025 Grand Finals):

Kill Timeline
============================================================
[04:48] disruptor killed earthshaker
[05:28] naga_siren killed shadow_demon
[06:06] nevermore killed pangolier
[06:13] shadow_demon killed disruptor
[09:11] juggernaut killed magnataur
[11:08] magnataur killed juggernaut
[11:12] pangolier killed shadow_demon
[13:39] juggernaut killed magnataur
[14:49] pangolier killed pugna
[15:27] juggernaut killed disruptor
[18:20] disruptor killed pugna
[18:35] pangolier killed earthshaker
[27:04] magnataur killed pugna
[27:19] medusa killed shadow_demon
[32:30] shadow_demon killed pangolier
[34:06] nevermore killed pangolier
[34:17] medusa killed shadow_demon
[35:53] medusa killed pugna
[38:03] disruptor killed pugna
[38:27] nevermore killed magnataur
[53:21] pangolier killed pugna
[64:13] pangolier killed juggernaut
[76:25] pangolier killed juggernaut
[76:38] medusa killed earthshaker
[76:51] medusa killed nevermore

Total hero deaths: 25


Item Build Tracker

Track item purchases for each player throughout the match using combat log.

from python_manta import MantaParser, CombatLogType
from collections import defaultdict

def track_item_builds(demo_path: str):
    parser = MantaParser()

    # Item purchases are in combat log PURCHASE type
    result = parser.parse_combat_log(
        demo_path,
        types=[CombatLogType.PURCHASE],
        max_entries=2000
    )

    # Group by hero
    hero_items = defaultdict(list)

    for entry in result.entries:
        hero = entry.target_name.replace("npc_dota_hero_", "")
        item = entry.value_name.replace("item_", "")

        # Calculate game time
        mins = int(abs(entry.game_time) // 60)
        secs = int(abs(entry.game_time)) % 60
        sign = "-" if entry.game_time < 0 else ""

        hero_items[hero].append({
            "time": f"{sign}{mins:02d}:{secs:02d}",
            "item": item,
            "game_time": entry.game_time
        })

    print("Item Build Order by Hero")
    print("=" * 60)

    for hero in sorted(hero_items.keys()):
        items = hero_items[hero]
        print(f"\n{hero}: ({len(items)} items)")
        print("-" * 50)

        for i, item in enumerate(items, 1):
            print(f"  {i:>3}. [{item['time']}] {item['item']}")

# Usage
track_item_builds("match.dem")

Expected Output (TI 2025 Grand Finals - Ame's Juggernaut build):

Item Build Order by Hero
============================================================

juggernaut: (54 items)
--------------------------------------------------
    1. [01:04] recipe_wraith_band
    2. [01:54] wraith_band
    3. [02:06] boots_of_elves
    4. [03:35] gloves
    5. [04:53] boots
    6. [05:32] power_treads          # Power Treads at 5:32
    7. [08:05] cornucopia
    8. [09:36] broadsword
    9. [12:01] broadsword
   10. [13:17] recipe_bfury
   11. [13:44] bfury                 # Battle Fury at 13:44
   12. [14:18] blade_of_alacrity
   13. [15:14] boots_of_elves
   14. [15:31] recipe_yasha
   15. [16:11] yasha
   16. [16:56] diadem
   17. [19:20] recipe_manta
   18. [19:44] manta                 # Manta Style at 19:44
   19. [22:56] eagle
   20. [24:19] claymore
   21. [25:56] talisman_of_evasion
   22. [26:40] butterfly             # Butterfly at 26:40
   ...


Ward Placement Analyzer

Analyze ward placement patterns from pings and events.

from python_manta import MantaParser
from collections import defaultdict

def analyze_wards(demo_path: str):
    parser = MantaParser()

    # Get ward-related entities at end of game
    wards = parser.query_entities(
        demo_path,
        class_filter="Ward",
        property_filter=["m_vecOrigin", "m_iTeamNum", "m_iHealth"],
        max_entities=100
    )

    print("Ward Analysis")
    print("=" * 50)

    radiant_wards = []
    dire_wards = []

    for ward in wards.entities:
        pos = ward.properties.get("m_vecOrigin", [0, 0, 0])
        team = ward.properties.get("m_iTeamNum", 0)
        alive = ward.properties.get("m_iHealth", 0) > 0

        ward_info = {
            "class": ward.class_name,
            "position": (pos[0], pos[1]),
            "alive": alive
        }

        if team == 2:
            radiant_wards.append(ward_info)
        elif team == 3:
            dire_wards.append(ward_info)

    print(f"\nRadiant wards (end of game): {len(radiant_wards)}")
    for w in radiant_wards:
        status = "ALIVE" if w["alive"] else "dead"
        print(f"  {w['class']}: ({w['position'][0]:.0f}, {w['position'][1]:.0f}) [{status}]")

    print(f"\nDire wards (end of game): {len(dire_wards)}")
    for w in dire_wards:
        status = "ALIVE" if w["alive"] else "dead"
        print(f"  {w['class']}: ({w['position'][0]:.0f}, {w['position'][1]:.0f}) [{status}]")

# Usage
analyze_wards("match.dem")

Damage Report

Generate a damage breakdown by hero and ability.

from python_manta import MantaParser
from collections import defaultdict

def generate_damage_report(demo_path: str):
    parser = MantaParser()

    result = parser.parse_combat_log(
        demo_path,
        types=[0],  # Damage only
        heroes_only=True,
        max_entries=10000
    )

    # Aggregate damage by attacker
    damage_dealt = defaultdict(int)
    damage_taken = defaultdict(int)
    ability_damage = defaultdict(lambda: defaultdict(int))

    for entry in result.entries:
        if entry.is_attacker_hero:
            damage_dealt[entry.attacker_name] += entry.value

            if entry.inflictor_name:
                ability_damage[entry.attacker_name][entry.inflictor_name] += entry.value

        if entry.is_target_hero:
            damage_taken[entry.target_name] += entry.value

    print("Damage Report")
    print("=" * 60)

    print("\nTotal Damage Dealt:")
    print("-" * 40)
    for hero, dmg in sorted(damage_dealt.items(), key=lambda x: -x[1]):
        print(f"  {hero:<35}: {dmg:>10,}")

    print("\nTotal Damage Taken:")
    print("-" * 40)
    for hero, dmg in sorted(damage_taken.items(), key=lambda x: -x[1]):
        print(f"  {hero:<35}: {dmg:>10,}")

    print("\nTop Damage Sources by Hero:")
    print("-" * 60)
    for hero in sorted(ability_damage.keys()):
        abilities = ability_damage[hero]
        top_abilities = sorted(abilities.items(), key=lambda x: -x[1])[:5]

        print(f"\n{hero}:")
        for ability, dmg in top_abilities:
            print(f"    {ability:<30}: {dmg:>8,}")

# Usage
generate_damage_report("match.dem")

Expected Output (TI 2025 Grand Finals):

Damage Report
============================================================

Total Damage Dealt:
----------------------------------------
  medusa                           :    923,091
  juggernaut                       :    878,418
  nevermore                        :    629,762
  earthshaker                      :    507,264
  pangolier                        :    421,043
  magnataur                        :    273,972
  naga_siren                       :    265,125
  pugna                            :    150,655
  shadow_demon                     :     60,697
  disruptor                        :     45,610

Note: skiter's Medusa dealt the most damage overall (923k), while Ame's Juggernaut (878k) was the second highest. This includes all damage to heroes, not just hero-vs-hero.


Team Fight Detector

Detect team fights by analyzing death clusters.

from python_manta import MantaParser
from collections import defaultdict

def detect_team_fights(demo_path: str, tick_window: int = 600):
    parser = MantaParser()

    # Get deaths from combat log
    result = parser.parse_combat_log(
        demo_path,
        types=[4],  # Deaths
        heroes_only=True,
        max_entries=500
    )

    if not result.entries:
        print("No hero deaths found")
        return

    # Group deaths by time window
    fights = []
    current_fight = []

    for entry in sorted(result.entries, key=lambda e: e.tick):
        if not current_fight:
            current_fight.append(entry)
        elif entry.tick - current_fight[-1].tick <= tick_window:
            current_fight.append(entry)
        else:
            if len(current_fight) >= 3:  # At least 3 deaths = team fight
                fights.append(current_fight)
            current_fight = [entry]

    # Don't forget last fight
    if len(current_fight) >= 3:
        fights.append(current_fight)

    print("Team Fight Detection")
    print("=" * 60)
    print(f"Found {len(fights)} team fights (3+ deaths within {tick_window} ticks)")

    for i, fight in enumerate(fights, 1):
        start_tick = fight[0].tick
        end_tick = fight[-1].tick
        duration = end_tick - start_tick

        radiant_deaths = sum(1 for e in fight if e.target_team == 2)
        dire_deaths = sum(1 for e in fight if e.target_team == 3)

        print(f"\nFight #{i}: Tick {start_tick} - {end_tick} ({duration} ticks)")
        print(f"  Deaths: Radiant {radiant_deaths}, Dire {dire_deaths}")
        print(f"  Outcome: {'Radiant' if dire_deaths > radiant_deaths else 'Dire'} won")

        print(f"  Deaths:")
        for entry in fight:
            team = "R" if entry.target_team == 2 else "D"
            print(f"    [{team}] {entry.target_name} killed by {entry.attacker_name}")

# Usage
detect_team_fights("match.dem")

Chat Log Extractor

Extract and format all chat messages.

from python_manta import MantaParser

def extract_chat_log(demo_path: str):
    parser = MantaParser()

    result = parser.parse_universal(
        demo_path,
        "CDOTAUserMsg_ChatMessage",
        max_messages=1000
    )

    # Get string tables for player names (if available)
    userinfo = parser.get_string_tables(
        demo_path,
        table_names=["userinfo"],
        max_entries=20
    )

    print("Chat Log")
    print("=" * 60)

    chat_types = {1: "ALL", 2: "TEAM", 3: "SPEC"}

    for msg in result.messages:
        player_id = msg.data.get("source_player_id", "?")
        text = msg.data.get("message_text", "")
        chat_type = msg.data.get("chat_type", 0)

        if not text:
            continue

        type_str = chat_types.get(chat_type, "???")
        tick = msg.tick

        print(f"[{tick:>7}] [{type_str:>4}] Player {player_id}: {text}")

# Usage
extract_chat_log("match.dem")

Expected Output (TI 2025 Grand Finals):

Chat Log
============================================================
[  28528] Player 1: gl hf
[  28534] Player 9: hf
[  28590] Player 0: gl
[  28624] Player 4: glhf
[  29624] Player 5: the lights are too much
[  29760] Player 7: its fine
[  29784] Player 7: g
[  29864] Player 4: g
[  29898] Player 1: G
[  65276] Player 5: teamspeak delay
[  67360] Player 9: g
[  67368] Player 6: g
[  67464] Player 3: g
[ 138941] Player 1: gg
[ 138991] Player 0: gg
[ 138997] Player 4: gg
[ 139019] Player 8: gg

Total chat messages: 17

Note: The TI Grand Finals captured the pre-game "gl hf" exchanges, a technical pause for "teamspeak delay", and the final "gg" at match end.


Rune Tracking

Track rune pickups throughout the match using combat log modifiers.

from python_manta import MantaParser, RuneType
from collections import defaultdict

def track_runes(demo_path: str):
    parser = MantaParser()

    # Rune pickups are tracked via combat log modifiers
    # When a hero picks up a rune, they receive a modifier_rune_* buff
    result = parser.parse_combat_log(
        demo_path,
        types=[2],  # MODIFIER_ADD only
        heroes_only=True,
        max_entries=50000
    )

    # Filter for rune modifiers using the RuneType enum
    rune_pickups = [
        e for e in result.entries
        if RuneType.is_rune_modifier(e.inflictor_name)
    ]

    print("Rune Tracking")
    print("=" * 50)
    print(f"\nTotal Rune Pickups: {len(rune_pickups)}")
    print("-" * 40)

    # Group by hero
    hero_runes = defaultdict(list)
    for pickup in rune_pickups:
        hero = pickup.target_name.replace("npc_dota_hero_", "")
        rune = RuneType.from_modifier(pickup.inflictor_name)
        rune_name = rune.display_name if rune else pickup.inflictor_name
        hero_runes[hero].append({
            "time": pickup.timestamp,
            "rune": rune_name
        })

    # Print timeline
    print("\nRune Pickup Timeline:")
    print("-" * 60)
    for pickup in rune_pickups:
        hero = pickup.target_name.replace("npc_dota_hero_", "")
        rune = RuneType.from_modifier(pickup.inflictor_name)
        rune_name = rune.display_name if rune else pickup.inflictor_name
        minutes = int(pickup.timestamp // 60)
        seconds = int(pickup.timestamp % 60)
        print(f"[{minutes:02d}:{seconds:02d}] {hero:<20} picked up {rune_name}")

    # Print summary by hero
    print("\nRunes by Hero:")
    print("-" * 40)
    for hero, runes in sorted(hero_runes.items()):
        rune_summary = ", ".join(r["rune"] for r in runes)
        print(f"  {hero}: {len(runes)} runes ({rune_summary})")

# Usage
track_runes("match.dem")

Expected Output (TI 2025 Grand Finals):

Rune Tracking
==================================================

Total Rune Pickups: 19
----------------------------------------

Rune Pickup Timeline:
------------------------------------------------------------
[23:30] naga_siren           picked up Arcane
[26:05] pangolier            picked up Invisibility
[28:44] pangolier            picked up Haste
[30:45] pangolier            picked up Double Damage
[35:56] pangolier            picked up Regeneration
[37:17] naga_siren           picked up Double Damage
[40:29] pangolier            picked up Invisibility
[41:24] pugna                picked up Shield
[44:55] pangolier            picked up Regeneration
[45:57] naga_siren           picked up Haste
[49:02] pangolier            picked up Arcane
[51:25] medusa               picked up Arcane
[53:40] medusa               picked up Double Damage
[58:01] pangolier            picked up Shield
[59:24] magnataur            picked up Invisibility
[63:44] pangolier            picked up Regeneration
[67:22] magnataur            picked up Shield
[71:20] magnataur            picked up Invisibility
[73:34] medusa               picked up Arcane

Runes by Hero:
----------------------------------------
  magnataur: 3 runes (Invisibility, Shield, Invisibility)
  medusa: 3 runes (Arcane, Double Damage, Arcane)
  naga_siren: 3 runes (Arcane, Double Damage, Haste)
  pangolier: 9 runes (Invisibility, Haste, Double Damage, Regeneration, Invisibility, Regeneration, Arcane, Shield, Regeneration)
  pugna: 1 runes (Shield)


Item Usage Tracker

Track when heroes use active items (smoke, wards, BKB, etc.).

from python_manta import MantaParser, CombatLogType
from collections import defaultdict

def track_item_usage(demo_path: str):
    parser = MantaParser()

    # Item usage is in combat log ITEM type
    result = parser.parse_combat_log(
        demo_path,
        types=[CombatLogType.ITEM],
        heroes_only=True,
        max_entries=2000
    )

    print("Item Usage Tracking")
    print("=" * 60)
    print(f"Total item activations: {result.total_entries}\n")

    # Group by hero
    hero_usage = defaultdict(lambda: defaultdict(int))

    for entry in result.entries:
        hero = entry.attacker_name.replace("npc_dota_hero_", "")
        item = entry.inflictor_name.replace("item_", "")
        hero_usage[hero][item] += 1

    # Print usage by hero
    for hero in sorted(hero_usage.keys()):
        items = hero_usage[hero]
        print(f"\n{hero}:")
        for item, count in sorted(items.items(), key=lambda x: -x[1]):
            print(f"  {item}: {count}x")

# Usage
track_item_usage("match.dem")

Lotus Orb Detection

Detect Lotus Orb spell reflections using ability triggers.

from python_manta import MantaParser, CombatLogType

def detect_lotus_orb(demo_path: str):
    parser = MantaParser()

    # Ability triggers include Lotus Orb reflections
    result = parser.parse_combat_log(
        demo_path,
        types=[CombatLogType.ABILITY_TRIGGER],
        max_entries=500
    )

    print("Lotus Orb / Ability Trigger Detection")
    print("=" * 60)

    lotus_events = []
    other_triggers = []

    for entry in result.entries:
        hero = entry.attacker_name.replace("npc_dota_hero_", "")
        target = entry.target_name.replace("npc_dota_hero_", "")
        item = entry.inflictor_name

        # Calculate time
        mins = int(entry.game_time // 60)
        secs = int(entry.game_time % 60)

        event = {
            "time": f"{mins:02d}:{secs:02d}",
            "hero": hero,
            "target": target,
            "item": item
        }

        if "lotus_orb" in item:
            lotus_events.append(event)
        else:
            other_triggers.append(event)

    if lotus_events:
        print(f"\nLotus Orb Reflections ({len(lotus_events)} total):")
        print("-" * 50)
        for e in lotus_events:
            print(f"[{e['time']}] {e['hero']} reflected spell to {e['target']}")

    if other_triggers:
        print(f"\nOther Ability Triggers ({len(other_triggers)} total):")
        print("-" * 50)
        for e in other_triggers[:10]:  # Show first 10
            print(f"[{e['time']}] {e['hero']} -> {e['target']} ({e['item']})")

# Usage
detect_lotus_orb("match.dem")

Neutral Item Tracker

Track neutral item drops, pickups, and usage throughout the game.

from python_manta import MantaParser, NeutralItem, NeutralItemTier, CombatLogType

def track_neutral_items(demo_path: str):
    parser = MantaParser()

    print("Neutral Item Analysis")
    print("=" * 60)

    # 1. Track neutral item pickups via CDOTAUserMsg_FoundNeutralItem
    found_result = parser.parse_universal(demo_path, "FoundNeutralItem", max_messages=100)

    print(f"\nNeutral Items Found: {len(found_result.messages)}")
    print("-" * 40)

    items_by_tier = {t: [] for t in NeutralItemTier}

    for msg in found_result.messages:
        player_id = msg.data.get('player_id')
        item_tier_value = msg.data.get('item_tier', 0)
        tier = NeutralItemTier.from_value(item_tier_value)

        if tier:
            mins = int(msg.tick / 30 / 60)  # Approximate game time
            items_by_tier[tier].append({
                "player": player_id,
                "time_approx": f"~{mins}m",
                "tick": msg.tick
            })

    for tier in NeutralItemTier:
        items = items_by_tier[tier]
        if items:
            print(f"\n{tier.display_name} (unlocks at {tier.unlock_time_minutes}min): {len(items)} drops")
            for item in items[:5]:  # Show first 5
                print(f"  Player {item['player']} at {item['time_approx']}")

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

    print(f"\n\nNeutral Item Active Usage")
    print("-" * 40)

    usage_count = {}
    for entry in result.entries:
        if NeutralItem.is_neutral_item(entry.inflictor_name):
            item = NeutralItem.from_item_name(entry.inflictor_name)
            if item:
                key = (entry.attacker_name, item.display_name)
                usage_count[key] = usage_count.get(key, 0) + 1

    # Sort by usage count
    sorted_usage = sorted(usage_count.items(), key=lambda x: x[1], reverse=True)
    for (hero, item_name), count in sorted_usage[:15]:
        hero_short = hero.replace("npc_dota_hero_", "")
        print(f"  {hero_short}: {item_name} x{count}")

    # 3. Track neutral item modifiers/buffs
    mod_result = parser.parse_combat_log(
        demo_path,
        types=[CombatLogType.MODIFIER_ADD],
        max_entries=2000
    )

    print(f"\n\nNeutral Item Buffs Applied")
    print("-" * 40)

    buff_count = {}
    for entry in mod_result.entries:
        inflictor = entry.inflictor_name
        # Check if it's a neutral item modifier
        if inflictor and inflictor.startswith("modifier_item_"):
            # Extract item name from modifier
            item_name = inflictor.replace("modifier_", "")
            if NeutralItem.is_neutral_item(item_name):
                buff_count[inflictor] = buff_count.get(inflictor, 0) + 1

    for mod, count in sorted(buff_count.items(), key=lambda x: x[1], reverse=True)[:10]:
        print(f"  {mod}: {count} applications")

# Usage
track_neutral_items("match.dem")

Defensive Item vs Ultimate Analysis

Analyze how defensive items counter ultimates. This example shows how skiter's Outworld Staff nullified Ame's Omnislash in a crucial teamfight at 64:08.

from python_manta import MantaParser, CombatLogType, NeutralItem

def analyze_defensive_counter(demo_path: str, start_time: float, end_time: float):
    """Analyze a specific window for ability vs defensive item interactions."""
    parser = MantaParser()

    print("Defensive Item Analysis")
    print("=" * 60)
    print(f"Window: {start_time/60:.0f}:{start_time%60:02.0f} - {end_time/60:.0f}:{end_time%60:02.0f}")

    # Get abilities cast in window
    abilities = parser.parse_combat_log(demo_path, types=[CombatLogType.ABILITY], max_entries=100000)
    items = parser.parse_combat_log(demo_path, types=[CombatLogType.ITEM], max_entries=100000)
    damage = parser.parse_combat_log(demo_path, types=[CombatLogType.DAMAGE], max_entries=500000)
    mods = parser.parse_combat_log(demo_path, types=[CombatLogType.MODIFIER_ADD], max_entries=100000)

    window_abilities = [e for e in abilities.entries if start_time <= e.timestamp <= end_time]
    window_items = [e for e in items.entries if start_time <= e.timestamp <= end_time]
    window_damage = [e for e in damage.entries if start_time <= e.timestamp <= end_time]
    window_mods = [e for e in mods.entries if start_time <= e.timestamp <= end_time]

    print("\n--- Abilities Cast ---")
    for e in window_abilities:
        mins = int(e.timestamp // 60)
        secs = int(e.timestamp % 60)
        caster = e.attacker_name.replace('npc_dota_hero_', '')
        ability = e.inflictor_name
        target = e.target_name.replace('npc_dota_hero_', '')
        print(f"  [{mins:02d}:{secs:02d}] {caster} -> {ability} on {target}")

    print("\n--- Items Used ---")
    for e in window_items:
        mins = int(e.timestamp // 60)
        secs = int(e.timestamp % 60)
        user = e.attacker_name.replace('npc_dota_hero_', '')
        item = e.inflictor_name
        print(f"  [{mins:02d}:{secs:02d}] {user} -> {item}")

    print("\n--- Key Modifiers ---")
    defensive_mods = [e for e in window_mods
                      if 'ethereal' in e.inflictor_name.lower()
                      or 'invulnerable' in e.inflictor_name.lower()
                      or 'outworld' in e.inflictor_name.lower()
                      or 'aeon' in e.inflictor_name.lower()]
    for e in defensive_mods:
        mins = int(e.timestamp // 60)
        secs = int(e.timestamp % 60)
        target = e.target_name.replace('npc_dota_hero_', '')
        mod = e.inflictor_name
        print(f"  [{mins:02d}:{secs:02d}] {mod} on {target}")

    # Check for Omnislash damage specifically
    omni_damage = [e for e in window_damage
                   if 'omni' in e.inflictor_name.lower() or 'swift' in e.inflictor_name.lower()]
    print(f"\n--- Omnislash Damage: {len(omni_damage)} hits ---")
    for e in omni_damage[:5]:
        mins = int(e.timestamp // 60)
        secs = int(e.timestamp % 60)
        target = e.target_name.replace('npc_dota_hero_', '')
        dmg = e.value
        print(f"  [{mins:02d}:{secs:02d}] Hit {target} for {dmg}")

# Usage - Analyze the 64:08 Omnislash vs Outworld Staff moment
# At 64:08, Ame Omnislashes Medusa, but she uses Outworld Staff at 64:09
# The ethereal form nullifies the Omnislash, leading to Juggernaut's death at 64:13
analyze_defensive_counter("match.dem", start_time=3848, end_time=3855)

Expected Output (TI 2025 Grand Finals - 64:08 fight):

Defensive Item Analysis
============================================================
Window: 64:08 - 64:15

--- Abilities Cast ---
  [64:08] juggernaut -> juggernaut_omni_slash on medusa
  [64:10] juggernaut -> juggernaut_blade_fury on dota_unknown
  [64:10] shadow_demon -> shadow_demon_demonic_cleanse on juggernaut
  [64:11] pangolier -> pangolier_gyroshell on dota_unknown

--- Items Used ---
  [64:09] medusa -> item_outworld_staff

--- Key Modifiers ---
  [64:09] modifier_item_outworld_staff on medusa

--- Omnislash Damage: 0 hits ---

Analysis: Ame's Omnislash targeted Medusa at 64:08, but skiter immediately used Outworld Staff at 64:09. The modifier_item_outworld_staff made Medusa ethereal (untargetable by physical attacks), causing Omnislash to deal zero damage and end prematurely. This forced Juggernaut into Blade Fury defensively, and he was killed at 64:13.

This demonstrates how: 1. Ability casts are tracked via CombatLogType.ABILITY 2. Defensive items are tracked via CombatLogType.ITEM 3. Buff effects are tracked via CombatLogType.MODIFIER_ADD 4. Missing damage indicates the counter was successful


Complete Analysis Pipeline

Combine multiple analyses into a single comprehensive report.

from python_manta import MantaParser, CombatLogType

def full_analysis(demo_path: str):
    parser = MantaParser()

    print("=" * 70)
    print("COMPREHENSIVE MATCH ANALYSIS")
    print("=" * 70)

    # 1. Basic Info
    header = parser.parse_header(demo_path)
    info = parser.get_parser_info(demo_path)

    print(f"\n[1] Match Information")
    print(f"    Build: {header.build_num}")
    print(f"    Duration: {info.tick} ticks")
    print(f"    Entities: {info.entity_count}")

    # 2. Draft
    game_info = parser.parse_game_info(demo_path)
    picks = [e for e in game_info.picks_bans if e.is_pick]
    bans = [e for e in game_info.picks_bans if not e.is_pick]

    print(f"\n[2] Draft")
    print(f"    Picks: {len(picks)}")
    print(f"    Bans: {len(bans)}")

    # 3. Kill Summary
    kills = parser.parse_game_events(demo_path, event_filter="dota_player_kill", max_events=500)
    print(f"\n[3] Kills")
    print(f"    Total: {len(kills.events)}")

    # 4. Combat Log Stats
    combat = parser.parse_combat_log(demo_path, heroes_only=True, max_entries=5000)
    damage_entries = [e for e in combat.entries if e.type == 0]
    death_entries = [e for e in combat.entries if e.type == 4]

    print(f"\n[4] Combat Log")
    print(f"    Damage events: {len(damage_entries)}")
    print(f"    Death events: {len(death_entries)}")

    # 5. Chat Activity
    chat = parser.parse_universal(demo_path, "CDOTAUserMsg_ChatMessage", max_messages=500)
    print(f"\n[5] Communication")
    print(f"    Chat messages: {chat.count}")

    # 6. Item Economy (via combat log)
    items = parser.parse_combat_log(demo_path, types=[CombatLogType.PURCHASE], max_entries=2000)
    print(f"\n[6] Economy")
    print(f"    Item purchases: {items.total_entries}")

    print("\n" + "=" * 70)
    print("Analysis complete")

# Usage
full_analysis("match.dem")