📚 API Reference¶
Demo¶
from boon import Demo
demo = Demo("match.dem")
A Deadlock demo file. The constructor reads the file header, file information, and first tick for match metadata, then preloads kills, damage, and abilities in one shared pass. Other datasets load on first access. Combat parsing errors are reported during construction when preloading is enabled.
Use Demo("match.dem", preload=False) for construction without combat preloading. This
keyword-only option disables combat preloading; datasets stay available on
first access or through demo.load(...). CLI commands use this mode to load
only the datasets they request.
Raises:
FileNotFoundError– If the file does not exist.InvalidDemoError– If the file is not a correct demo.DemoHeaderError– If necessary fields (build number, map name) are missing from the file header.DemoInfoError– If necessary fields (playback ticks, playback time) are missing from the file info.
Parameters:
path (
str) – Path to the.demfile.preload (
bool, keyword-only) – Load kills, damage, and abilities during construction. Default:True.
Methods¶
player_states(...)¶
Read player state names at selected ticks or every recorded tick.
Set data_version to a boon-data client version. Use steam_ids to select players.
See Player states for columns, mask sources, and missing values.
calculate_hero_stats(...)¶
Calculate hero stats at selected ticks. Select stats with HeroStat enum members
or their matching strings. See the full value table for names and units.
Set data_version and use steam_ids to select players.
Use mode="current" (default) for supported active effects, or mode="baseline"
for passive and permanent inputs. StatMode enum members also work. Results
include the selected mode.
See Calculate hero stats for supported stats, modes, equations, input sources, and limits.
See feature examples for complete calls.
calculate_ability_stats(...)¶
Calculate bonus percentages for each ability. Select stats with AbilityStat enum members
or their matching strings. See Percentage rules
for all accepted names.
Set ticks, data_version, and optional steam_ids.
Use mode="current" (default) for supported active effects, or mode="baseline"
for passive bonuses, permanent changes, and persistent imbues.
See Ability stats and imbues for targeting filters, equations, and limits.
imbues(...)¶
Read item imbue selections and their catalog effects.
Set ticks, data_version, and optional steam_ids.
See Ability stats and imbues for result tables and missing inputs.
get_item_purchases(...)¶
purchases = demo.get_item_purchases(data_version="6712")
Read item changes with catalog component links in upgraded_from_ability_ids.
Use data_version for an exact client version.
Without data_version, use the newest installed catalog. Download latest if none is installed.
See item-purchase columns and upgrade examples.
verify()¶
demo.verify() # -> bool
Make sure the file starts with the demo magic bytes. Correct magic bytes give True.
The constructor runs this check. An existing Demo instance always returns True.
available_datasets()¶
Demo.available_datasets() # -> list[str]
Return all dataset names. You can pass these names to load() or access them as properties.
The accepted strings are:
abilities, ability_upgrades, ability_ticks, active_modifiers, breakables, chat,
damage, flex_slots, item_purchases, kills, mid_boss, neutrals, objectives,
player_ticks, rift, sinners_sacrifice, stat_modifier_events, troopers, urn,
world_ticks, street_brawl_ticks, street_brawl_rounds
The two street_brawl_* names apply only to a Street Brawl demo. There is no Python
dataset enum. Calculated stats, state lists, imbues, and enemy minimap queries
use separate methods; their names are not accepted by load().
load()¶
demo.load("kills", "player_ticks", "world_ticks")
Load one or more datasets from the demo. Use available_datasets() to get accepted names.
Boon skips datasets that are already loaded. Event and entity datasets share one filtered pass. Player, world, and trooper snapshots share a parallel pass. A mixed request uses one pass for each group.
Combat and ability_ticks loads decode only needed properties. Requests that
also load active_modifiers or urn keep full entity fields. Later schema
changes also use full decoding.
Parameters:
*datasets (
str) – One or more dataset names to load.
Raises:
ValueError– If an unknown dataset name is provided.NotStreetBrawlError– If a street brawl dataset is requested on a non-street-brawl demo.
tick_to_seconds()¶
demo.tick_to_seconds(187554)
Convert a tick number to elapsed seconds. The result does not include paused time.
The method loads world_ticks on the first call to find pauses.
Parameters:
tick (
int) – The game tick to convert.
Returns: float – The elapsed time in seconds, without pauses.
tick_to_clock_time()¶
demo.tick_to_clock_time(187554)
Convert a tick number to a clock time string, such as "3:14" or "12:34".
The result does not include paused time. The method loads world_ticks on the first
call to find pauses.
Parameters:
tick (
int) – The game tick to convert.
Returns: str – A formatted clock time string.
tick_to_match_seconds()¶
demo.tick_to_match_seconds(187554)
Convert a demo tick to match-clock seconds, with pauses not included.
The method subtracts pregame_seconds from elapsed demo time.
The result is zero at game_start_tick and negative before the match starts.
Parameters:
tick (
int) – The game tick to convert.
Returns: float | None – Match-clock seconds, or None when pregame_seconds
is unavailable.
tick_to_match_clock()¶
demo.tick_to_match_clock(187554)
demo.tick_to_match_clock(500) # A pregame tick can show a negative clock.
The formatted counterpart to tick_to_match_seconds. Pre-game ticks read as a
negative clock.
Parameters:
tick (
int) – The game tick to convert.
Returns: str | None – A formatted match-clock string, or None when
pregame_seconds is unavailable.
snapshots()¶
demo.snapshots(every=64) # every 64 ticks
demo.snapshots(ticks=[29000, 30000]) # specific ticks
demo.snapshots(start_tick=29000, end_tick=30000) # a contiguous window
demo.snapshots("troopers", events="kills") # troopers at kill ticks
demo.snapshots(["player_ticks", "world_ticks"], seconds=1.0) # -> dict
demo.snapshots(ticks=187554, data_version="6712") # add calculated ammo
Read state for each tick only at selected ticks.
Up to four explicit ticks use direct seeks. Larger selections use parallel keyframe segments.
Barrier history and calculated ammo can use extra passes.
Sampling usually uses less memory than loading and filtering a full player_ticks frame.
datasets– which snapshot dataset(s):"player_ticks"(default),"world_ticks","troopers", or a list.ticks– a specific tick or list of ticks.every/seconds– a periodic stride (mutually exclusive).events– sample at thetickvalues in a named dataset, or a list. Accepts the dataset names above except"rift", which has notickcolumn. Sampling a table with a row at each tick can load that full table. Street Brawl names apply only to a Street Brawl demo.start_tick/end_tick– restrict to a contiguous window.
Return one DataFrame for one dataset. Return a dictionary for multiple datasets.
A window without another selector returns each tick in the window. A request
without a selector raises ValueError.
Player rows always include ammo_fraction from the primary weapon’s
m_flAmmoFrac. Missing, invalid, or ambiguous values are null. This field uses no catalog. Alternate weapons and selected spells do not replace the primary gun.
Set data_version to add these columns to player_ticks. Use a client version
from boon versions. Boon downloads a missing version. This option runs extra
stat and state queries at the selected ticks; include player_ticks in the request.
Column |
Type |
Description |
|---|---|---|
|
|
|
|
|
Finite capacity from the |
|
|
|
|
|
|
|
|
Missing inputs or calculation limits |
Partial capacity produces partial ammo. Missing capacity makes the two counts null.
A missing fraction makes ammo null, but can leave max_ammo available.
Unknown state bits make unlimited_ammo null unless INFINITE_CLIP is present.
Missing or duplicate Steam IDs leave calculated ammo and state values null.
Unlimited ammo does not change the counts to infinity. demo.player_ticks and demo.load("player_ticks") do not calculate these added fields.
enemy_minimap_ticks(*, ticks=None, every=None)¶
Read recorded enemy hero icons from each team’s minimap channel. Each row
identifies an observing team and an enemy icon at a completed demo tick.
This is shared team information, not an individual player’s line of sight.
The method reads CCitadelTeam.m_vecFOWEntities entries with class code 1.
Other map icons are excluded. No boon-data catalog is needed.
icons = demo.enemy_minimap_ticks(ticks=[187554])
sampled = demo.enemy_minimap_ticks(every=64)
ticks(Sequence[int] | None) – Exact demo ticks to return.every(int | None) – Positive interval between sampled ticks.
Omit both arguments to return every recorded tick. Pass only one selector;
ticks=[] returns an empty DataFrame with the same column types.
Each call scans the replay, including unsampled ticks, to preserve last-seen
history. Missing source entries do not produce rows.
Column |
Type |
Description |
|---|---|---|
|
|
Completed demo tick |
|
|
Team receiving the minimap channel: 2 or 3 |
|
|
Enemy team: 2 or 3 |
|
|
Hero ID from the current pawn |
|
|
Steam ID from a unique active controller bound to that pawn |
|
|
Resolved pawn entity index |
|
|
Pawn serial that distinguishes reuse of the index |
|
|
Decoded source entity index, kept even if unresolved |
|
|
Recorded icon visibility |
|
|
Current minimap X, from 0 to 255, only while shown |
|
|
Current minimap Y, from 0 to 255, only while shown |
|
|
Recorded hidden-tick field; its meaning and units are not inferred |
|
|
Most recent shown tick for this team and pawn lifetime |
|
|
Minimap X at that shown tick |
|
|
Minimap Y at that shown tick |
Minimap coordinates are not world coordinates. Current coordinates are null when the icon is hidden, visibility is unknown, or either coordinate is invalid. Last-seen values require a resolved pawn and an earlier or current shown position. They are kept separately for each observing team and pawn lifetime.
Missing pawns leave target identity fields null. Controller bindings use full
handles, including serials. target_steam_id is null if no valid binding or
Steam ID exists, current handles identify different pawns, or multiple
controllers claim the same pawn.
Use target_steam_id to join to demo.players; do not join null IDs.
Returns: polars.DataFrame with the columns above.
Raises: ValueError if both selectors are supplied or every <= 0.
See enemy minimap icons for a query example.
summary()¶
summary = demo.summary()
summary["snapshots"] # Player totals and state at each recorded snapshot
summary["gold_sources"] # Souls, kills, and damage by source at each snapshot
summary["last_hits"] # Final last-hit totals
summary["objectives"] # Recorded objective results
summary["damage"] # Source-to-target matrix: interval amounts and totals
summary["healing"] # Healing and regeneration from that matrix
Boon decodes the demo’s PostMatchDetails message. These tables contain recorded
post-match statistics. They do not estimate healing from changes in health.
The first call builds and caches all six tables.
snapshotshas one row for each player andsnapshot_time_s. It includessteam_id,hero_id,kills,deaths,assists,net_worth,denies,level,lane,creep_kills, andneutral_kills. Recorded damage totals areplayer_damage,creep_damage,neutral_damage,boss_damage,self_damage, andplayer_damage_taken. Healing totals areplayer_healing,teammate_healing, andself_healing.barrier_absorptionis damage stopped by barriers this player provided (recordedplayer_barriering). Healing has its own counters.damage_absorbedis damage stopped by barriers on this player. Other recorded counters aredamage_mitigated,damage_absorbed,absorption_provided,heal_prevented, andheal_lost. The added counters are null when missing from the message. Existing gold and orb columns stay available. Their prefixes areplayer_*,lane_creep_*,neutral_creep*,boss_*,treasure_*,denies_*,team_bonus_*,breakable_*,assassinate_*,trophy_collector_*,cultist_sacrifice_*,assists_*, andunknown_*. Theunknown_*columns refer to the Goose Egg source.gold_sourceshas one row for each recorded player, snapshot, and soul source. Columns aresnapshot_time_s,steam_id,hero_id,source_id,source_name,gold,gold_orbs,kills, anddamage. The counters are cumulative at that snapshot.goldandgold_orbskeep the two counters from the message. Usesnapshots.net_worthfor net worth. Source names are protobuf names, such ask_ePlayers,k_eLaneCreeps, andk_eAssists. Unknown IDs stay available asunknown_<id>. Missing source IDs, names, or counters are null. This table keeps sources that have no column insnapshots.last_hitscontainssteam_id,hero_id, and the final scoreboardlast_hitstotal.objectivescontainsteam_objective_id,team,destroyed_time_s,first_damage_time_s,creep_damage,player_damage, andplayer_spirit_damage. Missing times are null.damagecontains the full recorded matrix. Each row identifies adealer_steam_id,target_steam_id,source_name,stat_type, andsample_time_s.dealer_hero_idandtarget_hero_ididentify roster heroes. Non-player sources and targets have null hero IDs.damageis the amount for the interval frominterval_start_stosample_time_s.totalis the recorded cumulative amount atsample_time_s.stat_typeisdamage,healing,heal_prevented,mitigated,lethal, orregen. Unknown values useunknown_<id>.is_category=Truemarks broad source categories such asBullet,Ability,Melee,Misc, andUnknownAbility. These rows duplicate specific sources. Select categories or specific sources before aggregation. Do not add them together.healingselectshealingandregenrows from the matrix and does not include category duplicates. Columns areinterval_start_s,interval_end_s,healer_steam_id,healer_hero_id,target_steam_id,target_hero_id,source_name,stat_type,amount, andtotal.amountis the interval amount.totalis the recorded cumulative amount atinterval_end_s. Periods with no increase stay in the table.stat_typeidentifies healing or regeneration.source_namekeeps the recorded item, ability, modifier, or other source label.
Times use match-clock seconds. Player snapshots and matrix samples can use
different reporting periods. Use their recorded times; do not assume a fixed
interval or join them by row number. Sparse matrix histories start at later
samples. Boon does not add rows for unrecorded periods.
Hero IDs come from the match roster. Steam IDs identify players across hero changes.
Use Steam IDs to join these tables to demo.players.
Boon matches summary account IDs to recorded Steam IDs.
IDs use UInt64. Unmatched accounts and non-player sources have null Steam IDs.
Do not group or join null Steam IDs as if they were one player. Tables do not return player slots.
To get a total at one reporting period, select that time and sum total across
sources. Do not sum total across reporting periods. To combine periods,
sum the interval column (damage or amount).
import polars as pl
summary = demo.summary()
healing = summary["healing"]
matrix = summary["damage"]
period = matrix["sample_time_s"].max() # Select a recorded reporting period
steam_id = demo.players["steam_id"][0] # Select a player
# Healing and regeneration totals by player and type at each period.
healing_totals = healing.filter(pl.col("healer_steam_id").is_not_null()).group_by(
"interval_end_s", "healer_steam_id", "stat_type"
).agg(pl.col("total").sum())
# Healing or regeneration by source for one player at one period.
healing_sources = healing.filter(
(pl.col("interval_end_s") == period) & (pl.col("healer_steam_id") == steam_id)
).group_by("stat_type", "source_name").agg(pl.col("total").sum())
# Souls by source. This table uses the player snapshot schedule.
souls = summary["gold_sources"].filter(pl.col("steam_id") == steam_id)
# Damage dealt by type at one period: use only broad categories.
damage_types = matrix.filter(
(pl.col("sample_time_s") == period)
& (pl.col("stat_type") == "damage")
& pl.col("is_category")
& pl.col("dealer_steam_id").is_not_null()
).group_by("dealer_steam_id", "source_name").agg(pl.col("total").sum())
# Damage to/from players: use only specific sources to prevent duplicates.
player_damage = matrix.filter(
(pl.col("sample_time_s") == period)
& (pl.col("stat_type") == "damage")
& ~pl.col("is_category")
& pl.col("dealer_steam_id").is_not_null()
& pl.col("target_steam_id").is_not_null()
).group_by("dealer_steam_id", "target_steam_id").agg(pl.col("total").sum())
# Optional square matrix: rows deal damage, columns receive damage.
damage_matrix = player_damage.pivot(
on="target_steam_id", index="dealer_steam_id", values="total"
)
Returns: dict[str, polars.DataFrame] with the six tables above.
Raises: DemoMessageError if the post-match message is missing or invalid.
kill_participation()¶
demo.kill_participation() # whole match
demo.kill_participation(start_tick=0, end_tick=18000) # windowed
Each player’s kill participation is (kills + assists) / team_kills.
A kill credits a player as either the killer or an assister, never the two.
The ratio is in [0, 1], or null if the team has no kills.
This method calls boon.stats.kill_participation().
Optional start_tick / end_tick restrict the count to kills within that tick
window (the denominator is the team’s kills in the same window).
Returns: polars.DataFrame — one row for each player, sorted by team_num then
hero_id:
Column |
Type |
Description |
|---|---|---|
|
|
The player’s hero ID |
|
|
The player’s team number |
|
|
Kills credited to the player (in the window) |
|
|
Assists credited to the player (in the window) |
|
|
Total kills by the player’s team (in the window) |
|
|
|
time_dead()¶
demo.time_dead()
Time each player spent dead during regulation. A player is dead when
is_alive == False. The function counts only unpaused ticks up to game over.
The totals use the same time limits as regulation_ticks and
regulation_seconds. This method calls boon.stats.time_dead().
Returns: polars.DataFrame — one row for each player, sorted by team_num then
hero_id:
Column |
Type |
Description |
|---|---|---|
|
|
The player’s hero ID |
|
|
The player’s team number |
|
|
Non-paused regulation ticks spent dead |
|
|
|
|
|
|
Raises: ValueError — If the demo has no game-over event (regulation time,
and thus this metric, is undefined).
in_combat()¶
demo.in_combat()
Whether each player is in combat, for each tick. Each hit updates the pawn’s combat
window to last_damage_time + delay. The observed delay is approximately
0.5 seconds for trooper or denizen damage and 3 seconds for hero damage.
The function compares game time with player_ticks.in_combat_end_time.
This method calls boon.stats.in_combat().
Returns: polars.DataFrame — one row for each (tick, hero_id), so it joins
directly onto player_ticks, sorted by tick then hero_id:
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
The player’s hero ID |
|
|
Whether the player is in combat on that tick |
teamfights()¶
fights = demo.teamfights(gap_seconds=5.0, radius=1500.0, min_players=3)
print(fights)
Group damage between opposing heroes by time and location. A fight does not
depend on a kill. Events join the nearest active fight within radius; a gap of
gap_seconds ends a fight. Discard groups with fewer than min_players heroes.
These are detected groups, not official game events. The grouping gap uses
tick distance; the returned times do not include paused time.
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Maximum gap between nearby damage events, in seconds. |
|
|
Maximum distance from the current fight center, in Source map units. |
|
|
Minimum number of different heroes that deal or take damage. |
Returns one row for each fight, sorted by start_tick:
Columns |
Contents |
|---|---|
|
Sequential ID, starting at 1. |
|
First and last damage ticks. |
|
Elapsed times and duration, without pauses. |
|
Mean event position. |
|
Sorted hero IDs and their count. These IDs are not Steam IDs. |
|
Total damage between heroes. |
|
Kills attributed through each victim’s most recent damage event. |
The method loads damage, kills, and world ticks, and samples event positions.
It raises ValueError when the tick rate is zero.
Metadata Properties¶
path¶
demo.path # pathlib.Path
The path to the demo file.
total_ticks¶
demo.total_ticks # int
The total number of ticks in the demo.
total_seconds¶
demo.total_seconds # float
The total duration of the demo in seconds, covering the entire recording
(including pre-game and post-match time). For the duration of actual gameplay,
see regulation_seconds.
total_clock_time¶
demo.total_clock_time # str
The total duration of the demo as a formatted string (for example, "12:34"), covering
the entire recording. For gameplay duration, see regulation_clock_time.
build¶
demo.build # int
The build number of the game that recorded the demo.
map_name¶
demo.map_name # str
The name of the map the demo was recorded on.
match_id¶
demo.match_id # int | None
The match ID for this demo, or None if the demo does not carry one (for example a
partial capture or sandbox / custom content).
game_mode¶
demo.game_mode # int
The game mode ID for this demo (use game_mode_names() to resolve).
tick_rate¶
demo.tick_rate # int
The tick rate of the demo (ticks for each second).
winning_team_num¶
demo.winning_team_num # int | None
The team number of the winning team, or None if no game-over event was found.
Uses the cached k_EUserMsg_GameOver event, or scans for it if necessary.
game_over_tick¶
demo.game_over_tick # int | None
The tick when the game ended, or None if no game-over event was found.
Uses the cached k_EUserMsg_GameOver event, or scans for it if necessary.
game_start_tick¶
demo.game_start_tick # int | None
The tick at which the on-screen match clock reaches 0:00, when the barrier drops
and the game begins, ending the pre-game lobby. The counterpart to game_over_tick.
None if pregame_seconds is unavailable.
pregame_seconds¶
demo.pregame_seconds # float | None
The pregame duration in seconds, usually about 30 seconds. Boon reads the replicated match clock at game over to calculate the duration. It does not assume a fixed duration.
match clock = tick_to_seconds - pregame_seconds.
The result is None if the game-over event or match clock is missing.
It is also None if recording starts after pregame.
See tick_to_match_seconds and tick_to_match_clock.
regulation_ticks¶
demo.regulation_ticks # int | None
The number of match-clock ticks at the game-over event. Boon uses the
replicated HUD match clock, so this value does not include pregame, pauses, and
post-game time. When the recorded clock is missing or zero, Boon uses active demo
ticks as a fallback. The fallback does not include pauses and post-game time, but it can
include pregame recording time. Therefore, it might not match the HUD clock
exactly. None if no game-over event was found.
regulation_seconds¶
demo.regulation_seconds # float | None
The replicated HUD match-clock value at the game-over event, in seconds.
regulation_ticks is this value converted to ticks. None if no game-over
event was found.
regulation_clock_time¶
demo.regulation_clock_time # str | None
The match-clock value at game over as a formatted string (for example,
"32:45"). The format drops fractional seconds. None if no game-over event
was found.
DataFrame Properties¶
players¶
demo.players # polars.DataFrame
Player information. Computed from a snapshot at game over (or the final tick when no game-over event is available).
Column |
Type |
Description |
|---|---|---|
|
|
The player’s display name |
|
|
The player’s Steam ID |
|
|
The player’s hero ID (use |
|
|
Raw team number (use |
|
|
Original lane color (1=yellow, 3=green, 4=blue, 6=purple, 0=none; from the |
|
|
Packed competitive display rank; |
banned_heroes¶
demo.banned_heroes # polars.DataFrame
Heroes that the BannedHeroes user message identifies. The server can send
this message once before the match starts. GOTV recordings do not always
contain the message.
Column |
Type |
Description |
|---|---|---|
|
|
The banned hero’s ID (joins to |
|
|
Resolved hero name, or |
The message contains only hero IDs. It does not contain the team, banning player, or draft order. Boon can list unavailable heroes, but it cannot build the draft order.
An empty DataFrame means that the demo contains no ban data. It does not prove that the match had no bans. The demo gives no reason for missing ban data.
demo.banned_heroes
# shape: (2, 2)
# ┌─────────┬─────────────┐
# │ hero_id ┆ hero_name │
# │ --- ┆ --- │
# │ i64 ┆ str │
# ╞═════════╪═════════════╡
# │ 69 ┆ The Doorman │
# │ 63 ┆ Mina │
# └─────────┴─────────────┘
player_ticks¶
demo.player_ticks # polars.DataFrame
Recorded state for each player. Returns one row for each player at each tick.
Rows where the pawn is not found or hero_id == 0 are skipped.
Boon loads this dataset on first access.
Barrier values come from recorded packet changes. The first player snapshot query builds a cached barrier history. Direct seeks and full passes use this same history. Later queries reuse it. Barrier snapshots use no boon-data catalog.
Player fields (from the player pawn and controller):
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
Steam account ID |
|
|
Hero ID |
|
|
Recorded primary-gun ammo fraction |
|
|
Player X position in world (Hammer) units |
|
|
Player Y position in world (Hammer) units |
|
|
Player Z position in world (Hammer) units |
|
|
Camera pitch angle |
|
|
Camera yaw angle |
|
|
Camera roll angle |
|
|
In a regeneration zone |
|
|
In an item shop zone |
|
|
Time of death |
|
|
Time of last spawn |
|
|
Recorded respawn time |
|
|
Current health |
|
|
Maximum health |
|
|
Current barrier remaining; |
|
|
Life state value (use |
|
|
Current souls (currency) |
|
|
Total spent souls |
|
|
In-combat timer end |
|
|
In-combat last damage time |
|
|
In-combat timer start |
|
|
Damage dealt timer end |
|
|
Damage dealt last damage time |
|
|
Damage dealt timer start |
|
|
Damage taken timer end |
|
|
Damage taken last damage time |
|
|
Damage taken timer start |
|
|
Time revealed on minimap by NPC |
|
|
Hero build ID |
Controller fields (from CCitadelPlayerController):
Column |
Type |
Description |
|---|---|---|
|
|
Whether the player is alive |
|
|
Has rebirth |
|
|
Has rejuvenator |
|
|
Ultimate is trained |
|
|
Health regeneration rate |
|
|
Ultimate cooldown start time |
|
|
Ultimate cooldown end time |
|
|
Ability power net worth |
|
|
Gold net worth |
|
|
Total denies |
|
|
Total hero damage dealt |
|
|
Total hero healing |
|
|
Total objective damage |
|
|
Total self healing |
|
|
Current kill streak |
|
|
Total last hits |
|
|
Player level |
|
|
Total kills |
|
|
Total deaths |
|
|
Total assists |
|
|
Current recorded upgrade tokens |
|
|
Directly recorded boon count, if available |
upgrade_ids reads the controller’s m_PlayerDataGlobal.m_vecUpgrades vector
at that tick. It can include hero abilities. Use the matching boon-data catalog
to identify items. [] means a recorded empty vector; null means the vector or
an entry is missing or invalid. This column also appears in player snapshots.
boon_count reads a direct m_iBoonCount or m_nBoonCount field, at controller
level or under m_PlayerDataGlobal. It is null if the field is absent or invalid.
Boon does not calculate this value from the player’s level.
world_ticks¶
demo.world_ticks # polars.DataFrame
World state at every tick. Returns one row for each tick. Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
Whether the game is paused |
|
|
Time until next midboss spawn |
kills¶
demo.kills # polars.DataFrame
Hero kill events. Preloaded during construction unless preload=False.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the kill occurred |
|
|
The hero ID of the killed player |
|
|
The hero ID of the attacker |
|
|
List of hero IDs of players who assisted |
damage¶
demo.damage # polars.DataFrame
Damage events. Preloaded during construction unless preload=False.
Column |
Type |
Description |
|---|---|---|
|
|
The enclosing demo command tick |
|
|
The damage dealt |
|
|
The damage before mitigation |
|
|
Recorded absorption; null when missing |
|
|
Remaining shield after the hit |
|
|
Shield capacity |
|
|
Server tick recorded in the damage message |
|
|
The hero ID of the victim (0 if not a hero) |
|
|
The hero ID of the attacker (0 if not a hero) |
|
|
The victim’s health after damage |
|
|
The hitgroup that was hit (use |
|
|
Critical damage amount |
|
|
The attacker’s entity class ID |
|
|
The victim’s entity class ID |
|
|
Raw ability/weapon ID, or |
|
|
Raw Source |
|
|
Deadlock damage category; |
|
|
Raw Valve damage flags used for detailed classification |
|
|
Whether this is melee-typed damage ( |
|
|
|
tick comes from the demo command that contains the damage message. Use this
value with other Boon datasets and the tick-to-clock methods. server_tick
comes from the damage message itself. These counters can differ.
Do not assume a fixed offset between them. A null server_tick
means the message does not contain this field.
The shield fields are copied from the damage message. Observed hit sequences
suggest that victim_shield_new is the shield remaining after the hit and
victim_shield_max is the capacity of that shield pool. The shield fields are integers; damage_absorbed can have
a fractional value. These fields do not give a complete history of shield
gains and expiry. They can be missing even when absorption is positive. Null
means missing, not zero.
is_melee contains Valve’s melee damage category. The DFLAG_LIGHT_MELEE
and DFLAG_HEAVY_MELEE bits identify light and heavy hits.
These flags can also apply to ability damage. melee_type="other" means that
no flag or the two flags are set on melee-typed damage. Boon does not use
the ability name or damage value to classify melee damage.
flex_slots¶
demo.flex_slots # polars.DataFrame
Flex slot unlock events. Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the flex slot was unlocked |
|
|
The team number that unlocked the flex slot |
abilities¶
demo.abilities # polars.DataFrame
Important ability usage events. Preloaded during construction unless preload=False.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the ability was used |
|
|
The hero ID of the player |
|
|
The ability name |
ability_upgrades¶
demo.ability_upgrades # polars.DataFrame
Hero ability point spending events (skill tier upgrades). Emits a row each time a player upgrades one of their abilities. Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the upgrade occurred |
|
|
The hero ID of the player |
|
|
The raw MurmurHash2 ability ID (use |
|
|
Upgrade tier (1, 2, or 3) |
item_purchases¶
demo.item_purchases # polars.DataFrame
# Select the catalog version for this demo.
demo.get_item_purchases(data_version="6712")
Item and ability changes. Boon loads this dataset on first access. The property uses the newest installed boon-data version. If none is installed, Boon downloads the latest version. A missing selected version is also downloaded.
Column |
Type |
Description |
|---|---|---|
|
|
The tick of the transaction |
|
|
The player’s Steam ID, if recorded |
|
|
The hero ID of the player |
|
|
The raw MurmurHash2 item/ability ID (use |
|
|
Recorded change: |
|
|
IDs of matched component items |
Boon reads component links from abilities.json. It matches each purchase to
component sales by the same Steam ID at the same tick. No unique match gives an
empty list. Event order does not affect the match. These links are inferred;
change retains the recorded value. An item-path upgrade can have a purchased
row with component IDs and another row with change="sold".
Boon reads the client version from the header’s game directory to identify change value 4.
Client 6711 or later means "leveled_up"; earlier versions mean "failure".
A missing version means "unknown".
chat¶
demo.chat # polars.DataFrame
In-game chat messages. Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the message was sent |
|
|
The hero ID of the sender |
|
|
The message text |
|
|
|
objectives¶
demo.objectives # polars.DataFrame
Objective health state changes for walkers, barracks, shrines, patrons, and mid boss.
Records objectives with nonzero max_health. Emits their first observed state
and each change in health, max_health, or patron phase.
Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
|
|
|
The team that owns the objective |
|
|
Lane color (1=yellow, 3=green, 4=blue, 6=purple; 0 for patron/shrine/mid_boss) |
|
|
Current health |
|
|
Maximum health |
|
|
Patron phase — resolve with |
|
|
X position in world (Hammer) units |
|
|
Y position in world (Hammer) units |
|
|
Z position in world (Hammer) units |
|
|
Entity index; another entity can later use the same index |
|
|
Serial that distinguishes reuse of the index |
Use (entity_id, entity_serial) to identify one objective lifetime.
Filter objective_type == "mid_boss" to read boss health, including an observed
50% crossing. Use demo.mid_boss for lifecycle events.
mid_boss¶
demo.mid_boss # polars.DataFrame
Mid boss lifecycle events including spawn, kill, and rejuvenator buff tracking. Boon loads this dataset on first access.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
The team involved |
|
|
|
rift¶
demo.rift # polars.DataFrame
Rift lifecycle — one row for each Rift. Boon loads this dataset on first access.
The Rift is a periodic king-of-the-hill objective (Koth in the game files).
After the announcement, teams can capture it until it expires.
A successful capture grants enhanced troopers to the winning team in the
Rift’s lane.
Exactly one of capture_tick / expire_tick is set for each row. Only completed
Rifts appear. A Rift still active at the end of the demo is not included.
Column |
Type |
Description |
|---|---|---|
|
|
1-based Rift index in the match. Entity indices are recycled between Rifts, so this is the stable identifier |
|
|
Tick the spawner appeared, ahead of the Rift becoming contestable; |
|
|
Tick the Rift became contestable |
|
|
Tick a team captured it, or |
|
|
Tick it expired uncaptured, or |
|
|
Team that captured it; |
|
|
Lane the Rift spawned in ( |
|
|
X position of the cash-in in world (Hammer) units |
|
|
Y position of the cash-in in world (Hammer) units |
|
|
Z position of the cash-in in world (Hammer) units |
Boon reads the winner from the game rules’ scoring team.
The Rift entity’s m_iTeamNum records the last team to make capture progress.
That team can differ from the winner.
demo.rift.select(["rift_num", "capture_tick", "winning_team", "lane"])
# ┌──────────┬──────────────┬──────────────┬──────┐
# │ rift_num ┆ capture_tick ┆ winning_team ┆ lane │
# ╞══════════╪══════════════╪══════════════╪══════╡
# │ 1 ┆ 48871 ┆ 3 ┆ 1 │
# │ 2 ┆ 79884 ┆ 3 ┆ 6 │
# └──────────┴──────────────┴──────────────┴──────┘
troopers¶
demo.troopers # polars.DataFrame
Alive lane trooper state for each tick. Tracks CNPC_Trooper and CNPC_TrooperBoss entities.
Emits a row for every alive trooper at every tick.
Warning: This is a large dataset (~5M+ rows). Not loaded by default.
Access this property or call load("troopers") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
|
|
|
The trooper’s team |
|
|
Lane assignment (1, 4, or 6) |
|
|
Current health |
|
|
Maximum health |
|
|
X position in world (Hammer) units |
|
|
Y position in world (Hammer) units |
|
|
Z position in world (Hammer) units |
|
|
Entity index (stable for each trooper across ticks) |
neutrals¶
demo.neutrals # polars.DataFrame
Neutral creep state changes. Tracks CNPC_TrooperNeutral and CNPC_TrooperNeutralNodeMover.
Only emits a row when an alive neutral’s state changes (health, position), significantly
reducing data volume compared to tracking at each tick.
Not loaded by default. Access this property or call load("neutrals") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the state changed |
|
|
The neutral’s team |
|
|
Current health |
|
|
Maximum health |
|
|
X position in world (Hammer) units |
|
|
Y position in world (Hammer) units |
|
|
Z position in world (Hammer) units |
|
|
Entity index (stable for each neutral across ticks) |
breakables¶
demo.breakables # polars.DataFrame
Breakable map-prop destruction events. Boon tracks
CCitadel_BreakableProp entities. A broken prop leaves the PVS without a
health-zero update or a permanent delete. Boon keeps the leave as a candidate
until parsing is complete. It emits the event only when the same
(entity_id, entity_serial) identity does not become active again. Boon
ignores full-packet delete and create replacements.
The server does not report health zero or a dead lifestate before the leave. Therefore, the dataset does not contain these columns. Position and team use the last values before the final leave.
Boon does not load this dataset by default. Access the property or call load("breakables").
Subclass names come from the newest verified local boon-data misc.json,
with a download of latest if no installation exists. The recorded
subclass_id is kept even when no catalog entry matches it. To resolve
those IDs against a different version, use breakable_names(version="6698").
Catalog acquisition failures raise boon.data.DataError before parsing begins.
Column |
Type |
Description |
|---|---|---|
|
|
Tick when the prop was broken |
|
|
Always |
|
|
Entity slot index |
|
|
Serial distinguishing reuse of the slot |
|
|
Raw unsigned |
|
|
Resolved internal subclass name, or |
|
|
Last-known team |
|
|
Last-known X position in world (Hammer) units |
|
|
Last-known Y position in world (Hammer) units |
|
|
Last-known Z position in world (Hammer) units |
sinners_sacrifice¶
demo.sinners_sacrifice # polars.DataFrame
Sinner’s Sacrifice machine lifecycle and hit events. Boon tracks
CNPC_Neutral_SinnersSacrifice and
CNPC_Neutral_SinnersSacrifice_Hideout. Entity state supplies the identity,
health, and position. Each Damage message supplies the victim, incoming damage,
and attacker hero. Boon keeps a health decrease that has no matching message.
For this event, attacker_hero_id is 0.
health is the machine state at the end of the tick. Multiple hits in one
tick can have the same health value. A completed machine stays alive at one
health. An inactive machine can have no health fields. Therefore, the dataset does
not add a false death or lifestate event.
Boon does not load this dataset by default. Access the property or call
load("sinners_sacrifice").
Column |
Type |
Description |
|---|---|---|
|
|
Tick when the event occurred |
|
|
|
|
|
Entity slot index |
|
|
Serial distinguishing reuse of the slot |
|
|
Attacker hero ID, or |
|
|
Incoming damage, or |
|
|
Machine health at the end of this tick |
|
|
Maximum machine health |
|
|
Machine team |
|
|
X position in world (Hammer) units |
|
|
Y position in world (Hammer) units |
|
|
Z position in world (Hammer) units |
stat_modifier_events¶
demo.stat_modifier_events # polars.DataFrame
Permanent stat bonus changes from urn and breakable pickups. Boon emits a row when a stat total changes.
Types come from the newest installed boon-data catalog. If no catalog is installed, Boon downloads the latest version. Use a catalog for the replay’s client version. Numeric enum IDs can change between versions.
Not loaded by default. Access this property or call load("stat_modifier_events") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the stat changed |
|
|
The player’s hero ID |
|
|
|
|
|
The signed change from this event |
active_modifiers¶
demo.active_modifiers # polars.DataFrame
Effective buff and debuff modifiers on players. Boon tracks applied,
changed, and removed events for each Source 2 modifier serial. A
changed event records a change to stacks, duration, or application time.
Finite durations do not include paused time. Boon ends these effects when their timers
end, even if the replay retains their rows in the modifier table.
One ability can create multiple modifier instances. The number of rows is not
the stack count. Use stacks. A serial identifies an active entry. The
game can reuse the serial after removal. Use the apply, change, and remove
events to identify each cycle. Do not use the serial as a globally unique
lifecycle ID.
Not loaded by default. Access this property or call load("active_modifiers") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the modifier event occurred |
|
|
The affected player’s hero ID |
|
|
|
|
|
Source 2 runtime identifier; unique while live, reusable after removal |
|
|
Raw modifier subclass hash ID (use |
|
|
Raw ability subclass hash ID (use |
|
|
Current modifier duration in seconds ( |
|
|
Hero ID of the caster |
|
|
Number of stacks |
ability_ticks¶
demo.ability_ticks # polars.DataFrame
Ability cooldown and charge state over time. The dataset is change-only.
Boon emits a row only when the cooldown or charge state changes. One entity
exists for each ability that a player owns. These entities include movement
abilities such as jump, dash, and slide. Use slot to remove these abilities
from the result.
Not loaded by default. Access this property or call load("ability_ticks") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick of the state change |
|
|
The owning player’s hero ID |
|
|
Ability subclass hash (use |
|
|
Ability slot ( |
|
|
Game time the cooldown started |
|
|
Game time the ability is available again |
|
|
Charges available |
|
|
Game time the regenerating charge started |
|
|
Game time the regenerating charge completes |
urn¶
demo.urn # polars.DataFrame
Urn lifecycle events use the shared effective modifier tracker and boon-data IDs.
The carrier modifier gives pickup and drop events. A return requires carrier loss,
a completed channel, and delivery-point closure on the same tick.
Entry into the delivery area does not give a return event.
Boon reads delivery-point changes from CCitadelIdolReturnTrigger entities.
Missing return evidence gives a drop event. See Urn limits.
Not loaded by default. Access this property or call load("urn") explicitly.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick when the event occurred |
|
|
|
|
|
The hero involved (0 for delivery events) |
|
|
Team of the delivery point (0 for modifier events) |
|
|
Delivery point or pawn X position in world (Hammer) units |
|
|
Delivery point or pawn Y position in world (Hammer) units |
|
|
Delivery point or pawn Z position in world (Hammer) units |
street_brawl_ticks¶
demo.street_brawl_ticks # polars.DataFrame
Street Brawl state for each tick. Only available for street brawl demos (game_mode=4). Boon loads this dataset on first access.
Raises: NotStreetBrawlError if the demo is not a street brawl game.
Column |
Type |
Description |
|---|---|---|
|
|
The game tick |
|
|
Current round number |
|
|
Street brawl state enum value |
|
|
The Hidden King (old name: Amber Hand) score |
|
|
The Archmother (old name: Sapphire Flame) score |
|
|
Last buy phase countdown value |
|
|
Time of next state transition |
|
|
Time the current state started |
|
|
Total non-combat time elapsed |
street_brawl_rounds¶
demo.street_brawl_rounds # polars.DataFrame
Street brawl round scoring events. Only available for street brawl demos (game_mode=4). Boon loads this dataset on first access.
Raises: NotStreetBrawlError if the demo is not a street brawl game.
Column |
Type |
Description |
|---|---|---|
|
|
Sequential round number (1-indexed) |
|
|
The game tick when the round ended |
|
|
The team that scored |
|
|
The Hidden King (old name: Amber Hand) cumulative score |
|
|
The Archmother (old name: Sapphire Flame) cumulative score |
Name Lookup Functions¶
Module-level functions resolve IDs to names without parsing a demo.
hero_names, ability_names, ability_display_names, modifier_names, and breakable_names
read boon-data catalogs. Each accepts an optional version client-version string.
Without it, Boon selects the newest verified local installation. If none exists,
it downloads the latest release. Explicit missing versions are
also downloaded. Importing Boon and parsing raw IDs do not download data.
Installed data works offline. Failed acquisition or invalid catalog contents
raise boon.data.DataError. Use boon versions and boon get VERSION to manage
installations; use boon get VERSION --force to repair corrupt files.
See Game data downloads for cache configuration and verification.
from boon import ability_names, hero_names
hero_names(version="6698")
ability_names(version="6698")
hero_names()¶
from boon import hero_names
hero_names() # -> dict[int, str]
Return hero IDs mapped to localized names. Unlocalized heroes use their internal names.
Returns: dict[int, str] – Hero ID to hero name mapping (for example, {1: "Infernus", 2: "Seven", ...}).
team_names()¶
from boon import team_names
team_names() # -> dict[int, str]
Return a mapping of team number to team name.
Returns: dict[int, str] – {1: "Spectator", 2: "Hidden King", 3: "Archmother"}.
ability_names()¶
from boon import ability_names
ability_names() # -> dict[int, str]
Return a mapping of MurmurHash2 ability ID to ability name.
The returned values are stable internal VData names such as
upgrade_quick_silver, not localized in-game labels.
Returns: dict[int, str] – Ability hash to internal-name mapping.
ability_display_names()¶
from boon import ability_display_names
ability_display_names() # -> dict[str, str]
Return exact current internal ability/item names mapped to their English
in-game display names. This includes ability_*, upgrade_*, and
citadel_ability_* naming schemes. For example,
upgrade_quick_silver maps to "Quicksilver Reload", while
ability_unicorn_luminousstrike maps to "Radiant Daggers".
citadel_ability_hook maps to "Grapple Arm".
Hidden, test, retired, and other unlocalized internal entries are not included; Boon never invents a display name by stripping prefixes or title-casing.
Returns: dict[str, str] – Internal ability/item name to English display
name mapping.
breakable_names()¶
from boon import breakable_names
breakable_names().get(3986897915) # -> "citadel_breakable_prop_wooden_crate"
breakable_names(version="6698") # explicitly select a client version
Returns {subclass_id: internal_name} from misc.json, restricted to records
whose definition._class is citadel_breakable_prop. Other misc definitions
such as pickups are not included. IDs missing from the catalog are not in the map;
demo.breakables.subclass_name uses "BREAKABLE_NOT_FOUND" for them.
game_mode_names()¶
from boon import game_mode_names
game_mode_names() # -> dict[int, str]
Return a mapping of game mode ID to game mode name.
Returns: dict[int, str] – Game mode ID to name mapping (for example, {1: "6v6", 4: "street_brawl"}).
modifier_names()¶
from boon import modifier_names
modifier_names() # -> dict[int, str]
Return unqualified and owner-qualified modifier IDs mapped to their corresponding
names. For example, an owner-qualified token resolves to
ability_afterburn/modifier_afterburn_dot. Repeated records with the same ID and
name share one entry. Conflicting names raise DataError; use the raw boon-data
record indexes to get all candidate definitions and their effects.
Returns: dict[int, str] – Modifier hash to name mapping.
patron_phase_names()¶
from boon import patron_phase_names
patron_phase_names() # -> dict[int, str]
Return a mapping of patron phase ID to phase name. Phases are the values of
CNPC_Boss_Tier3.m_ePhase: 0=normal (shielded), 1=final (killable),
2=transforming (vulnerable). Non-patron objectives report 0 by default.
Returns: dict[int, str] – Patron phase ID to name mapping (for example, {0: "normal", 1: "final", 2: "transforming"}).
hitgroup_names()¶
from boon import hitgroup_names
hitgroup_names() # -> dict[int, str]
Return a mapping of hit group ID to hit group name, for resolving the
hitgroup_id column on the damage frame. Values come from Source 2’s HitGroup_t enum.
The map includes 0=generic, 1=head, 2=chest, and 3=stomach.
Limb values are 4=left_arm, 5=right_arm, 6=left_leg, and 7=right_leg.
Other values include 8=neck, 10=gear, 11=special, 19=head_no_resist,
and -1=invalid. Values 12–18 identify tier-2 and drone boss weakpoints. The HITGROUP_COUNT sentinel is not included.
Returns: dict[int, str] – Hit group ID to name mapping.
lifestate_names()¶
from boon import lifestate_names
lifestate_names() # -> dict[int, str]
Return a mapping of life state ID to life state name, for resolving the
lifestate column on player_ticks. Values are Source 2’s LifeState_t enum.
Returns: dict[int, str] – Life state ID to name mapping ({0: "alive", 1: "dying", 2: "dead", 3: "respawnable", 4: "respawning"}).
Stats (boon.stats)¶
These functions take a Demo and return a Polars DataFrame.
Each function also has a Demo method with the same parameters and result columns.
Function |
Demo method |
|---|---|
|
|
|
|
|
|
|
from boon import stats
stats.kill_participation(demo, start_tick=0, end_tick=18000)
stats.time_dead(demo)
stats.in_combat(demo)
stats.teamfights(demo, gap_seconds=5.0, radius=1500.0, min_players=3)
Exceptions¶
InvalidDemoError¶
from boon import InvalidDemoError
Raised when a demo file is invalid or cannot be parsed (bad magic bytes, corrupted data).
DemoHeaderError¶
from boon import DemoHeaderError
Raised when necessary fields are missing from the demo file header (build number, map name).
DemoInfoError¶
from boon import DemoInfoError
Raised when necessary fields are missing from the demo file info (playback ticks, playback time).
DemoMessageError¶
from boon import DemoMessageError
Raised when a requested protobuf message is missing or cannot be decoded, such as post-match details or a malformed kill/damage event.
NotStreetBrawlError¶
from boon import NotStreetBrawlError
Raised when accessing street brawl datasets (street_brawl_ticks, street_brawl_rounds) on a demo that is not a street brawl game (game_mode != 4).
CalculationError¶
A stat query cannot calculate a requested value from the selected inputs.
With strict=False, unsupported inputs give unresolved rows with null values.
Invalid selections and missing ticks still cause errors.
data.DataError¶
A catalog download, metadata check, or file-integrity check failed.
Read the error message for repair commands.
See Game data downloads for installation and boon get VERSION --force.
Modifier events¶
demo.active_modifiers returns applied, changed, and removed events.
It uses the shared lifetime rules for durations, aura range, observed catalog
states, and deletion of an observed source ability for intrinsic effects.
A table snapshot is not a new event. New applications can affect multiple players
on the same tick. Missing evidence does not end an effect.
The query uses local game data and downloads the latest data if none is installed.
Ability history identity¶
ability_ticks records changes to ability cooldowns, charges, owners, IDs, and
slots. A new entity application emits its first row even when its cooldown and
charge values match the previous occupant. Owner links require a matching full
entity handle. No new columns are required.