Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,52 +17,19 @@
import org.bukkit.entity.Player;

public final class TableRenderSnapshotFactory {
private static final SeatWind[] SEAT_WINDS = SeatWind.values();

private ViewerMembershipPlan cachedViewerMembershipPlan;

public TableRenderSnapshot create(TableRenderSubject session, long version, long cancellationNonce) {
Location tableCenter = session.center();
boolean started = session.isStarted();
List<SerializedViewerId> serializedOnlineViewerIds = session.viewers().stream()
.map(Player::getUniqueId)
.distinct()
.map(viewerId -> new SerializedViewerId(viewerId, viewerId.toString()))
.sorted(Comparator.comparing(SerializedViewerId::serializedId))
.toList();
List<UUID> onlineViewerIds = serializedOnlineViewerIds.stream()
.map(SerializedViewerId::id)
.toList();
Set<UUID> onlineViewerIdSet = new HashSet<>(onlineViewerIds);
SeatWind[] seatWinds = SeatWind.values();
EnumMap<SeatWind, UUID> seatPlayerIds = new EnumMap<>(SeatWind.class);
for (SeatWind wind : seatWinds) {
seatPlayerIds.put(wind, session.playerAt(wind));
}
Map<UUID, String> viewerMembershipSignatures = new HashMap<>();
Map<UUID, List<UUID>> viewerIdsExcluding = new HashMap<>();
for (UUID playerId : seatPlayerIds.values()) {
if (viewerMembershipSignatures.containsKey(playerId)
|| playerId != null && !onlineViewerIdSet.contains(playerId)) {
continue;
}
viewerMembershipSignatures.put(
playerId,
this.viewerMembershipSignature(serializedOnlineViewerIds, playerId)
);
viewerIdsExcluding.put(
playerId,
playerId == null ? List.copyOf(onlineViewerIds) : this.viewerIdsExcluding(onlineViewerIds, playerId)
);
}
ViewerMembershipPlan viewerMembershipPlan = this.resolveViewerMembershipPlan(session);
EnumMap<SeatWind, TableSeatRenderSnapshot> seats = new EnumMap<>(SeatWind.class);
for (SeatWind wind : seatWinds) {
for (SeatWind wind : SEAT_WINDS) {
seats.put(
wind,
this.captureSeatSnapshot(
session,
wind,
seatPlayerIds.get(wind),
onlineViewerIdSet,
viewerMembershipSignatures,
viewerIdsExcluding
)
this.captureSeatSnapshot(session, wind, viewerMembershipPlan.seat(wind))
);
}
return new TableRenderSnapshot(
Expand Down Expand Up @@ -127,14 +94,72 @@ public TableSeatRenderSnapshot createPrivateHandSeat(TableRenderSubject session,
);
}

private ViewerMembershipPlan resolveViewerMembershipPlan(TableRenderSubject session) {
List<Player> viewers = session.viewers();
ViewerMembershipPlan cached = this.cachedViewerMembershipPlan;
if (cached != null && cached.matches(session, viewers)) {
return cached;
}
ViewerMembershipPlan captured = this.captureViewerMembershipPlan(session, viewers);
this.cachedViewerMembershipPlan = captured;
return captured;
}

private ViewerMembershipPlan captureViewerMembershipPlan(TableRenderSubject session, List<Player> viewers) {
List<UUID> sourceViewerIds = viewers.stream()
.map(Player::getUniqueId)
.toList();
List<SerializedViewerId> serializedOnlineViewerIds = sourceViewerIds.stream()
.distinct()
.map(viewerId -> new SerializedViewerId(viewerId, viewerId.toString()))
.sorted(Comparator.comparing(SerializedViewerId::serializedId))
.toList();
List<UUID> onlineViewerIds = serializedOnlineViewerIds.stream()
.map(SerializedViewerId::id)
.toList();
Set<UUID> onlineViewerIdSet = new HashSet<>(onlineViewerIds);
Map<UUID, SeatViewerMembership> membershipsByPlayerId = new HashMap<>();
EnumMap<SeatWind, SeatViewerMembership> seatMemberships = new EnumMap<>(SeatWind.class);
for (SeatWind wind : SEAT_WINDS) {
UUID playerId = session.playerAt(wind);
SeatViewerMembership membership = membershipsByPlayerId.get(playerId);
if (membership == null && !membershipsByPlayerId.containsKey(playerId)) {
membership = this.captureSeatViewerMembership(
playerId,
serializedOnlineViewerIds,
onlineViewerIds,
onlineViewerIdSet
);
membershipsByPlayerId.put(playerId, membership);
}
seatMemberships.put(wind, membership);
}
return new ViewerMembershipPlan(session, sourceViewerIds, seatMemberships);
}

private SeatViewerMembership captureSeatViewerMembership(
UUID playerId,
List<SerializedViewerId> serializedOnlineViewerIds,
List<UUID> onlineViewerIds,
Set<UUID> onlineViewerIdSet
) {
if (playerId != null && !onlineViewerIdSet.contains(playerId)) {
return new SeatViewerMembership(playerId, false, "", List.of());
}
return new SeatViewerMembership(
playerId,
playerId != null,
this.viewerMembershipSignature(serializedOnlineViewerIds, playerId),
playerId == null ? List.copyOf(onlineViewerIds) : this.viewerIdsExcluding(onlineViewerIds, playerId)
);
}

private TableSeatRenderSnapshot captureSeatSnapshot(
TableRenderSubject session,
SeatWind wind,
UUID playerId,
Set<UUID> onlineViewerIdSet,
Map<UUID, String> viewerMembershipSignatures,
Map<UUID, List<UUID>> viewerIdsExcluding
SeatViewerMembership viewerMembership
) {
UUID playerId = viewerMembership.playerId();
boolean occupied = playerId != null;
return new TableSeatRenderSnapshot(
wind,
Expand All @@ -145,13 +170,13 @@ private TableSeatRenderSnapshot captureSeatSnapshot(
occupied && session.isRiichi(playerId),
occupied && session.isReady(playerId),
occupied && session.isQueuedToLeave(playerId),
occupied && onlineViewerIdSet.contains(playerId),
viewerMembershipSignatures.getOrDefault(playerId, ""),
viewerMembership.online(),
viewerMembership.signature(),
occupied ? session.selectedHandTileIndex(playerId) : -1,
occupied ? session.selectedHandTileIndices(playerId) : List.of(),
occupied ? session.riichiDiscardIndex(playerId) : -1,
session.stickLayoutCount(wind),
viewerIdsExcluding.getOrDefault(playerId, List.of()),
viewerMembership.viewerIdsExcluding(),
occupied ? session.hand(playerId) : List.of(),
occupied ? session.discards(playerId) : List.of(),
occupied ? session.fuuro(playerId) : List.of(),
Expand All @@ -176,5 +201,39 @@ private List<UUID> viewerIdsExcluding(List<UUID> onlineViewerIds, UUID excludedP
.toList();
}

private record ViewerMembershipPlan(
TableRenderSubject subject,
List<UUID> sourceViewerIds,
EnumMap<SeatWind, SeatViewerMembership> seatMemberships
) {
private boolean matches(TableRenderSubject session, List<Player> viewers) {
if (this.subject != session || this.sourceViewerIds.size() != viewers.size()) {
return false;
}
for (SeatWind wind : SEAT_WINDS) {
if (!Objects.equals(this.seat(wind).playerId(), session.playerAt(wind))) {
return false;
}
}
for (int index = 0; index < this.sourceViewerIds.size(); index++) {
if (!this.sourceViewerIds.get(index).equals(viewers.get(index).getUniqueId())) {
return false;
}
}
return true;
}

private SeatViewerMembership seat(SeatWind wind) {
return this.seatMemberships.get(wind);
}
}

private record SeatViewerMembership(
UUID playerId,
boolean online,
String signature,
List<UUID> viewerIdsExcluding
) {}

private record SerializedViewerId(UUID id, String serializedId) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package top.ellan.mahjong.table.render

import org.bukkit.Location
import org.bukkit.entity.Player
import org.mockito.AdditionalAnswers
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
Expand All @@ -15,6 +16,8 @@ import top.ellan.mahjong.table.core.MahjongTableSession
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertSame
import kotlin.test.assertTrue

class TableRenderSnapshotFactoryTest {
Expand Down Expand Up @@ -117,8 +120,55 @@ class TableRenderSnapshotFactoryTest {
assertEquals(144, snapshot.wallCapacity())
assertTrue(!snapshot.usesDeadWall())

val reusedSnapshot = factory.create(session, 2L, 0L)
for (wind in SeatWind.values()) {
assertSame(snapshot.seat(wind).viewerIdsExcluding(), reusedSnapshot.seat(wind).viewerIdsExcluding())
assertSame(
snapshot.seat(wind).viewerMembershipSignature(),
reusedSnapshot.seat(wind).viewerMembershipSignature(),
)
}

`when`(session.viewers()).thenReturn(
listOf(eastViewer, duplicateEastViewer, westViewer, southViewer),
)
val reorderedSnapshot = factory.create(session, 3L, 0L)
assertEquals(eastSeat.viewerIdsExcluding(), reorderedSnapshot.seat(SeatWind.EAST).viewerIdsExcluding())
assertEquals(
eastSeat.viewerMembershipSignature(),
reorderedSnapshot.seat(SeatWind.EAST).viewerMembershipSignature(),
)
assertNotSame(
reusedSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
reorderedSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
)

`when`(session.viewers()).thenReturn(
listOf(southViewer, westViewer, duplicateEastViewer, eastViewer),
)
val restoredSnapshot = factory.create(session, 4L, 0L)
val secondSession =
mock(
MahjongTableSession::class.java,
AdditionalAnswers.delegatesTo<MahjongTableSession>(session),
)
val secondSessionSnapshot = factory.create(secondSession, 5L, 0L)
assertEquals(
restoredSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
secondSessionSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
)
assertNotSame(
restoredSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
secondSessionSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
)
assertNotSame(
restoredSnapshot.seat(SeatWind.EAST).viewerMembershipSignature(),
secondSessionSnapshot.seat(SeatWind.EAST).viewerMembershipSignature(),
)

factory.create(session, 6L, 0L)
`when`(session.playerAt(SeatWind.NORTH)).thenReturn(null)
val emptyNorthSeat = factory.create(session, 2L, 0L).seat(SeatWind.NORTH)
val emptyNorthSeat = factory.create(session, 7L, 0L).seat(SeatWind.NORTH)
assertEquals(listOf(eastId, westId, southId), emptyNorthSeat.viewerIdsExcluding())
assertEquals(
"00000000-0000-0000-0000-000000000011" +
Expand All @@ -127,6 +177,24 @@ class TableRenderSnapshotFactoryTest {
emptyNorthSeat.viewerMembershipSignature(),
)

`when`(session.viewers()).thenReturn(listOf(westViewer, eastViewer))
val removedViewerSnapshot = factory.create(session, 8L, 0L)
assertTrue(!removedViewerSnapshot.seat(SeatWind.SOUTH).online())
assertTrue(removedViewerSnapshot.seat(SeatWind.SOUTH).viewerIdsExcluding().isEmpty())
assertTrue(removedViewerSnapshot.seat(SeatWind.SOUTH).viewerMembershipSignature().isEmpty())
assertEquals(
listOf(westId),
removedViewerSnapshot.seat(SeatWind.EAST).viewerIdsExcluding(),
)
assertEquals(
westId.toString(),
removedViewerSnapshot.seat(SeatWind.EAST).viewerMembershipSignature(),
)
assertEquals(
listOf(eastId, westId),
removedViewerSnapshot.seat(SeatWind.NORTH).viewerIdsExcluding(),
)

verify(session, never()).onlinePlayer(ArgumentMatchers.any(UUID::class.java))
verify(session, never()).viewerIdsExcluding(ArgumentMatchers.any(UUID::class.java))
verify(session, never()).viewerMembershipSignatureFor(ArgumentMatchers.any(UUID::class.java))
Expand Down