From 395df8820894632e0bf0c67f85f2e9f2fe85594c Mon Sep 17 00:00:00 2001 From: shoeless Date: Sun, 9 Aug 2026 15:38:58 -0700 Subject: [PATCH 1/2] test(net): deterministic guard that hidden-zone reveals reach the client delta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RevealSyncDeltaTest sets up an exact board with the AITest helpers (no AI, no sockets), grants a top-library reveal via Future Sight's continuous static, runs the real DeltaSyncManager.collectDeltas, and asserts the top card's PlayerMayLook travels in the delta the networked client receives. This guards the delta walker: DeltaSyncManager.walkAndCollect recurses into hidden zone (Library/Hand) collections and collects each nested CardView's dirty props, so an owner sees their own revealed top card without any extra zone flagging. No existing test covers this path — DeltaSyncUnitTest only checks packet byte-sizes, and NetworkPlayIntegrationTest checks public-zone visibility only (and is stress-gated). If a future change stopped walking hidden-zone collections, this fails; otherwise it is a fast (~10s), default-run regression guard, and a reusable pattern for "does host change X reach the client delta?". Co-Authored-By: Claude Opus 4.8 --- .../java/forge/net/RevealSyncDeltaTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 forge-gui-desktop/src/test/java/forge/net/RevealSyncDeltaTest.java diff --git a/forge-gui-desktop/src/test/java/forge/net/RevealSyncDeltaTest.java b/forge-gui-desktop/src/test/java/forge/net/RevealSyncDeltaTest.java new file mode 100644 index 000000000000..6bd805470264 --- /dev/null +++ b/forge-gui-desktop/src/test/java/forge/net/RevealSyncDeltaTest.java @@ -0,0 +1,102 @@ +package forge.net; + +import forge.ai.AITest; +import forge.game.Game; +import forge.game.GameView; +import forge.game.card.CardView; +import forge.game.player.Player; +import forge.game.player.PlayerView; +import forge.game.zone.ZoneType; +import forge.gamemodes.net.DeltaPacket; +import forge.gamemodes.net.server.DeltaSyncManager; +import forge.trackable.TrackableProperty; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Map; + +/** + * Reusable harness for verifying that host-side game-state changes actually reach a + * networked client through the real delta-sync path ({@link DeltaSyncManager}). + * + *

Pattern for tests of this type: + *

    + *
  1. Build an exact board with {@link AITest} helpers (deterministic — no AI, no sockets).
  2. + *
  3. Take an initial full sync with a fresh {@link DeltaSyncManager} (client is "caught up").
  4. + *
  5. Mutate the host state (the thing under test).
  6. + *
  7. Collect the incremental delta and assert the change is present in it — i.e. the client + * would receive it.
  8. + *
+ * + *

The first scenario here is the "own top-library card reveal" sync: a Future Sight controller + * must be able to see the revealed top card of their own (hidden) library on their client. This runs + * the SAME delta code the iOS/desktop network clients use (default {@code useDeltaSync=true}), but + * deterministically and in-process. + * + *

Why a dedicated test: a delta regression that drops a hidden-zone reveal is otherwise + * invisible — the periodic network checksums did not walk the Library zone. + */ +public class RevealSyncDeltaTest extends AITest { + + /** Find the delta entry (new-object or incremental) for a given trackable id, or null. */ + private static Map deltaFor(DeltaPacket packet, int cardViewId) { + int key = DeltaPacket.makeDeltaKey(DeltaPacket.TYPE_CARD_VIEW, cardViewId); + Map d = packet.getObjectDeltas().get(key); + if (d == null) { + d = packet.getNewObjects().get(key); + } + return d; + } + + @Test + public void topLibraryRevealReachesClientDelta() { + Game game = initAndCreateGame(); + Player a = game.getPlayers().get(1); // active player; the "reveal owner" + + // One card in A's (hidden) library — this is the top card Future Sight reveals. + addCardToZone("Island", a, ZoneType.Library); + game.getAction().checkStateEffects(true); + + GameView gv = game.getView(); + // Realize the library CardView so it exists in the view graph before the initial sync. + CardView topBefore = gv.getPlayers().get(1).getCards(ZoneType.Library).iterator().next(); + PlayerView aView = gv.getPlayers().get(1); + int topId = topBefore.getId(); + + Assert.assertFalse(topBefore.canBeShownTo(aView), + "Precondition: with no reveal source, A must NOT see its own top library card"); + + // 1. Initial full sync — the client is now caught up (no reveal yet). + DeltaSyncManager sync = new DeltaSyncManager(); + sync.collectDeltas(gv); + + // 2. Host-side change under test: Future Sight enters A's battlefield and its continuous + // static grants A may-look on the top library card. + addCard("Future Sight", a); + game.getAction().checkStateEffects(true); + + // Host truth: A can now see its own top library card. + Assert.assertTrue(topBefore.canBeShownTo(aView), + "Host state: with Future Sight in play, A should see its own top library card"); + + // 3. Collect the incremental delta the client would receive. + sync.registerNewObjects(gv); + DeltaPacket delta = sync.collectDeltas(gv); + + // 4. The decisive check: does the reveal (PlayerMayLook on the top library CardView) + // actually travel in the delta? If yes, the client syncs the reveal. + Map cardDelta = deltaFor(delta, topId); + boolean mayLookInDelta = cardDelta != null && cardDelta.containsKey(TrackableProperty.PlayerMayLook); + + System.out.println("[REVEALSYNC] top library CardView id=" + topId + + " present-in-delta=" + (cardDelta != null) + + " PlayerMayLook-in-delta=" + mayLookInDelta + + (cardDelta != null ? " props=" + cardDelta.keySet() : "")); + + Assert.assertTrue(mayLookInDelta, + "The top library card's PlayerMayLook reveal must be present in the client delta, " + + "otherwise the owning player never sees their own revealed top card. " + + "cardDelta=" + cardDelta); + } +} From 5c6b7d4b46ec3e21a3c27c8036fd7caa773410e1 Mon Sep 17 00:00:00 2001 From: shoeless Date: Mon, 10 Aug 2026 23:51:09 -0700 Subject: [PATCH 2/2] Net checksums: include the Library zone in the sampled checksum walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collectChecksumObjects walked Battlefield/Hand/Graveyard/Exile/Command but not Library, so hidden-zone card state (e.g. a PlayerMayLook reveal from Future Sight-style effects) could desync silently — no checksum ever saw it. Add Library to the walk; the collection hash is order-insensitive (sorted ids) and the cost is a few hundred extra property hashes per sampled checksum. Co-Authored-By: Claude Opus 4.8 --- .../src/main/java/forge/gamemodes/net/NetworkChecksumUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/src/main/java/forge/gamemodes/net/NetworkChecksumUtil.java b/forge-gui/src/main/java/forge/gamemodes/net/NetworkChecksumUtil.java index 95aeaa36f1ec..d5df029c0d28 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetworkChecksumUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetworkChecksumUtil.java @@ -382,6 +382,7 @@ public static List collectChecksumObjects(GameView gameView) { addCards(cards, getEffectiveValue(player, TrackableProperty.Graveyard)); addCards(cards, getEffectiveValue(player, TrackableProperty.Exile)); addCards(cards, getEffectiveValue(player, TrackableProperty.Command)); + addCards(cards, getEffectiveValue(player, TrackableProperty.Library)); cards.sort(Comparator.comparingInt(CardView::getId)); for (CardView card : cards) {