Skip to content

Add GameEventProxy to reduce network event serialization size#10018

Merged
tool4ever merged 9 commits intoCard-Forge:masterfrom
MostCromulent:gameeventproxy
Mar 9, 2026
Merged

Add GameEventProxy to reduce network event serialization size#10018
tool4ever merged 9 commits intoCard-Forge:masterfrom
MostCromulent:gameeventproxy

Conversation

@MostCromulent
Copy link
Copy Markdown
Contributor

@MostCromulent MostCromulent commented Mar 8, 2026

@tool4ever latest salvo in our war to win the GameEvent refactor.

Summary

  • Introduces GameEventProxy, which wraps GameEvent objects by replacing CardView and PlayerView references with lightweight ID markers before network serialization, preventing Java serialization from expanding the full game state object graph in each event batch
  • Increases the network event flush interval from 50ms to 500ms to reduce the number of batches sent per game
  • Adds a unit test that discovers all GameEvent record types via reflection and verifies each can be serialized by the proxy

How it works

GameEventProxy uses Java's ObjectOutputStream.replaceObject() / ObjectInputStream.resolveObject() hooks to automatically substitute TrackableObject references with IdRef(typeTag, id) markers during local byte-array serialization. This handles all nesting (records, collections, maps) automatically — no per-event-type handling needed.

Server side: In NetGuiGame.handleGameEvents(), each GameEvent is serialized into a byte array with CardView/PlayerView references replaced by IdRef markers. The resulting GameEventProxy is just a byte[] wrapper — Netty encodes it without traversing the game state object graph.

Client side: In GameClientHandler.beforeCall(handleGameEvents), proxy byte arrays are deserialized with IdRef markers resolved back to TrackableObject instances from the client's Tracker. To ensure all objects are available for resolution, updateObjLookup() is called synchronously on the IO thread when setGameView arrives (since copyChangedProps() runs asynchronously on the EDT and may not have completed yet).

Why only CardView and PlayerView?

Only these two types are replaced with ID markers. Other TrackableObject types serialize normally (they are small and don't cause graph expansion):

  • StackItemView — Ephemeral. Only exists on the stack while a spell is resolving. With the 500ms flush interval, spells often resolve before the batch sends. The GameView reflects current state, so the StackItemView may no longer be present for updateObjLookup() to register.
  • SpellAbilityView — Not reachable from GameView's property graph. No TrackableProperty uses SpellAbilityViewType, so updateObjLookup() on the incoming GameView never discovers these objects.
  • CombatView — Not referenced by any GameEvent record.
  • CardStateView — Nested inside CardView; not independently referenced in events.

If wrapping fails for any individual event, the original event is sent as a fallback.

## Test

GameEventProxyTest uses Guava's ClassPath to discover all GameEvent record classes in forge.game.event, constructs each with default values, and verifies GameEventProxy.wrap() succeeds. This catches non-serializable fields in new or modified events at build time rather than at runtime during network play.

Local testing

Verified with manual network games (server + client on localhost):

  • Zero proxy-related errors (no "Failed to wrap", "Failed to unwrap", or "Could not resolve" in logs)
  • handleGameEvents messages no longer appear in the encoder size log (below the 20KB reporting threshold), confirming the proxy is effective
  • Only setGameView messages (~20KB compressed) appear in the encoder log — unchanged from before
  • Game plays through combat, spells, and zone changes with no visual issues on the client side
  • Unit test passes

🤖 Generated with Claude Code

Wraps GameEvent objects with lightweight ID markers for CardView and
PlayerView references, preventing Java serialization from expanding
the full game state object graph in each event batch. Increases the
network event flush interval from 50ms to 500ms to reduce batch count.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread forge-gui-desktop/src/test/java/forge/net/GameEventProxyTest.java Outdated
Comment thread forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java Outdated
Address PR review feedback: remove GameEventProxyTest (redundant with
GameEventSerializationTest) and remove the two-arg constructor overload
from GameEventForwarder, making 500ms the default flush interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread forge-gui/src/main/java/forge/gamemodes/net/GameEventProxy.java Outdated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tool4EvEr and others added 3 commits March 9, 2026 08:43
Previously events were only flushed when a new event arrived AND the
interval had elapsed. Events buffered mid-action (e.g. land play) would
stay pending until the next player action or priority change. Now a
deferred flush is scheduled when events are buffered, guaranteeing
delivery within the flush interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread forge-gui/src/main/java/forge/gui/control/GameEventForwarder.java
MostCromulent and others added 3 commits March 9, 2026 21:33
When GameEventProxy resolves IdRefs, it looks up CardViews from the
client's Tracker. These CardViews were registered on the first
setGameView and never updated — updateObjLookup skips existing entries.
The stale CardViews retained their original zone (e.g. Library), causing
canBeShownTo to return false and getImageKey to return the hidden-card
token instead of the actual card art.

Add refreshTrackerCardViews() to replace tracker entries with incoming
server CardViews on each setGameView, so IdRef resolution always gets
current zone and state data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NetGuiGame.shutdownForwarder() and call it from
HostedMatch.endCurrentGame() to clean up the scheduled executor
when the game ends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tool4ever tool4ever merged commit bc86ed2 into Card-Forge:master Mar 9, 2026
2 checks passed
@MostCromulent MostCromulent deleted the gameeventproxy branch March 9, 2026 19:10
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