Skip to content

engine: an item action has to survive the wire, not just a loopback (RFC 0021 follow-up) - #2288

Open
campavao wants to merge 1 commit into
bryanthaboi:devfrom
campavao:rfc-0021-item-survives-the-wire
Open

campavao wants to merge 1 commit into
bryanthaboi:devfrom
campavao:rfc-0021-item-survives-the-wire

Conversation

@campavao

Copy link
Copy Markdown
Contributor

Follow-up to #2284 (RFC 0021). The feature works in-process and does not work between two machines; this is the one line that was missing, plus the test that would have caught it.

The bug

opts.items puts the turn's item on the wire as { type = "action", kind = "item", item, index, move }. Wire.sanitize rebuilds every inbound message field by field from SCHEMAS[type] and drops anything the schema does not name, and SCHEMAS.action names only kind, slot and index. So item and move were stripped on the way in.

What the peer then receives is an item action with no item in it: LinkItems.apply returns {}, the turn is still spent, nothing is printed, and the two simulations part by one heal with nothing on either screen to say so — until a per-turn hash several turns later blames the wrong turn.

SCHEMAS.spectate calls the same sanitize, so a spectator was drifting for the same reason and is fixed by the same line.

Why #2284's tests are green

tests/engine/link_items.lua, and every other in-process link test, pairs the two sides with Net.loopbackPair, which hands the message table straight across. sanitize runs only on the way through Session, which is every real transport. The gap is invisible to a loopback pair by construction.

Found by a downstream mod's two-client harness on the first real duel with items on: the guest healed its lead 12 → 18 and the host watched its copy of that same Pokémon go 12 → 6.

The change

  • src/link/Wire.luaSCHEMAS.action gains item (a string id) and move (clamped to 1..MAX_MOVES), beside slot and index.
  • tests/engine/link_items.lua — pins the schema directly: an item action through sanitize, the same through a spectate wrapper, and a move action that carries no item. A check a loopback pair cannot make.
  • docs/rfcs/0021-link-battle-items.md — a section saying a new field on an existing message type is invisible until SCHEMAS names it, and describing the failure mode, so the next person adding one does not repeat it.

Verification

  • luajit tests/run_link_tests.lua — ALL LINK TESTS PASSED
  • luajit tests/engine/link_items.lua — 28/28
  • luajit tests/run_engine.lua — 572/584, the same twelve reds as origin/dev with this branch stashed (audio device, shell-script scan, curl env, version dev identity and friends; none link-related)
  • Two real clients over a local relay: the bag opens in a duel, the POTION rides the wire, the other machine applies the heal to its copy and prints it, and the fight keeps resolving turns afterwards.

…RFC 0021)

RFC 0021 gave a mode back the bag on the link cable: opts.items makes an
item the turn, and `{ type = "action", kind = "item", item, index, move }`
rides the wire so the peer -- and a spectator -- can apply the same effect
to their copies before the moves.  The peer never got the item.

Wire.sanitize rebuilds every inbound message field by field from
SCHEMAS[type] and drops anything the schema does not name.  SCHEMAS.action
names kind, slot and index; `item` and `move` are not among them, so they
were stripped on the way in.  Added beside the others, clamped the same
way (a string id, a move slot in 1..MAX_MOVES).  SCHEMAS.spectate calls
the same sanitize, so a spectator is fixed by the same line.

Why the tests did not see it.  tests/engine/link_items.lua -- and every
in-process link test -- pairs the two sides with Net.loopbackPair, which
hands the table straight over; sanitize only runs on the way through
Session, which is every REAL transport.  So the failure needed two
machines to show up, and when it did it was the quietest kind: the peer
receives an item action with no item in it, LinkItems.apply returns
nothing, the turn is still spent, nothing is printed, and the two
simulations part by one heal with nothing on either screen to say so --
until a hash several turns later blames the wrong turn.  Found by a
downstream mod's two-client harness on its first real duel: the guest
healed 12 -> 18 and the host watched its copy of that mon go 12 -> 6.

link_items.lua now pins the schema directly (an item action through
sanitize, through a spectate wrapper, and a move action carrying no
item), which is a check a loopback pair cannot make.  docs/rfcs/0021
gains the section saying a new field on an existing message type is
invisible until SCHEMAS knows about it.

tests/run_link_tests.lua green; the engine tier matches stock's reds
(572/584, the same twelve).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

CI build artifacts

Commit: #e3ee1f7
Build Time: 2026-09-16 14:43:54 UTC

This comment was automatically generated. View workflow run

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