Skip to content

feat(#1127): track endurance, general buffs, and per-ability resource cost/cooldown - #1128

Merged
djhenry merged 5 commits into
mainfrom
worktree-fix-1127
Sep 16, 2026
Merged

djhenry merged 5 commits into
mainfrom
worktree-fix-1127

Conversation

@djhenry

@djhenry djhenry commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Implements #1127 in full — prereq groundwork for the agent-plugin-host Observation space:

  • Part 1 — Endurance: track current/max endurance (and percent), parsed alongside HP/mana off OP_EnduranceUpdate and OP_ManaChange's stamina field.
  • Part 2 — General buff list: player.buffs — the full active-buff list ({slot, spell_id, duration_ticks}[]), extending (not replacing) the existing levitate-only OP_Buff/OP_BuffCreate parse.
  • Part 3 — Per-ability resource cost/cooldown: SpellDb now also parses mana_cost, cast_time_ms, and recast_time_ms from spells_us.txt (cols 19/13/15 — EQEmu spdat.h's SPDat_Spell_Struct ordinals, cross-checked against 230_spells_table.sql), exposed per memorized gem via GET /v1/observe/spells.

Per the repo's agent-honesty convention, Part 3's three new fields are null (never a fabricated 0) for an empty gem or an id the loaded spell table has no row for, since 0 is 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 clean cargo clippy --workspace --all-targets -- -D warnings, plus two similar fixes to Part 1/2's own new code (a named type alias for parse_mana_change's return type, and contains_key instead of get(..).is_none() in the new buff tests).

Test plan

  • rbuild . test --workspace --locked — 0 failed across the whole workspace
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • docs/http-api.md updated for all three parts (endurance, buffs, mana_cost/cast_time_ms/recast_time_ms)

🤖 Generated with Claude Code

https://claude.ai/code/session_01CWrs7cDd1NAJmaLscBjgcj

djhenry and others added 5 commits September 16, 2026 12:05
…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
@djhenry

djhenry commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

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 spells_us.txt rows exactly, and a live-cast buff correctly appeared in player.buffs with the mana cost deducted as expected.

Code review (independent pass, cross-checked against the EQEmu source): found one real correctness bug — duration_ticks (OP_Buff's duration / OP_BuffCreate's tics_remaining) was parsed as u32, but EQEmu's own field is int32 and the server uses -1000 (PERMANENT_BUFF_DURATION) as a real sentinel for permanent buffs, copied bit-for-bit into the wire's nominally-unsigned field. A permanently-buffed character would have shown duration_ticks: 4294966296 instead of -1000 — a fabricated-looking huge number in exactly the API this issue exists to make trustworthy. Fixed in ff8ac84, with a new test round-tripping the sentinel through both opcodes, and 0 failures across the full workspace test suite + a clean clippy --workspace --all-targets -- -D warnings afterward.

Two minor stylistic notes from the review (mana/cast/recast columns default to 0 rather than a distinct sentinel on an unparseable column, matching the existing convention for icon_id/good_effect/target_type) were left as-is — low practical risk against a stable static data file, and fixing them would mean special-casing three fields differently from their sibling scalar fields for no concrete reachable bug.

🤖 Generated with Claude Code

@djhenry
djhenry merged commit bfd7d65 into main Sep 16, 2026
1 check passed
@djhenry
djhenry deleted the worktree-fix-1127 branch September 16, 2026 19:21
djhenry added a commit that referenced this pull request Sep 16, 2026
… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant