Skip to content
This repository was archived by the owner on Oct 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/gw2_analytics/src/gw2_analytics/buff_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"stability": 25,
"regeneration": 5,
"stealth": 9,
"fury": 9,
"fury": 15,
"quickness": 9,
"alacrity": 9,
"protection": 9,
Expand Down
34 changes: 21 additions & 13 deletions libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,19 +728,27 @@ def parse_events(source: BinaryIO | bytes) -> Iterator[Event]: # noqa: PLR0912,
# overstacked, carrying the wasted duration in
# overstack_value with value == 0, and it does not change
# the target's buff state.
if is_statechange in _BUFF_STATECHANGES_2026_05:
is_statechange = 0
elif is_statechange == _CAST_START_STATECHANGE_2026_05:
# Cast start. The old channel signalled this with
# is_activation = NORMAL (or QUICKNESS, which no log in
# the corpus uses); the new one leaves the byte at 0 and
# carries the same value/buff_dmg duration pair.
is_statechange = 0
is_activation = ActivationType.NORMAL
elif is_statechange == _CAST_END_STATECHANGE_2026_05:
# Cast end: is_activation still holds the terminal
# ActivationType (MINIMUM / CANCEL / RESET / NO_DATA).
is_statechange = 0
if build_int >= _CONDITION_ENUM_REBASE_BUILD and is_statechange in (
_BUFF_STATECHANGES_2026_05
| {_CAST_START_STATECHANGE_2026_05, _CAST_END_STATECHANGE_2026_05}
):
# Code 70 is deliberately NOT folded in: it is an apply that
# overstacked, carrying the wasted duration in
# overstack_value with value == 0, and it does not change
# the target's buff state.
if is_statechange in _BUFF_STATECHANGES_2026_05:
is_statechange = 0
elif is_statechange == _CAST_START_STATECHANGE_2026_05:
# Cast start. The old channel signalled this with
# is_activation = NORMAL (or QUICKNESS, which no log in
# the corpus uses); the new one leaves the byte at 0 and
# carries the same value/buff_dmg duration pair.
is_statechange = 0
is_activation = ActivationType.NORMAL
elif is_statechange == _CAST_END_STATECHANGE_2026_05:
# Cast end: is_activation still holds the terminal
# ActivationType (MINIMUM / CANCEL / RESET / NO_DATA).
is_statechange = 0
else:
(
time_ms,
Expand Down
41 changes: 40 additions & 1 deletion libs/gw2_evtc_parser/tests/test_parser_emit_buff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1379,7 +1379,7 @@ def test_scan_regeneration_overstacks_reports_uncredited_removals() -> None:
"""
evtc = _build_minimal_evtc(
[(1, 1, 1, "Src", True)],
build="20250925",
build="20260507",
skills=[(718, "Regeneration")],
events=[
# Uncredited: iff UNKNOWN and no target agent.
Expand Down Expand Up @@ -1429,3 +1429,42 @@ def test_scan_regeneration_overstacks_reports_uncredited_removals() -> None:
for event in PythonEvtcParser().parse_events(evtc)
if getattr(event, "kind", None) == "remove_single"
] == [2_000]


def test_2026_05_statechange_fold_is_build_gated() -> None:
"""Codes 69/71/72 (buff apply/remove) and 67/68 (cast) only fold on
builds >= 2026-05-07.

arcdps moved buff application/removal and cast start/stop onto
dedicated statechange codes only from build 2026-05-07; before that
the legacy channel (statechange 0 with ``ev.buff``/``is_activation``)
carried them. Folding unconditionally would reinterpret whatever a
pre-2026-05-07 log wrote in those code slots.
"""
skill_725 = _build_event_record_2025(
time_ms=1_000,
src_agent=1,
dst_agent=2,
value=10_000,
skill_id=725, # Fury
is_statechange=69, # BuffApply on the 2026-05 channel
)
for build in ("20250925", "20260506"):
evtc = _build_minimal_evtc(
[(1, 1, 1, "Src", True)],
build=build,
skills=[(725, "Fury")],
events=[skill_725],
)
events = list(PythonEvtcParser().parse_events(evtc))
assert not any(e.skill_id == 725 for e in events), (
f"statechange 69 folded on pre-2026-05-07 build {build}"
)
evtc = _build_minimal_evtc(
[(1, 1, 1, "Src", True)],
build="20260507",
skills=[(725, "Fury")],
events=[skill_725],
)
events = list(PythonEvtcParser().parse_events(evtc))
assert any(e.skill_id == 725 for e in events), "statechange 69 not folded on 2026-05-07 build"
6 changes: 3 additions & 3 deletions scripts/ei-parity/corpus-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
"cli_sha256": "5ffe8565a55f2f80a88dc28f4ec0f383fd84a64954c360c47264ecafed7faa38",
"export_type": "detailed_wvw_kill"
},
"manifest_sha256": "ac66f2f7531dc9402592c8c7fe17918f71adfc5baa90b806af00105a7c39fce9",
"manifest_sha256": "c6a3bb012c2974200951b3149687151d462feed8228452b9fc2e7c778dfeb148",
"log_count": 35,
"buckets": {
"players.buffUptimes.uptime": 52,
"players.buffUptimes.uptime": 518,
"players.group": 6,
"players.rotation": 52,
"players.rotation": 41,
"players.statsAll.appliedCrowdControl": 1,
"players.statsAll.appliedCrowdControlDownContribution": 2,
"players.statsAll.appliedCrowdControlDuration": 1,
Expand Down
Loading
Loading