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 54d0cfb4be4..907ccae7845 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..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); @@ -128,6 +131,10 @@ protected void beforeCall(final ChannelHandlerContext ctx, final ProtocolMethod * This method is used to recursively update the tracker * 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 */ private void updateTrackers(final Object[] objs) {