diff --git a/src/main/java/top/ellan/mahjong/table/render/TableRenderSnapshotFactory.java b/src/main/java/top/ellan/mahjong/table/render/TableRenderSnapshotFactory.java index 1523f107..ad880526 100644 --- a/src/main/java/top/ellan/mahjong/table/render/TableRenderSnapshotFactory.java +++ b/src/main/java/top/ellan/mahjong/table/render/TableRenderSnapshotFactory.java @@ -4,6 +4,7 @@ import top.ellan.mahjong.render.TableRenderSubject; import top.ellan.mahjong.render.snapshot.TableRenderSnapshot; import top.ellan.mahjong.render.snapshot.TableSeatRenderSnapshot; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; @@ -12,7 +13,6 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.Comparator; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -20,16 +20,24 @@ public final class TableRenderSnapshotFactory { public TableRenderSnapshot create(TableRenderSubject session, long version, long cancellationNonce) { Location tableCenter = session.center(); boolean started = session.isStarted(); - List onlineViewerIds = session.viewers().stream() + List serializedOnlineViewerIds = session.viewers().stream() .map(Player::getUniqueId) .distinct() - .sorted(Comparator.comparing(UUID::toString)) + .map(viewerId -> new SerializedViewerId(viewerId, viewerId.toString())) + .sorted(Comparator.comparing(SerializedViewerId::serializedId)) + .toList(); + List onlineViewerIds = serializedOnlineViewerIds.stream() + .map(SerializedViewerId::id) .toList(); Set onlineViewerIdSet = new HashSet<>(onlineViewerIds); Map viewerMembershipSignatures = new HashMap<>(); Map> viewerIdsExcluding = new HashMap<>(); - for (UUID viewerId : onlineViewerIds) { - viewerMembershipSignatures.put(viewerId, this.viewerMembershipSignature(onlineViewerIds, viewerId)); + for (SerializedViewerId viewer : serializedOnlineViewerIds) { + UUID viewerId = viewer.id(); + viewerMembershipSignatures.put( + viewerId, + this.viewerMembershipSignature(serializedOnlineViewerIds, viewerId) + ); viewerIdsExcluding.put(viewerId, this.viewerIdsExcluding(onlineViewerIds, viewerId)); } EnumMap seats = new EnumMap<>(SeatWind.class); @@ -130,11 +138,11 @@ private TableSeatRenderSnapshot captureSeatSnapshot( ); } - private String viewerMembershipSignature(List onlineViewerIds, UUID excludedPlayerId) { + private String viewerMembershipSignature(List onlineViewerIds, UUID excludedPlayerId) { StringBuilder builder = new StringBuilder(onlineViewerIds.size() * 36); - for (UUID viewerId : onlineViewerIds) { - if (!viewerId.equals(excludedPlayerId)) { - builder.append(viewerId); + for (SerializedViewerId viewer : onlineViewerIds) { + if (!viewer.id().equals(excludedPlayerId)) { + builder.append(viewer.serializedId()); } } return builder.toString(); @@ -145,4 +153,6 @@ private List viewerIdsExcluding(List onlineViewerIds, UUID excludedP .filter(viewerId -> !viewerId.equals(excludedPlayerId)) .toList(); } + + private record SerializedViewerId(UUID id, String serializedId) {} } diff --git a/src/test/kotlin/top/ellan/mahjong/table/render/TableRenderSnapshotFactoryTest.kt b/src/test/kotlin/top/ellan/mahjong/table/render/TableRenderSnapshotFactoryTest.kt index 404cb5d4..ece38a27 100644 --- a/src/test/kotlin/top/ellan/mahjong/table/render/TableRenderSnapshotFactoryTest.kt +++ b/src/test/kotlin/top/ellan/mahjong/table/render/TableRenderSnapshotFactoryTest.kt @@ -1,8 +1,5 @@ package top.ellan.mahjong.table.render -import top.ellan.mahjong.model.SeatWind -import top.ellan.mahjong.render.scene.MeldView -import top.ellan.mahjong.table.core.MahjongTableSession import org.bukkit.Location import org.bukkit.entity.Player import org.mockito.ArgumentMatchers @@ -11,6 +8,9 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import top.ellan.mahjong.model.SeatWind +import top.ellan.mahjong.render.scene.MeldView +import top.ellan.mahjong.table.core.MahjongTableSession import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals @@ -18,19 +18,29 @@ import kotlin.test.assertTrue class TableRenderSnapshotFactoryTest { @Test - fun `render snapshot reuses precomputed online viewers`() { + fun `render snapshot preserves serialized viewer ordering and membership`() { val session = mock(MahjongTableSession::class.java) val factory = TableRenderSnapshotFactory() val eastId = UUID.fromString("00000000-0000-0000-0000-000000000011") - val southId = UUID.fromString("00000000-0000-0000-0000-000000000012") + val southId = UUID.fromString("80000000-0000-0000-0000-000000000012") + val westId = UUID.fromString("7fffffff-ffff-ffff-ffff-ffffffffffff") + val northId = UUID.fromString("00000000-0000-0000-0000-000000000014") val eastViewer = mock(Player::class.java) + val duplicateEastViewer = mock(Player::class.java) val southViewer = mock(Player::class.java) + val westViewer = mock(Player::class.java) + assertTrue(southId.compareTo(eastId) < 0) + assertTrue(southId.toString().compareTo(eastId.toString()) > 0) `when`(eastViewer.uniqueId).thenReturn(eastId) + `when`(duplicateEastViewer.uniqueId).thenReturn(eastId) `when`(southViewer.uniqueId).thenReturn(southId) + `when`(westViewer.uniqueId).thenReturn(westId) `when`(session.center()).thenReturn(Location(null, 0.0, 64.0, 0.0)) - `when`(session.viewers()).thenReturn(listOf(eastViewer, southViewer)) + `when`(session.viewers()).thenReturn( + listOf(southViewer, westViewer, duplicateEastViewer, eastViewer), + ) `when`(session.isStarted()).thenReturn(false) `when`(session.isRoundFinished()).thenReturn(false) `when`(session.remainingWallCount()).thenReturn(0) @@ -49,8 +59,8 @@ class TableRenderSnapshotFactoryTest { `when`(session.doraIndicators()).thenReturn(emptyList()) `when`(session.playerAt(SeatWind.EAST)).thenReturn(eastId) `when`(session.playerAt(SeatWind.SOUTH)).thenReturn(southId) - `when`(session.playerAt(SeatWind.WEST)).thenReturn(null) - `when`(session.playerAt(SeatWind.NORTH)).thenReturn(null) + `when`(session.playerAt(SeatWind.WEST)).thenReturn(westId) + `when`(session.playerAt(SeatWind.NORTH)).thenReturn(northId) doAnswer { invocation -> (invocation.arguments[0] as UUID?)?.toString() ?: "" @@ -61,7 +71,7 @@ class TableRenderSnapshotFactoryTest { `when`(session.stickLayoutCount(wind)).thenReturn(0) `when`(session.cornerSticks(wind)).thenReturn(emptyList()) } - for (playerId in listOf(eastId, southId)) { + for (playerId in listOf(eastId, southId, westId, northId)) { `when`(session.points(playerId)).thenReturn(25000) `when`(session.isRiichi(playerId)).thenReturn(false) `when`(session.isReady(playerId)).thenReturn(false) @@ -77,13 +87,30 @@ class TableRenderSnapshotFactoryTest { val snapshot = factory.create(session, 1L, 0L) val eastSeat = snapshot.seat(SeatWind.EAST) val southSeat = snapshot.seat(SeatWind.SOUTH) + val westSeat = snapshot.seat(SeatWind.WEST) + val northSeat = snapshot.seat(SeatWind.NORTH) assertTrue(eastSeat.online()) assertTrue(southSeat.online()) - assertEquals(listOf(southId), eastSeat.viewerIdsExcluding()) - assertEquals(listOf(eastId), southSeat.viewerIdsExcluding()) - assertEquals(southId.toString(), eastSeat.viewerMembershipSignature()) - assertEquals(eastId.toString(), southSeat.viewerMembershipSignature()) + assertTrue(westSeat.online()) + assertTrue(!northSeat.online()) + assertEquals(listOf(westId, southId), eastSeat.viewerIdsExcluding()) + assertEquals(listOf(eastId, westId), southSeat.viewerIdsExcluding()) + assertEquals(listOf(eastId, southId), westSeat.viewerIdsExcluding()) + assertTrue(northSeat.viewerIdsExcluding().isEmpty()) + assertEquals( + "7fffffff-ffff-ffff-ffff-ffffffffffff80000000-0000-0000-0000-000000000012", + eastSeat.viewerMembershipSignature(), + ) + assertEquals( + "00000000-0000-0000-0000-0000000000117fffffff-ffff-ffff-ffff-ffffffffffff", + southSeat.viewerMembershipSignature(), + ) + assertEquals( + "00000000-0000-0000-0000-00000000001180000000-0000-0000-0000-000000000012", + westSeat.viewerMembershipSignature(), + ) + assertTrue(northSeat.viewerMembershipSignature().isEmpty()) verify(session, never()).onlinePlayer(ArgumentMatchers.any(UUID::class.java)) verify(session, never()).viewerIdsExcluding(ArgumentMatchers.any(UUID::class.java)) @@ -123,7 +150,12 @@ class TableRenderSnapshotFactoryTest { val snapshot = factory.create(session, 1L, 0L) assertTrue(snapshot.doraIndicators().isEmpty()) + for (wind in SeatWind.values()) { + val seat = snapshot.seat(wind) + assertTrue(!seat.online()) + assertTrue(seat.viewerIdsExcluding().isEmpty()) + assertTrue(seat.viewerMembershipSignature().isEmpty()) + } verify(session, never()).doraIndicators() } } -