feat(#1127): track endurance, general buffs, and per-ability resource cost/cooldown - #1128
Conversation
…hange stamina Adds cur_endurance/max_endurance/endurance_pct/endurance_confirmed to GameState, fed by two wire sources: OP_EnduranceUpdate (0x5f42, EnduranceUpdate_Struct) is the authoritative source — it carries a real max directly, unlike mana's high-water-mark inference. OP_ManaChange's existing-but-previously-discarded `stamina` field feeds a cheaper trickle update (set_endurance_current) that never lowers a max already confirmed by OP_EnduranceUpdate. Exposed via PlayerState and GET /v1/observe/debug as endurance/endurance_max/ endurance_pct/endurance_verified, with the same honesty contract as hp_verified: false (not a confident 0) until the server has actually said something. Part 1 of 3 for #1127 (agent-plugin-host Observation space prereq). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj
Extends the existing OP_Buff/OP_BuffCreate wire parsing (previously only
fed the narrow SPA-57 levitate channel) to also populate a general
buff list: GameState.buffs is a BTreeMap<slot, BuffSlot{spell_id,
duration_ticks}>, updated by the same two opcodes and left in sync
with LevitateState without altering its existing behavior.
Exposed via PlayerState.buffs (Vec<PlayerBuff>) and served at
GET /v1/observe/debug as player.buffs — an array of
{slot, spell_id, duration_ticks} sorted by slot, always present
(empty array when no buffs are active).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj
…de-ipc tests Pre-existing clippy::bool_assert_comparison warnings, unrelated to #1127 — surfaced only when running a strict --workspace --all-targets -D warnings pass while chasing a clean clippy run for that issue's Part 3 commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj
Part 3 of #1127: extend SpellDb's spells_us.txt parse (already reading target_type/effects from the same caret-delimited row) to also read mana_cost (col 19, signed), cast_time_ms (col 13), and recast_time_ms (col 15) — EQEmu spdat.h's SPDat_Spell_Struct ordinals, cross-checked against 230_spells_table.sql's spells_new column order. GET /v1/observe/spells now reports these three fields per memorized gem alongside name. Per the agent-honesty convention, an empty gem or an unresolved spell id reports null (not a fabricated 0) for all three, since 0 is itself a real, meaningful cost/delay for many spells. Also fixes clippy warnings this change's neighborhood surfaced under a strict --workspace --all-targets -D warnings pass: parse_mana_change's 4-tuple return (widened by Part 1's endurance work) now uses a named ManaChangeFields type alias to stay under clippy's type_complexity threshold, and two Part 2 buff tests use contains_key instead of get(..).is_none(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj
Independent review caught a real correctness bug in the general-buff-list work: OP_Buff's `duration` field and OP_BuffCreate's `tics_remaining` field were read via u32::from_le_bytes, but EQEmu's own Buffs_Struct::ticsremaining is int32, and the server writes PERMANENT_BUFF_DURATION (-1000, common/spdat.h) into it for a permanent buff. That value is copied bit-for-bit into the wire's nominally-uint32 field (confirmed against zone/spells.cpp's SendBuffDurationPacket/MakeBuffsPacket and patches/rof2.cpp's ENCODE(OP_Buff)/ENCODE(OP_BuffCreate) in the EQEmu source), so reading it as unsigned turned a permanent buff into duration_ticks: 4_294_966_296 instead of the server's real -1000 — a fabricated-looking huge number in exactly the API this issue exists to make trustworthy. Fixes BuffSlot::duration_ticks (u32 → i32), the wire parse in packet_handler.rs's apply_buff_with/apply_buff_create_with, buff_slot_set/ buffs_resync's signatures, and the HTTP-exposed PlayerBuff struct, with a new test round-tripping the -1000 sentinel through both OP_Buff and OP_BuffCreate. Docs updated to explain the sentinel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj
|
Ran an independent code review + a live functional test against a real running client before merge. Live test (real EQEmu server, character cast a real self-buff): endurance, buff list, and per-spell cost/cooldown all behaved exactly as documented — mana/cast/recast numbers for memorized gems matched the raw Code review (independent pass, cross-checked against the EQEmu source): found one real correctness bug — Two minor stylistic notes from the review (mana/cast/recast columns default to 🤖 Generated with Claude Code |
… tasks #1127 (bfd7d65/#1128) landed on main during this plan's rebase, adding endurance, general buffs, and per-spell mana cost/cast time/recast delay to GameState/SpellInfo. Update Task 7's OwnState/AbilityFeature/BuffView types, Task 12's legal_actions construction and tests, and Task 13's observation_builder mapping and tests to use the real fields instead of deferring them. Also add OwnState.hp_verified, mirroring GameState's existing hp_verified() honesty contract alongside the new endurance_confirmed field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQTnMEjMF8Y7G5ZUeibYRE
Summary
Implements #1127 in full — prereq groundwork for the agent-plugin-host Observation space:
OP_EnduranceUpdateandOP_ManaChange's stamina field.player.buffs— the full active-buff list ({slot, spell_id, duration_ticks}[]), extending (not replacing) the existing levitate-onlyOP_Buff/OP_BuffCreateparse.SpellDbnow also parsesmana_cost,cast_time_ms, andrecast_time_msfromspells_us.txt(cols 19/13/15 — EQEmuspdat.h'sSPDat_Spell_Structordinals, cross-checked against230_spells_table.sql), exposed per memorized gem viaGET /v1/observe/spells.Per the repo's agent-honesty convention, Part 3's three new fields are
null(never a fabricated0) for an empty gem or an id the loaded spell table has no row for, since0is itself a real, meaningful cost/delay for many spells.Also includes a small unrelated clippy fix (
crates/eqoxide-ipc/src/lib.rs,bool_assert_comparison) needed to get a cleancargo clippy --workspace --all-targets -- -D warnings, plus two similar fixes to Part 1/2's own new code (a named type alias forparse_mana_change's return type, andcontains_keyinstead ofget(..).is_none()in the new buff tests).Test plan
rbuild . test --workspace --locked— 0 failed across the whole workspacecargo clippy --workspace --all-targets -- -D warnings— cleandocs/http-api.mdupdated for all three parts (endurance,buffs,mana_cost/cast_time_ms/recast_time_ms)🤖 Generated with Claude Code
https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj