Skip to content

Extend IdRef serialization from game events to protocol method args#10343

Open
MostCromulent wants to merge 8 commits intoCard-Forge:masterfrom
MostCromulent:fix-protocol-serialization-delta
Open

Extend IdRef serialization from game events to protocol method args#10343
MostCromulent wants to merge 8 commits intoCard-Forge:masterfrom
MostCromulent:fix-protocol-serialization-delta

Conversation

@MostCromulent
Copy link
Copy Markdown
Contributor

@tool4ever re-targeting this at master now that delta is merged.

Previous PR discussion should be preserved here: MostCromulent#17


Summary

Protocol methods serialize CardView/PlayerView references as full object graphs — every selectCard, selectPlayer, and similar call sends the entire trackable object tree. This replaces that with lightweight IdRef(typeTag, id) markers at the Netty encoder/decoder level, and consolidates all TrackableObject serialization logic into a single TrackableSerializer class.

  • Server→client and client→server protocol args are replaced with IdRef markers by the encoder and resolved from the Tracker by the decoder
  • DeltaPacket events are wrapped with IdRef replacement at packet construction time and unwrapped after state application on the client (avoids a decoder timing issue where new objects aren't in the tracker yet during deserialization)
  • GameEventProxy removed — its IdRef/StaleCardRef replacement logic is folded into TrackableSerializer; its event byte-array wrapping is replaced by wrapEvents/unwrapEvents
  • findByView fallback added in Game.java — when a zone-specific search misses (e.g. IdRef resolved from a tracker that wasn't updated after a zone change), falls back to global ID search

setGameView and openView are excluded from replacement since they carry the full state that populates the client's Tracker.

Design decisions

Fallback search vs tracker update (findByView): When the server tracker holds a stale CardView with an old zone (because updateObjLookup skips objects whose ID is already registered), findByView searches the wrong zone. Rather than modifying the tracker's !hasObj guard (shared by delta sync and view updates, high blast radius), a fallback global search is added — it only triggers on a miss, matching the existing null-zone fallback path.

Event wrap/unwrap timing: Events in DeltaPacket can't use encoder-level replacement because the decoder runs before applyDelta creates new objects in the tracker. Instead, events are serialized to byte arrays with IdRef replacement at packet construction time (server-side), and deserialized with resolution after state application (client-side), when the tracker is fully populated.

Server vs client encoder modes: The server encoder uses a full Tracker (set by RemoteClientGuiGame.setGameView) for verified replacement with StaleCardRef detection. The client encoder does simple IdRef replacement without a tracker — using one would create StaleCardRef markers that the server resolves as detached CardViews, breaking game object identity.


🤖 Generated with Claude Code

RafaelHGOliveira added a commit to RafaelHGOliveira/forge that referenced this pull request Apr 10, 2026
…l method args

Replaces full CardView/PlayerView object graphs in protocol method args
with lightweight IdRef(typeTag, id) markers resolved from the local Tracker.
Consolidates TrackableObject serialization into TrackableSerializer;
removes GameEventProxy (absorbed into TrackableSerializer).

Also fixes pre-existing PlayerControllerHuman call to updateHasAvailableActions
using removed int signature; updated to Predicate<SpellAbility> form.
MostCromulent and others added 7 commits April 11, 2026 20:14
Protocol methods were serializing the entire CardView/PlayerView object
graph for each argument. Add IdRef replacement at the Netty encoder/decoder
level: the server encoder swaps CardView/PlayerView with lightweight
IdRef(typeTag, id) markers, and the client decoder resolves them from the
Tracker. setGameView and openView are excluded (they carry the full state).

Extract shared ID-mapping primitives (IdRef, type tags, typeTagFor,
trackableTypeFor) from GameEventProxy into TrackableRef so both the
encoder/decoder path and GameEventProxy can reuse them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends encoder replacement (previously server→client only) to the
client→server path. The client encoder now replaces CardView/PlayerView
with lightweight IdRef markers in protocol method args like selectCard,
and the server decoder resolves them from the tracker.

Testing revealed that after a zone change (e.g. playing a land from
hand), the server tracker holds a stale CardView with the old zone,
causing Game.findByView to search the wrong zone and return null. Added
a fallback global search in findByView when the zone-specific search
misses, rather than modifying tracker update behavior which has wide
blast radius.

Also removes GameEventProxy — raw GameEvent objects travel inside
DeltaPacket, with replacement handled by the encoder/decoder. Confirmed
by batch testing (0 dropped events).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…coder

Events embedded in DeltaPacket are excluded from encoder IdRef replacement
(to protect state data), so their TrackableObject references arrive as
raw deserialized instances with null trackers. When event handlers call
TrackableTypes.lookup(), the null tracker causes NPE.

Added a fallback in TrackableSerializer.resolve() that detects raw
TrackableObjects with no tracker and resolves them to the canonical
tracker instance, matching what GameEventProxy.unwrapAll previously did.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Events bundled in DeltaPacket were excluded from encoder-level IdRef
replacement to avoid a decoder timing issue: the Netty decoder resolves
IdRefs during deserialization, before applyDelta creates new objects in
the client tracker. This caused events to travel as raw TrackableObjects,
losing StaleCardRef detection and inflating wire size.

Restore the wrap/unwrap pattern from the removed GameEventProxy, now in
TrackableSerializer: events are serialized to byte arrays with IdRef
replacement at DeltaPacket construction time (server), and deserialized
with resolution after state application (client), when the tracker is
fully populated. This gives events proper IdRef/StaleCardRef handling
without the decoder timing problem.

- Remove applyDelta exclusion from shouldReplaceTrackables
- Remove raw-TrackableObject fallback from TrackableSerializer.resolve
- Add wrapEvents/unwrapEvents to TrackableSerializer
- Wrap events in both delta+events and events-only paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…verload

Address PR #17 review feedback from tool4ever:

- Delete single-arg TrackableSerializer.replace(Object) — verified
  byte-equivalent to replace(obj, null) (the tracker null-check is the
  only difference, and skipping it falls through to the same IdRef
  return). Update CObjectOutputStream and TrackableSerializer's own
  ReplacingOutputStream to use the unified two-arg form.

- Delete CompatibleObjectEncoder.ReplacingObjectOutputStream and
  CompatibleObjectDecoder.ResolvingObjectInputStream — exact duplicates
  of TrackableSerializer.ReplacingOutputStream/ResolvingInputStream
  modulo class qualifier. Make those inner classes package-private and
  reuse them directly from the encoder/decoder.
@tool4ever tool4ever force-pushed the fix-protocol-serialization-delta branch from d751aba to d4771ab Compare April 11, 2026 18:17
@tool4ever
Copy link
Copy Markdown
Contributor

Planning to merge in around two weeks if nothing else comes up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants