Skip to content

Releases: darkharasho/axilog

v1.16.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 08:04

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 to casts_drawn exactly; 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-missing

v1.15.0

Choose a tag to compare

@github-actions github-actions released this 03 Oct 11:35

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 SAME GuardianShout effect, 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.py skipped 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
    rotation nor skillMap.

    A large minority of those lambdas are not arbitrary closures: they are
    conjunctions of calls to NAMED CombatData/AgentItem helpers. Five
    Check variants now express them — RelatedHit (HasRelatedHit),
    RelatedEffectDst (HasRelatedEffectDst), RelatedBuff
    (HasGainedBuff/HasLostBuff/HasLostBuffStack), SelfBuffApply (the
    GetBuffApplyDataByIDBySrc + FindRelatedEvents duration-window and
    stack-count shapes) and Species (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
    is Check::NoAnimatedCast, and an AppliedDuration comparison on the
    triggering event is Check::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 that RevenantHelper.ProcessGadgets assigns 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.zevtc against a GW2EI CLI oracle
    built from upstream cc820b66:

    • 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
      own rotation exactly, 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 but blocks.rotation.by_entity.*.casts changed 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.

Changed

  • The replay-enabled HTML size gate moved 600KB → 620KB. The four
    newly-detected skills each gain a skill_map row (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. rotation itself
    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-missing

v1.14.1

Choose a tag to compare

@github-actions github-actions released this 02 Oct 01:34

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 subgroup triple, once anonymised to the player's
    WvW rank title with a blank account and no subgroup — and resolve() makes
    one Player per raw agent row, so both reached wvw::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_players now merges rows sharing an agent_addr first. 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 unions agent_addrs rather 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-missing

v1.14.0

Choose a tag to compare

@github-actions github-actions released this 12 Sep 19:12

Added

  • variant_label on catalogs.skills[skill_id]. A readable label for
    skill ids that share a name, 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-missing

v1.13.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 00:26

Fixed

  • playerTotal now reaches the EI-shaped document. v1.13.0 added
    player_total to every blocks.damage.by_entity[].by_skill_taken[] row,
    but only to the native container. The consumer it was built for reads
    ei-json exclusively through parseFileEi, so the field it needed was
    unreachable from the surface it reads. dist_rows_ei_json now emits it as
    playerTotal on totalDamageTaken rows.

    The emit is gated on the field's own Option rather than on the call
    site, so it scopes itself to the one grouping that measures the split:
    outgoing and per-target rows carry None and keep their previous key set
    byte for byte. playerTotal is an axilog extension — GW2EI has no such
    key — and it refines totalDamage rather 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 a playerTotal under
    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-missing

v1.13.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 22:53

Added

  • player_total on every blocks.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_taken rows only. On by_skill/per_target it 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 <= total holds per row, and the split is a refinement of total rather than
    a filter on it, so sum(by_skill_taken[*].total) == taken is 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)] on SkillEntryOut, which
    carries it only to reach the 1.0 block builder.

Fixed

  • InstidRegistry::build no longer registers agent addr 0, which could
    shadow an instid's real owner.
    arcdps emits damage rows with a real
    src_instid but src_agent == 0 when 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 made resolve_at answer
    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
    their src_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-missing

v1.12.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 00:52

Changed

  • blocks.focus is now unsupported (and omitted) on pre-2026-05 logs,
    instead of empty with a zeroed roster.
    The enemy cast-start census
    only exists in the post-rework CBTS_ANIMATIONSTART statechange 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 zeroed by_entity
    roster under coverage.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.zevtc fixture (build 20260114) — so every schema test of
    this block was asserting on a structurally empty one.

    coverage.focus now reads "unsupported" and the block is omitted, the
    same rule blocks.healing already follows for a log recorded without the
    healing extension. Consumers must treat a missing blocks.focus as "not
    measurable here", never as "nobody was targeted".

Added

  • total_minion_casts / casts_drawn_minions on blocks.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 of casts_drawn, total_casts, focus_index and pre_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 (via CBTS_BUFFINITIAL, which the filter never emits), which is
    what makes the "exactly zero ANIMATIONSTOP" and "zero null dst" 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-missing

v1.11.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 05:10

Added

  • blocks.focus — enemy attention per squad player. arcdps's enemy-event
    filter is DST-driven for CBTS_ANIMATIONSTART: an enemy cast-start row
    survives into the log exactly when its dst_agent is 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_index is a player's share of those aimed casts divided by an even
    1/squad_size share — 1.0 is exactly average attention, 3.0 is three
    times what an evenly-targeted squad member draws. Also per player:
    casts_drawn, downs, and pre_down_casts (casts aimed at them in the
    3s before a down, the window carried on the wire as pre_down_window_ms).
    skills[] carries per-skill casts_at_squad / hits / damage_total
    against a whole-log mean_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 why skills[] 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
    and coverage.focus reads empty.

    Two limits are structural, both from the same filter: no enemy
    CBTS_ANIMATIONSTOP row 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.js and skeleton.html are 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, past golden_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
    .gitattributes rule now holds the whole asset directory at LF, which also
    restores the cross-platform byte-identity the crate's determinism tests
    otherwise imply.
  • The parseFileEi byte-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 — the skillMap entries 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-missing

v1.10.2

Choose a tag to compare

@github-actions github-actions released this 31 Aug 16:12

Fixed

  • Non-squad friendly players no longer collapse by elite spec.
    wvw::dedupe_players keyed on account, falling back to character,
    and it runs on enc.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
    character substituted 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_players regroups on
    instid, mirroring GW2EI's non-squad GroupBy(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_players now keys on account where known (still the primary
    key, so a relogged squadmate collapses regardless of instid) and falls
    back to instid where it is blank, never to character. A
    blank-account row with no resolvable instid stays distinct. Both dedupes
    now share one InstidRegistry instead 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 on 20260117-175120 and 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-missing

v1.10.1

Choose a tag to compare

@github-actions github-actions released this 31 Aug 03:49

Fixed

  • blocks.replay no longer interpolates across gaps arcdps never
    claimed.
    analysis::replay lerped between whichever two
    CBTS_POSITION events 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. On 20260830-180925.zevtc one 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_window now inserts a synthetic sample carrying the
    earlier position at b.t - 300 for 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 + rate and velocity is ≈0.

    Note this changes replay sample values, and with them the dist_to_com /
    stack_dist scalars reduced from the tracks. The
    native-json-baseline.sha256.json digest was re-taken accordingly, after
    a full structural diff against a worktree build of the previous commit
    proved that nothing outside blocks.replay moved.

Added

  • sc::TELEPORT (85) is decoded, and a teleport target is treated as a
    position fact.
    Its payload uses CBTS_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.txt statechange enum (between
    TICK 84 and JUMP 86). GW2EI models this statechange nowhere, so there
    is no cross-implementation check for the ordinal.

Unchanged

  • analysis::ei_replay deliberately keeps GW2EI's weaker gap > 300 + rate && vel ≈ 0 rule. 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