Repository navigation
Releases: darkharasho/axilog
Release list
v1.16.0
Added
blocks.focus.by_entity[].casts_by_skill: enemy casts aimed at each
squad player, split by skill. The block already counted casts per player
(casts_drawn) and per skill across the whole squad (skills[]), but not
the two together, so a consumer could not say "Meteor Shower was cast at
this player 14 times". Each row is{ skill, casts }, ascending by skill
id, summing tocasts_drawnexactly; minion-targeted casts stay out, as
they do everywhere else in the block. Additive, omitted when empty, and
absent on pre-2026-05 logs along with the rest of the block. Requested for
AxiBridge's per-player incoming skill breakdown.
Install
# Node
npm install @axiapps/axilog@1.16.0
# Python
pip install axilog==1.16.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.15.0
Added
-
The instant-cast finder catalog reads the lambda checkers it used to skip:
571 → 603 of GW2EI's 649 finders. Reported in Discord: guardian's
"Advance!" never appeared in AxiBridge's APM breakdown. It is an instant
cast, so arcdps emits no activation event for it, and GW2EI synthesizes one
— but every guardian shout spawns the SAMEGuardianShouteffect, so the
only thing telling "Advance!" apart from "Stand Your Ground!" is a
.UsingChecker(lambda)identifying it by self-applied Aegis of 20s to 40s.
scripts/gen_instant_cast_catalog.pyskipped any finder carrying such a
lambda — correctly, since dropping the condition would WIDEN the finder —
so no finder for skill 9084 existed at all and the cast reached neither
rotationnorskillMap.A large minority of those lambdas are not arbitrary closures: they are
conjunctions of calls to NAMEDCombatData/AgentItemhelpers. Five
Checkvariants now express them —RelatedHit(HasRelatedHit),
RelatedEffectDst(HasRelatedEffectDst),RelatedBuff
(HasGainedBuff/HasLostBuff/HasLostBuffStack),SelfBuffApply(the
GetBuffApplyDataByIDBySrc+FindRelatedEventsduration-window and
stack-count shapes) andSpecies(IsSpecies) — and the generator
decomposes a lambda body into them, falling back to a skip when any
conjunct does not match. Two existing checks absorbed more:IsCasting
isCheck::NoAnimatedCast, and anAppliedDurationcomparison on the
triggering event isCheck::Duration.The matchers are ANCHORED on purpose. A lambda that only partly matches a
known shape still skips, because a partial match that silently dropped the
rest of the condition is the exact failure mode the generator exists to
prevent. 46 finders remain skipped and each now names WHICH unmodelled
thing it reads instead of all 70 reporting "arbitrary predicate": 16 need
the weaver dual-attunement history, 7 key on a GW2EI-synthesized negative
species id thatRevenantHelper.ProcessGadgetsassigns in a pre-pass, 8
need a subclass this project does not decode, and only 3 are genuinely
arbitrary.Measured on
fixtures/wvw-small.anon.zevtcagainst a GW2EI CLI oracle
built from upstreamcc820b66:- 21 new casts across 9 players — "Advance!" ×10, "Stand Your Ground!" ×8,
Distortion ×2, Lesser Signet of Stone ×1 — and every one matches EI's
ownrotationexactly, per player. - Instant-cast recovery 336/369 → 357/369 (91.1% → 96.7%).
- The casts axilog emits that EI does not stayed at 19, unchanged — no
over-firing was introduced. The casts EI emits that axilog does not fell
52 → 31. - Purely additive in the native document: no key added or removed anywhere,
no block butblocks.rotation.by_entity.*.castschanged length, and no
pre-existing cast row was removed or altered.
AxiBridge needs only a dependency bump; nothing on the application side
filters the APM used-skills list, so the missing rows were missing data. - 21 new casts across 9 players — "Advance!" ×10, "Stand Your Ground!" ×8,
Changed
- The replay-enabled HTML size gate moved 600KB → 620KB. The four
newly-detected skills each gain askill_maprow (454 bytes) and the
fixture was sitting 392 bytes under the limit — 0.07% headroom, against a
guard whose neighbours are documented as holding ~19KB.rotationitself
is not embedded in that fixture.
Install
# Node
npm install @axiapps/axilog@1.15.0
# Python
pip install axilog==1.15.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.14.1
Fixed
-
One agent address is now one player. arcdps can write the same agent
twice in the agent table — once with the real
character \0 account \0 subgrouptriple, once anonymised to the player's
WvW rank title with a blank account and no subgroup — andresolve()makes
onePlayerper raw agent row, so both reachedwvw::dedupe_players. The
account and instid keys there could never merge them: the named row keys by
account, the anonymous one by instid, and the two maps never meet.Every data block (replay track, boons, damage, defenses, hit_stats,
rotation, focus, …) landed on the anonymous row, so the named squad member
read zero for everything and carried no position track at all. In AxiBridge
that dropped the player from the combat replay outright — including, on the
log that surfaced this, the squad commander, whose tag and range rings
therefore never rendered.dedupe_playersnow merges rows sharing anagent_addrfirst. Unlike the
account and instid keys that follow it, this is not an inference: an agent
address identifies an agent exactly. The merge promotes identity per field
and independently of agent-table order — a known account, a real character
name, a real subgroup, squad membership and a commander tag each beat their
absent counterpart — and unionsagent_addrsrather than concatenating, so a
relogged player still reads as one person.Measured over 400 real WvW logs: 6 affected, 11 squad members losing their
track entirely, 1 of them a commander. All 6 now report zero duplicate-addr
groups and zero squad members without a track. No committed fixture changes,
so every digest baseline is unmoved.
Install
# Node
npm install @axiapps/axilog@1.14.1
# Python
pip install axilog==1.14.1
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.14.0
Added
variant_labeloncatalogs.skills[skill_id]. A readable label for
skill ids that share aname, so a consumer can render
${name} (${variant_label})instead of indistinguishable rows: the
adrenaline tier of a warrior burst ("Adrenaline 3"), a Berserker primal
burst ("Primal Burst"), or an elementalist attunement variant ("Fire").
Comes from a curated table generated from the GW2 API and is set for every
id that table covers, in every report, so an id's label never varies
between logs. Never a raw skill id: other same-name ids carry no label and
should be merged on(name, variant_label). Purely additive to the wire
document; both SDK stubs and the key-set golden updated to match.
Install
# Node
npm install @axiapps/axilog@1.14.0
# Python
pip install axilog==1.14.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.13.1
Fixed
-
playerTotalnow reaches the EI-shaped document. v1.13.0 added
player_totalto everyblocks.damage.by_entity[].by_skill_taken[]row,
but only to the native container. The consumer it was built for reads
ei-json exclusively throughparseFileEi, so the field it needed was
unreachable from the surface it reads.dist_rows_ei_jsonnow emits it as
playerTotalontotalDamageTakenrows.The emit is gated on the field's own
Optionrather than on the call
site, so it scopes itself to the one grouping that measures the split:
outgoing and per-target rows carryNoneand keep their previous key set
byte for byte.playerTotalis an axilog extension — GW2EI has no such
key — and it refinestotalDamagerather than partitioning it, so sums
over the distribution are unchanged. Absent still means "not measured",
never "no player damage".Purely additive to the wire document, verified by leaf-diffing a build of
the parent commit against this one: 1,391 leaves added, 0 removed, 0
changed, every one of them aplayerTotalunder
players[].totalDamageTaken.
Install
# Node
npm install @axiapps/axilog@1.13.1
# Python
pip install axilog==1.13.1
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.13.0
Added
-
player_totalon everyblocks.damage.by_entity[].by_skill_taken[]row.
The portion of that skill's incoming damage dealt by a player or a player's
minion; siege, guards, NPCs and unattributable rows are the remainder. This
lets a consumer present incoming damage the way the arcdps in-game filters
do — separating what other players did to you from what the environment
did — without shipping a second distribution.Present on
by_skill_takenrows only. Onby_skill/per_targetit is
absent because those rows are player-sourced by construction, and on enemy
rows because that pass does not classify sources. Absent means "not
measured for this grouping", never "no player damage."player_total <= totalholds per row, and the split is a refinement oftotalrather than
a filter on it, sosum(by_skill_taken[*].total) == takenis unchanged.Strictly additive, verified as such: a structural diff of the full native
document against the previous baseline showed 0 removed keys, 0 changed
values and 1,391 added keys, all of them this field. The legacy document is
byte-identical — the field is#[serde(skip)]onSkillEntryOut, which
carries it only to reach the 1.0 block builder.
Fixed
-
InstidRegistry::buildno longer registers agent addr0, which could
shadow an instid's real owner. arcdps emits damage rows with a real
src_instidbutsrc_agent == 0when the source is outside the POV
client's update bubble and the target is a squad member near its edge: the
client is told which instid hit but holds no agent record to name it.
Registering that null as an ownership change maderesolve_atanswer
Some(0)— an addr in no agent table — for the rest of the window.Measured over 400 real WvW captures: 222,892 null registrations, shadowing
1,827 of 11,480,017 master-instid lookups across 398 of the 400 logs.
Squad-facing impact was nonetheless exactly zero (0 rows, 0 damage, 0
healing), and structurally so — null registrations only arise from
out-of-bubble agents, which are never squad members, and every consumer
already drops non-squad resolutions. No shipped metric changes, and the
native/legacy documents are unmoved apart from the additive field above.It is fixed here because it is a precondition for the source split, which
is the first consumer that wants those out-of-bubble agents. Resolving
theirsrc_instid— time-windowed, since ~16% of the affected rows use an
instid bound to more than one agent in the same log — recovers 94.2% of
that damage to a player and 3.0% to a non-player, dropping the residual
unattributable share of incoming direct damage from 3.60% to 0.10%.
Confirmed with arcdps upstream that no arcdps-side change is coming or
needed: it streams, and would have to buffer the orphaned rows and
retro-patch them, whereas a completed log can simply look the binding up.
Install
# Node
npm install @axiapps/axilog@1.13.0
# Python
pip install axilog==1.13.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.12.0
Changed
-
blocks.focusis nowunsupported(and omitted) on pre-2026-05 logs,
instead ofemptywith a zeroed roster. The enemy cast-start census
only exists in the post-reworkCBTS_ANIMATIONSTARTstatechange encoding.
Measured over 4,143 real WvW logs: the 2,334 pre-rework ones carry zero
enemy cast rows in either era's encoding, while carrying 7.34M enemy→squad
strike rows in the same files — so there is nothing an older decoding path
could recover. Until now those logs shipped a full zeroedby_entity
roster undercoverage.focus: "empty", which a consumer reads as "the
squad drew no enemy attention" when the truth is "this log cannot answer
the question". That was 56% of the corpus, and it included this repo's own
wvw-small.anon.zevtcfixture (build20260114) — so every schema test of
this block was asserting on a structurally empty one.coverage.focusnow reads"unsupported"and the block is omitted, the
same ruleblocks.healingalready follows for a log recorded without the
healing extension. Consumers must treat a missingblocks.focusas "not
measurable here", never as "nobody was targeted".
Added
-
total_minion_casts/casts_drawn_minionsonblocks.focus. Enemy
casts aimed at squad pets, clones, phantasms, spirit weapons, turrets and
gyros survive the same dst-driven filter and carry their owner's instid in
dst_master_instid. They are 8.3% of the census on the 4,143-log corpus
and were previously discarded, which quietly undercounted pet professions.They are attributed to the owner on their own axis and deliberately kept
out ofcasts_drawn,total_casts,focus_indexandpre_down_casts:
folding them into the index measurably weakens its commander separation
on every holdout slice (2.70→2.36, 2.82→2.45, 2.81→2.48 across three
disjoint slices). A cast aimed at your pet is enemy effort spent on your
account, but it is not the enemy shooting you. -
examples/enemy_cast_census.rs— the probe behind the numbers above.
Separates enemy-filtered casters from agents arcdps tracked at full
fidelity (viaCBTS_BUFFINITIAL, which the filter never emits), which is
what makes the "exactly zeroANIMATIONSTOP" and "zero nulldst" results
exact rather than approximate. -
docs/enemy-cast-census.md— the write-up of the filter's behaviour,
intended for the arcdps author.
Install
# Node
npm install @axiapps/axilog@1.12.0
# Python
pip install axilog==1.12.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.11.0
Added
-
blocks.focus— enemy attention per squad player. arcdps's enemy-event
filter is DST-driven forCBTS_ANIMATIONSTART: an enemy cast-start row
survives into the log exactly when itsdst_agentis a squad member. The
surviving rows are therefore a census of enemy activity aimed at the
squad, not a sample of it, and this block reports it.focus_indexis a player's share of those aimed casts divided by an even
1/squad_sizeshare —1.0is exactly average attention,3.0is three
times what an evenly-targeted squad member draws. Also per player:
casts_drawn,downs, andpre_down_casts(casts aimed at them in the
3s before a down, the window carried on the wire aspre_down_window_ms).
skills[]carries per-skillcasts_at_squad/hits/damage_total
against a whole-logmean_strike_damage.Validated on ~1,400 real WvW logs held out from the corpus the pass was
developed on, across three disjoint slices: the commander reads 1.92× /
1.86× / 1.47× against a median other squad member's 0.56× / 0.53× /
0.64× — roughly 3× the attention, consistently. Pre-down lift 1.58× /
1.52× / 1.37×.A damage-weighted variant (per-skill empirical weights, empirical-Bayes
shrinkage toward a 60-skill corpus prior) was built and then rejected:
on the same holdout it never beat plain cast counting (1.87 vs 1.92, 1.86
vs 1.86, 1.40 vs 1.47).analysis::focus' module doc records the full
table so it is not re-derived. The reason it could not work is worth
carrying: the median enemy skill connects three times in one WvW log,
which is whyskills[]ships(hits, damage_total)pairs — poolable
across logs — rather than a per-log mean.Always-on (one linear scan of the event list, like
squad_buffs), so no
flag gates it. Counts casts from enemy players only — an NPC does not
choose a target the way a player does — so on a PvE log every row is zero
andcoverage.focusreadsempty.Two limits are structural, both from the same filter: no enemy
CBTS_ANIMATIONSTOProw survives (no durations, no interrupts), and an
untargeted ground AoE emits no row at all.Additive under the 1.x rules. The native-JSON baseline was re-digested
after a recursive leaf diff against the previous build showed exactly two
added subtrees and zero other changes.
Fixed
- The HTML report's embedded assets are pinned to LF.
report.css,
report.jsandskeleton.htmlare baked into the report verbatim with
include_str!, so a Windows checkout converting them to CRLF added one
byte per line — 2,009 lines — and pushed the replay-enabled fixture render
from 599,608 to 601,617 bytes, pastgolden_html.rs' 600KB gate. That gate
had been the Windows job's failure for four consecutive releases. The
budget was never the problem and is unchanged; the checkout was. A
.gitattributesrule now holds the whole asset directory at LF, which also
restores the cross-platform byte-identity the crate's determinism tests
otherwise imply. - The
parseFileEibyte-identity baseline is current again. It had gone
stale for the third time, by +736 code units. Attributed by leaf-diffing a
worktree build at the recorded anchor against HEAD: exactly four keys
added, zero removed, zero changed — theskillMapentries for arcdps's
generic control effect ids (23294/23295/23296/23299) that v1.10.0's CC
attribution lane began naming. Nothing axibridge already reads moved. It
stayed hidden by the same mechanism as last time: main's Linux job died at
the version-sync gate on every release from v1.8.0 through v1.10.2 and
never reached the Node SDK tests.
Install
# Node
npm install @axiapps/axilog@1.11.0
# Python
pip install axilog==1.11.0
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.10.2
Fixed
-
Non-squad friendly players no longer collapse by elite spec.
wvw::dedupe_playerskeyed onaccount, falling back tocharacter,
and it runs onenc.players— which is the whole friendly roster, the
recorder's squad and every non-squad friendly on the map. Squad members
always reveal an account so they were unaffected, but arcdps anonymises
non-squad friendlies exactly like enemies: blank account, with
charactersubstituted by the profession/elite-spec label ("Druid",
"Scrapper"). The fallback therefore merged every pug sharing a spec into
one person, silently capping the ally roster at distinct specs present.The enemy side already had the fix —
dedupe_enemy_playersregroups on
instid, mirroring GW2EI's non-squadGroupBy(x => x.InstID), and its own
doc comment describes this exact hazard — the rule was just never routed
over the friendly non-squad agents.dedupe_playersnow keys onaccountwhere known (still the primary
key, so a relogged squadmate collapses regardless of instid) and falls
back to instid where it is blank, never tocharacter. A
blank-account row with no resolvable instid stays distinct. Both dedupes
now share oneInstidRegistryinstead of building two.Verified against a GW2EI 3.21 CLI oracle over all 8 reference captures:
the non-squad friendly count now matches EI's non-squad player count on
8/8. It was 38 → 17 on20260117-175120and 15 → 10 on
20260128-190427; the other six were already exact only because no two
pugs there happened to share a spec, which is why no fixture caught
this. Squad and enemy counts are unchanged on all 8.
Install
# Node
npm install @axiapps/axilog@1.10.2
# Python
pip install axilog==1.10.2
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missingv1.10.1
Fixed
-
blocks.replayno longer interpolates across gaps arcdps never
claimed.analysis::replaylerped between whichever two
CBTS_POSITIONevents bracketed a grid point, with no bound. But arcdps
polls position every 300ms and emits nothing when the value is
unchanged (confirmed by its author, deltaconnected, 2026-08-30), so a
gap proves the agent was stationary and that the move happened inside
the last poll window before the event that reported it — the exact
opposite of a drift. The naive lerp was therefore wrong for every gap,
and catastrophic for the long ones: arcdps stops reporting entirely
while a player is dead / loading / waypointing and resumes at the
respawn point, so the track fabricated a smooth march across the map at
impossible speed. On20260830-180925.zevtcone player was interpolated
56,668 inches while dead, at 23x run speed, dragging their death
circle ~19,000 inches off the death spot — which is how this surfaced,
reported downstream as "the downed/dead circles seem delayed".hold_until_change_windownow inserts a synthetic sample carrying the
earlier position atb.t - 300for any wider gap: the agent holds still
for the part arcdps proved it was still, then covers the move across the
window it could have happened in. Deliberately a held line rather than a
teleport-and-vanish — arcdps's author asked for the relocation to stay
visible ("perhaps theres a way to keep the line for it while doing so,
so its not a vanishing act").Measured against GW2EI's own exported
combatReplayData, the calibration
gate (tests/replay_golden.rs) moved from 99.77% → 99.97% of samples
within 1 map-pixel on the secondary fixture (100.00% on the primary). An
independent implementation agreeing more after the change is the
evidence that this is closer to truth and not merely different; GW2EI has
the same bug in weaker form, holding only when the gap exceeds
300 + rateand velocity is ≈0.Note this changes replay sample values, and with them the
dist_to_com/
stack_distscalars reduced from the tracks. The
native-json-baseline.sha256.jsondigest was re-taken accordingly, after
a full structural diff against a worktree build of the previous commit
proved that nothing outsideblocks.replaymoved.
Added
-
sc::TELEPORT(85) is decoded, and a teleport target is treated as a
position fact. Its payload usesCBTS_POSITION's exact packed layout
and carries the relocation's destination, so it folds straight into the
same position list. Because a teleport timestamps the move exactly, it
also collapses the poll window to an instant — spending 300ms of
ignorance on a move whose time is known would place a fabricated mid-air
sample on any grid point inside it.This is a no-op on today's logs (arcdps emits an ordinary position event
immediately before every teleport, so blinks and portals already arrive
fully bracketed within one poll). It matters for the sequence arcdps's
author says future builds will emit for a death respawn —unalive -> alive -> teleport— where nothing reported position while dead and the
preceding fact is seconds stale. Today that case is already handled to
within one poll by the window rule above; the event will silently sharpen
it to exact, with no further change needed here.Hand-counted from the arcdps
evtc/README.txtstatechange enum (between
TICK84 andJUMP86). GW2EI models this statechange nowhere, so there
is no cross-implementation check for the ordinal.
Unchanged
analysis::ei_replaydeliberately keeps GW2EI's weakergap > 300 + rate && vel ≈ 0rule. That module exists for EI parity, not for correctness;
making it more accurate would make it less parity-faithful.
Install
# Node
npm install @axiapps/axilog@1.10.1
# Python
pip install axilog==1.10.1
The CLI ships as a static binary — download the archive for your platform from
the assets below, unpack, and run. No runtime to install alongside it.
Verify
Every archive is listed in SHA256SUMS, attached to this release:
sha256sum -c SHA256SUMS --ignore-missing