diff --git a/src/main/java/top/ellan/mahjong/render/display/DisplayInteractionRayRegistry.java b/src/main/java/top/ellan/mahjong/render/display/DisplayInteractionRayRegistry.java index 2516236..15929a5 100644 --- a/src/main/java/top/ellan/mahjong/render/display/DisplayInteractionRayRegistry.java +++ b/src/main/java/top/ellan/mahjong/render/display/DisplayInteractionRayRegistry.java @@ -46,15 +46,19 @@ public static void replaceRegion( return; } VIEWER_INTERACTIONS.compute(viewerId, (ignored, current) -> { - if (current != null && !tableId.equals(current.tableId())) { - return interactions == null || interactions.isEmpty() - ? current - : ViewerInteractions.single(tableId, regionKey, interactions); + boolean empty = interactions == null || interactions.isEmpty(); + if (current == null) { + return empty ? null : ViewerInteractions.single(tableId, regionKey, interactions); } - Map> regions = current == null - ? new LinkedHashMap<>() - : new LinkedHashMap<>(current.regions()); - if (interactions == null || interactions.isEmpty()) { + if (!tableId.equals(current.tableId())) { + return empty ? current : ViewerInteractions.single(tableId, regionKey, interactions); + } + List previous = current.regions().get(regionKey); + if ((empty && previous == null) || (!empty && Objects.equals(previous, interactions))) { + return current; + } + Map> regions = new LinkedHashMap<>(current.regions()); + if (empty) { regions.remove(regionKey); } else { regions.put(regionKey, List.copyOf(interactions)); @@ -129,11 +133,11 @@ public static synchronized void replacePublicJoinRegion( return; } PublicRegionKey key = new PublicRegionKey(tableId, regionKey); - List publicJoins = interactions == null - ? List.of() - : interactions.stream() - .filter(interaction -> isPublicJoinForTable(interaction, tableId)) - .toList(); + List previous = PUBLIC_JOIN_REGIONS.get(key); + if (samePublicJoinInteractions(previous, tableId, interactions)) { + return; + } + List publicJoins = filterPublicJoinInteractions(tableId, interactions); if (publicJoins.isEmpty()) { PUBLIC_JOIN_REGIONS.remove(key); } else { @@ -211,6 +215,44 @@ private static void rebuildPublicJoinSnapshot() { publicJoinInteractions = List.copyOf(flattened); } + private static boolean samePublicJoinInteractions( + List previous, + String tableId, + List interactions + ) { + int previousIndex = 0; + if (interactions != null) { + for (RayInteraction interaction : interactions) { + if (!isPublicJoinForTable(interaction, tableId)) { + continue; + } + if (previous == null + || previousIndex >= previous.size() + || !Objects.equals(previous.get(previousIndex), interaction)) { + return false; + } + previousIndex++; + } + } + return previous == null ? previousIndex == 0 : previousIndex == previous.size(); + } + + private static List filterPublicJoinInteractions( + String tableId, + List interactions + ) { + if (interactions == null || interactions.isEmpty()) { + return List.of(); + } + List publicJoins = new ArrayList<>(interactions.size()); + for (RayInteraction interaction : interactions) { + if (isPublicJoinForTable(interaction, tableId)) { + publicJoins.add(interaction); + } + } + return publicJoins.isEmpty() ? List.of() : List.copyOf(publicJoins); + } + private static boolean isPublicJoinForTable(RayInteraction interaction, String tableId) { return interaction != null && interaction.action().actionType() == DisplayClickAction.ActionType.JOIN_SEAT diff --git a/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java b/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java index b92da00..c2c7271 100644 --- a/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java +++ b/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java @@ -62,6 +62,9 @@ synchronized void replace( } Map previous = this.regions.getOrDefault(regionKey, Map.of()); + if (this.canReuseRegion(previous, interactionsByViewer)) { + return; + } Map next = new LinkedHashMap<>(); List pendingSpawns = new ArrayList<>(); try { @@ -132,6 +135,31 @@ synchronized void replace( } } + private boolean canReuseRegion( + Map previous, + Map> interactionsByViewer + ) { + int reusableViewers = 0; + for (Map.Entry> entry + : interactionsByViewer.entrySet()) { + UUID viewerId = entry.getKey(); + List interactions = entry.getValue(); + if (viewerId == null || interactions == null || interactions.isEmpty()) { + continue; + } + Player viewer = this.session.onlinePlayer(viewerId); + if (viewer == null || !viewer.isOnline()) { + continue; + } + ActiveProxies active = previous.get(viewerId); + if (!this.canReuse(viewerId, viewer, active, interactions)) { + return false; + } + reusableViewers++; + } + return reusableViewers == previous.size(); + } + private boolean canReuse( UUID viewerId, Player viewer, diff --git a/src/test/java/top/ellan/mahjong/render/display/FlatDisplayInteractionRegistryTest.java b/src/test/java/top/ellan/mahjong/render/display/FlatDisplayInteractionRegistryTest.java index 7fba400..b3c8292 100644 --- a/src/test/java/top/ellan/mahjong/render/display/FlatDisplayInteractionRegistryTest.java +++ b/src/test/java/top/ellan/mahjong/render/display/FlatDisplayInteractionRegistryTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import java.util.List; import java.util.UUID; @@ -132,6 +133,109 @@ void independentActionAndHandRegionsCoexistAndClearSeparately() { assertEquals(List.of(tile), DisplayInteractionRayRegistry.snapshot(VIEWER_ID)); } + @Test + void identicalRegionReplacementKeepsTheExistingSnapshotAndActionChangesStillApply() { + DisplayInteractionRayRegistry.RayInteraction first = interaction( + 0.0D, + 3.0D, + 1.0D, + 0.0D, + action("first") + ); + DisplayInteractionRayRegistry.replaceRegion( + VIEWER_ID, + "table-a", + "viewer-actions", + List.of(first) + ); + List firstSnapshot = + DisplayInteractionRayRegistry.snapshot(VIEWER_ID); + + DisplayInteractionRayRegistry.replaceRegion( + VIEWER_ID, + "table-a", + "viewer-actions", + List.of(first) + ); + + assertSame(firstSnapshot, DisplayInteractionRayRegistry.snapshot(VIEWER_ID)); + + DisplayInteractionRayRegistry.RayInteraction changed = interaction( + 0.0D, + 3.0D, + 1.0D, + 0.0D, + action("second") + ); + DisplayInteractionRayRegistry.replaceRegion( + VIEWER_ID, + "table-a", + "viewer-actions", + List.of(changed) + ); + + assertEquals(List.of(changed), DisplayInteractionRayRegistry.snapshot(VIEWER_ID)); + } + + @Test + void identicalPublicJoinReplacementKeepsTheExistingFlattenedSnapshot() { + DisplayInteractionRayRegistry.RayInteraction join = interaction( + 0.0D, + 3.0D, + 1.0D, + 0.0D, + DisplayClickAction.joinSeat("table-a", SeatWind.EAST) + ); + DisplayInteractionRayRegistry.RayInteraction privateDecision = interaction( + 0.0D, + 4.0D, + 1.0D, + 0.0D, + action("private") + ); + List input = List.of(join, privateDecision); + + DisplayInteractionRayRegistry.replacePublicJoinRegion("table-a", "seat-label:EAST", input); + List firstSnapshot = + DisplayInteractionRayRegistry.publicJoinSnapshot(); + + DisplayInteractionRayRegistry.replacePublicJoinRegion("table-a", "seat-label:EAST", input); + + assertSame(firstSnapshot, DisplayInteractionRayRegistry.publicJoinSnapshot()); + assertEquals(List.of(join), firstSnapshot); + } + + @Test + void replacingPublicJoinActionStillUpdatesTheFlattenedSnapshot() { + DisplayInteractionRayRegistry.RayInteraction east = interaction( + 0.0D, + 3.0D, + 1.0D, + 0.0D, + DisplayClickAction.joinSeat("table-a", SeatWind.EAST) + ); + DisplayInteractionRayRegistry.RayInteraction south = interaction( + 0.0D, + 3.0D, + 1.0D, + 0.0D, + DisplayClickAction.joinSeat("table-a", SeatWind.SOUTH) + ); + + DisplayInteractionRayRegistry.replacePublicJoinRegion( + "table-a", + "seat-label:EAST", + List.of(east) + ); + DisplayInteractionRayRegistry.replacePublicJoinRegion( + "table-a", + "seat-label:EAST", + List.of(south) + ); + + assertEquals(List.of(south), DisplayInteractionRayRegistry.publicJoinSnapshot()); + } + @Test void tableAndViewerCleanupCannotLeaveStaleControls() { DisplayInteractionRayRegistry.RayInteraction interaction = interaction(