From e328da066e467a2de82f0bc1e2b71cdfad72f471 Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Sun, 10 May 2026 07:25:54 +0930 Subject: [PATCH 1/2] Fix remote player not seeing Jhoira choice dialog Skip IdRef replacement for CardViews absent from the host tracker so ephemerals (e.g. Jhoira choice copies that never enter a tracked zone) serialize natively. The receiver registers them in its tracker on arrival so subsequent IdRef references resolve correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../gamemodes/net/TrackableSerializer.java | 16 +++++++++++++++- .../gamemodes/net/client/GameClientHandler.java | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/forge-gui/src/main/java/forge/gamemodes/net/TrackableSerializer.java b/forge-gui/src/main/java/forge/gamemodes/net/TrackableSerializer.java index 4219c6339a8..50ed7888617 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/TrackableSerializer.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/TrackableSerializer.java @@ -65,16 +65,30 @@ static TrackableType trackableTypeFor(byte typeTag) { * set so the receiver decodes a detached CardView from the carried name * and image key. When {@code tracker} is null, the snapshot check is * skipped (used by the client encoder, which has no game-state awareness). + * + *

In non-event mode, CardViews missing from the tracker pass through + * unchanged so Java serializes the full object inline (covers ephemeral + * choice copies that never enter a tracked zone). */ static Object replace(Object obj, Tracker tracker, boolean eventMode) { if (obj instanceof TrackableObject trackable) { byte tag = typeTagFor(trackable); if (tag < 0) return obj; - if (!eventMode || tag == TYPE_PLAYER_VIEW) { + if (tag == TYPE_PLAYER_VIEW) { return new IdRef(tag, trackable.getId()); } + if (!eventMode) { + if (tracker != null) { + TrackableType type = trackableTypeFor(tag); + if (type != null && tracker.getObj(type, trackable.getId()) != null) { + return new IdRef(tag, trackable.getId()); + } + } + return obj; // ephemeral or tracker-less encoder — serialize the full object + } + boolean preserveSnapshot = false; if (tracker != null) { TrackableType type = trackableTypeFor(tag); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java b/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java index aea0d0a0b4e..a21549fcc81 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java @@ -126,7 +126,9 @@ protected void beforeCall(final ChannelHandlerContext ctx, final ProtocolMethod /** * This method is used to recursively update the tracker - * references on all objects and their props. + * references on all objects and their props, and register them in the + * id lookup so inline-serialized CardViews are findable by IdRef + * in subsequent messages. * * @param objs */ @@ -135,6 +137,7 @@ private void updateTrackers(final Object[] objs) { if (obj instanceof TrackableObject trackableObject) { if (trackableObject.getTracker() == null) { trackableObject.setTracker(this.tracker); + registerInTracker(trackableObject); // walk the props EnumMap props = trackableObject.getProps(); if (props != null) { @@ -153,6 +156,18 @@ private void updateTrackers(final Object[] objs) { } } + private void registerInTracker(final TrackableObject obj) { + if (obj instanceof CardView cv) { + if (tracker.getObj(TrackableTypes.CardViewType, cv.getId()) == null) { + tracker.putObj(TrackableTypes.CardViewType, cv.getId(), cv); + } + } else if (obj instanceof PlayerView pv) { + if (tracker.getObj(TrackableTypes.PlayerViewType, pv.getId()) == null) { + tracker.putObj(TrackableTypes.PlayerViewType, pv.getId(), pv); + } + } + } + private void replicateProps(final Object[] objs) { for (Object obj: objs) { if (obj instanceof PlayerView pv) { From dc3c95e611b8e0f2fbfb8340f8b189ce4568b07e Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Mon, 11 May 2026 06:36:34 +0930 Subject: [PATCH 2/2] Mirror IdRef compression on the client encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set the tracker on the client encoder so client→server CardView args serialize as IdRef when the tracker holds them. Drop the bulk-register of inline-arrived CardViews — tracker miss is now the symmetric ephemeral signal on both ends. Addresses review feedback that the prior commit dropped the protocol-arg IdRef optimization on the client→server leg. With the client encoder running the same presence check the host uses, real CardViews (which reach the client via delta first) round-trip as IdRef; ephemerals (absent from both trackers) serialize as full objects in both directions, matching the host's behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../net/client/GameClientHandler.java | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java b/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java index a21549fcc81..5a9d666dbac 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/GameClientHandler.java @@ -4,6 +4,7 @@ import forge.game.card.CardView; import forge.game.player.PlayerView; import forge.gamemodes.net.CompatibleObjectDecoder; +import forge.gamemodes.net.CompatibleObjectEncoder; import forge.gamemodes.net.GameProtocolHandler; import forge.gui.GuiBase; import forge.util.IHasForgeLog; @@ -74,12 +75,14 @@ protected void beforeCall(final ChannelHandlerContext ctx, final ProtocolMethod if (args.length > 0 && args[0] instanceof GameView gameView) { if (this.tracker == null) { this.tracker = new Tracker(); - // Set tracker on decoder for IdRef resolution in server messages. - // The client encoder does NOT get a tracker — it uses simple - // IdRef replacement without stale detection. Stale detection - // on the client would create StaleCardRef markers for cards - // updated by delta sync, causing the server to create detached - // CardViews that don't match real game objects. + // Encoder uses the tracker to emit IdRef for client→server + // CardView args (presence check only — stale detection is + // server-only). Ephemerals absent from the tracker + // serialize as full objects in both directions. + CompatibleObjectEncoder encoder = ctx.pipeline().get(CompatibleObjectEncoder.class); + if (encoder != null) { + encoder.setTracker(this.tracker); + } CompatibleObjectDecoder decoder = ctx.pipeline().get(CompatibleObjectDecoder.class); if (decoder != null) { decoder.setTracker(this.tracker); @@ -126,9 +129,11 @@ protected void beforeCall(final ChannelHandlerContext ctx, final ProtocolMethod /** * This method is used to recursively update the tracker - * references on all objects and their props, and register them in the - * id lookup so inline-serialized CardViews are findable by IdRef - * in subsequent messages. + * references on all objects and their props. + * + *

Inline-serialized CardViews are intentionally NOT registered in the + * tracker's id lookup: a tracker miss is the symmetric signal that a + * CardView is ephemeral, mirroring the host's encoder check. * * @param objs */ @@ -137,7 +142,6 @@ private void updateTrackers(final Object[] objs) { if (obj instanceof TrackableObject trackableObject) { if (trackableObject.getTracker() == null) { trackableObject.setTracker(this.tracker); - registerInTracker(trackableObject); // walk the props EnumMap props = trackableObject.getProps(); if (props != null) { @@ -156,18 +160,6 @@ private void updateTrackers(final Object[] objs) { } } - private void registerInTracker(final TrackableObject obj) { - if (obj instanceof CardView cv) { - if (tracker.getObj(TrackableTypes.CardViewType, cv.getId()) == null) { - tracker.putObj(TrackableTypes.CardViewType, cv.getId(), cv); - } - } else if (obj instanceof PlayerView pv) { - if (tracker.getObj(TrackableTypes.PlayerViewType, pv.getId()) == null) { - tracker.putObj(TrackableTypes.PlayerViewType, pv.getId(), pv); - } - } - } - private void replicateProps(final Object[] objs) { for (Object obj: objs) { if (obj instanceof PlayerView pv) {