Skip to content
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
23 changes: 5 additions & 18 deletions libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,6 @@
9: {52, 63, 68, 79}, # Revenant
}

#: v0.16.3: map legacy arcdps elite IDs to the current API-correct
#: ID. Old arcdps revisions sometimes wrote IDs that differ from
#: the GW2 v2 API IDs used by EVTC2025+. Currently identity (no
#: overrides needed for the known old-format IDs) — the EliteSpec
#: enum already uses API-correct values.
_LEGACY_ELITE_OVERRIDE: Final[dict[int, int]] = {}

#: v0.11.0 hotfix: sanity cap for damage / heal / strip values.
#: arcdps uses INT32_MAX (2,147,483,647) as a sentinel for "no
Expand Down Expand Up @@ -1652,11 +1646,10 @@ def _decode_agent_2025(data: bytes, offset: int) -> Agent:
def _decode_agent(data: bytes, offset: int) -> Agent:
"""Decode a single 96-byte legacy agent record at ``offset``.

The legacy path applies ``_LEGACY_ELITE_OVERRIDE`` to map old
arcdps IDs to API IDs, then validates the mapped value against
the profession. If validation fails, the raw value is tried
(handles collision IDs 55 and 63 where the raw value IS the
API ID for one profession). If both fail, degrades to BASE.
The elite spec is validated against the agent's profession via
``_VALID_ELITE_BY_PROFESSION``, which resolves shared collision
IDs (55, 63, 73, 74, 75, 77) by profession membership. Falls
back to ``EliteSpec.BASE`` if validation fails.
"""
aid, prof_raw, elite_raw, _tough, _conc, _heal, _width, name_buf = _AGENT_STRUCT.unpack_from(
data, offset
Expand Down Expand Up @@ -1706,14 +1699,8 @@ def _decode_agent(data: bytes, offset: int) -> Agent:
# against the profession. If the override fails validation, try
# the raw value (handles collision IDs 55 and 63 where the raw
# value IS the API ID for one profession).
mapped_elite = _LEGACY_ELITE_OVERRIDE.get(int(elite_raw), int(elite_raw))
valid_set = _VALID_ELITE_BY_PROFESSION.get(int(prof_raw))
if valid_set is not None and mapped_elite in valid_set:
elite = EliteSpec(mapped_elite)
elif valid_set is not None and int(elite_raw) in valid_set:
# Raw value passes — handles collisions where the override
# maps away from a value that IS valid for this profession
# (e.g. Revenant with elite_raw=63 → 63 in Rev set → Renegade).
if valid_set is not None and int(elite_raw) in valid_set:
elite = EliteSpec(int(elite_raw))
else:
elite = EliteSpec.BASE
Expand Down
89 changes: 4 additions & 85 deletions web/tests/components/PlayerReadoutCells.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import React from "react";

import { buildPlayerReadoutRow } from "../fixtures/playerReadoutRow";
import {
CommanderCellRenderer,
EliteSpecCellRenderer,
Expand All @@ -13,7 +14,6 @@ import {
getProfessionLabel,
getEliteLabel,
} from "@/components/icons/Professions";
import type { PlayerReadoutOut } from "@/lib/api";

/**
* F17 W.1 v2 — vitest specs for the wire-format-aware icon
Expand All @@ -30,90 +30,9 @@ import type { PlayerReadoutOut } from "@/lib/api";
* literals which never matched the wire.
*/

function makeRow(overrides: Partial<PlayerReadoutOut>): PlayerReadoutOut {
return {
agent_id: 1,
account_name: ":test.1234",
name: "Test Character",
profession: "PROF(1)", // Guardian
elite_spec: "BASE",
subgroup: 1,
is_commander: false,
roles: ["DPS"],
damage: {
dps_total: 0,
dps_power: 0,
dps_condi: 0,
strips: 0,
cc_applied: 0,
down_contribution_dps: 0,
kills: 0,
cleave_targets: 0,
kill_participation: 0,
},
heal: {
heal_total: 0,
hps: 0,
barrier_total: 0,
barrier_ps: 0,
cleanses: 0,
stun_breaks: 0,
},
boons: {
boons_out_rate: 0,
boons_in_rate: 0,
stability_out: 0,
alacrity_out: 0,
resistance_out: 0,
aegis_out: 0,
superspeed_out: 0,
stealth_out: 0,
might_uptime: null,
fury_uptime: null,
quickness_uptime: null,
alacrity_uptime: null,
protection_uptime: null,
regeneration_uptime: null,
vigor_uptime: null,
aegis_uptime: null,
stability_uptime: null,
swiftness_uptime: null,
resistance_uptime: null,
resolution_uptime: null,
superspeed_uptime: null,
stealth_uptime: null,
other_boons_out: {},
outgoing_might: null,
outgoing_fury: null,
outgoing_quickness: null,
outgoing_alacrity: null,
outgoing_protection: null,
outgoing_regeneration: null,
outgoing_vigor: null,
outgoing_aegis: null,
outgoing_stability: null,
outgoing_swiftness: null,
outgoing_resistance: null,
outgoing_resolution: null,
outgoing_superspeed: null,
outgoing_stealth: null,
},
defense: {
damage_taken: 0,
cc_taken: 0,
deaths: 0,
time_downed_ms: 0,
dodges: 0,
blocks: 0,
interrupts: 0,
barrier_absorbed: 0,
presence_pct: null,
dist_to_commander: null,
kill_participation: 0,
},
...overrides,
};
}
// makeRow is aliased to buildPlayerReadoutRow for backward compat
// with existing test call-sites that import it as `makeRow`.
const makeRow = buildPlayerReadoutRow;

/* ------------------------------------------------------------------ *
* parseWireFormat
Expand Down
25 changes: 14 additions & 11 deletions web/tests/components/compare-readout-table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import { render, screen } from "@testing-library/react";
import React from "react";

import { CompareReadoutTable } from "@/components/CompareReadoutTable";
import type { PlayerReadoutOut } from "@/lib/api";

function buildRow(overrides: Partial<PlayerReadoutOut> = {}): PlayerReadoutOut {
return {
agent_id: 1,
account_name: ":test.1234",
import { buildPlayerReadoutRow } from "../fixtures/playerReadoutRow";

/**
* Build a ``PlayerReadoutOut`` row with nonzero default values
* suitable for data-rendering assertions. The structural base
* (zero defaults) comes from the shared fixture; the test-local
* defaults here set the values that individual tests check
* (e.g. ``presence_pct: 95`` → ``getByText("95%")``).
*/
function buildRow(
overrides: Parameters<typeof buildPlayerReadoutRow>[0] = {},
) {
return buildPlayerReadoutRow({
name: "Test Player",
profession: "PROF(1)",
elite_spec: "ELITE(27)",
subgroup: 1,
is_commander: false,
roles: ["DPS"],
damage: {
dps_total: 15000,
dps_power: 10000,
Expand Down Expand Up @@ -87,7 +90,7 @@ function buildRow(overrides: Partial<PlayerReadoutOut> = {}): PlayerReadoutOut {
kill_participation: 10,
},
...overrides,
};
});
}

describe("CompareReadoutTable", () => {
Expand Down
113 changes: 113 additions & 0 deletions web/tests/fixtures/playerReadoutRow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { PlayerReadoutOut } from "@/lib/api";

/**
* Shared test fixture builder for ``PlayerReadoutOut``.
*
* Returns a minimal row with all numeric fields set to 0 / null so
* individual tests can override only the fields they care about.
* The default uses ``PROF(1)`` (Guardian) / ``BASE`` (core spec),
* matching the wire format the API emits.
*
* Use the optional ``overrides`` param to customise the returned row
* for a specific test case — the spread ``...overrides`` happens
* **after** the defaults, so you can override any top-level field
* (damage, heal, boons, defense, etc.).
*
* ```ts
* // Default row — all zeros, Guardian core
* buildPlayerReadoutRow()
*
* // Customised row — Guardian Dragonhunter with 30k DPS
* buildPlayerReadoutRow({
* name: "Top DPS",
* elite_spec: "ELITE(27)",
* damage: { dps_total: 30000, dps_power: 20000, dps_condi: 10000, ... },
* })
* ```
*/
export function buildPlayerReadoutRow(
overrides: Partial<PlayerReadoutOut> = {},
): PlayerReadoutOut {
return {
agent_id: 1,
account_name: ":test.1234",
name: "Test Character",
profession: "PROF(1)", // Guardian
elite_spec: "BASE",
subgroup: 1,
is_commander: false,
roles: ["DPS"],
damage: {
dps_total: 0,
dps_power: 0,
dps_condi: 0,
strips: 0,
cc_applied: 0,
down_contribution_dps: 0,
kills: 0,
cleave_targets: 0,
kill_participation: 0,
},
heal: {
heal_total: 0,
hps: 0,
barrier_total: 0,
barrier_ps: 0,
cleanses: 0,
stun_breaks: 0,
},
boons: {
boons_out_rate: 0,
boons_in_rate: 0,
stability_out: 0,
alacrity_out: 0,
resistance_out: 0,
aegis_out: 0,
superspeed_out: 0,
stealth_out: 0,
might_uptime: null,
fury_uptime: null,
quickness_uptime: null,
alacrity_uptime: null,
protection_uptime: null,
regeneration_uptime: null,
vigor_uptime: null,
aegis_uptime: null,
stability_uptime: null,
swiftness_uptime: null,
resistance_uptime: null,
resolution_uptime: null,
superspeed_uptime: null,
stealth_uptime: null,
other_boons_out: {},
outgoing_might: null,
outgoing_fury: null,
outgoing_quickness: null,
outgoing_alacrity: null,
outgoing_protection: null,
outgoing_regeneration: null,
outgoing_vigor: null,
outgoing_aegis: null,
outgoing_stability: null,
outgoing_swiftness: null,
outgoing_resistance: null,
outgoing_resolution: null,
outgoing_superspeed: null,
outgoing_stealth: null,
},
defense: {
damage_taken: 0,
cc_taken: 0,
deaths: 0,
time_downed_ms: 0,
dodges: 0,
blocks: 0,
interrupts: 0,
barrier_absorbed: 0,
presence_pct: null,
dist_to_commander: null,
kill_participation: 0,
},
...overrides,
};
}
Loading