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 c2c7271..10a91d3 100644 --- a/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java +++ b/src/main/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinator.java @@ -75,23 +75,21 @@ synchronized void replace( if (viewerId == null || interactions == null || interactions.isEmpty()) { continue; } - Player viewer = this.session.onlinePlayer(viewerId); - if (viewer == null || !viewer.isOnline()) { - continue; - } ActiveProxies current = previous.get(viewerId); - if (this.canReuse(viewerId, viewer, current, interactions)) { + if (current != null + && current.viewer().isOnline() + && this.canReuse(viewerId, current, interactions)) { next.put(viewerId, current); continue; } + Player viewer = this.session.onlinePlayer(viewerId); + if (viewer == null || !viewer.isOnline()) { + continue; + } List geometry = interactionGeometry(interactions); List proxies = this.backend.create(viewer, interactions); if (!proxies.isEmpty()) { - ActiveProxies created = new ActiveProxies( - viewer, - List.copyOf(proxies), - geometry - ); + ActiveProxies created = new ActiveProxies(viewer, List.copyOf(proxies), geometry); next.put(viewerId, created); pendingSpawns.add(new PendingSpawn(viewerId, created)); } @@ -147,12 +145,10 @@ private boolean canReuseRegion( 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)) { + if (active == null + || !active.viewer().isOnline() + || !this.canReuse(viewerId, active, interactions)) { return false; } reusableViewers++; @@ -162,12 +158,10 @@ private boolean canReuseRegion( private boolean canReuse( UUID viewerId, - Player viewer, ActiveProxies active, List interactions ) { if (active == null - || active.viewer() != viewer || !sameGeometry(active.geometry(), interactions) || active.proxies().isEmpty()) { return false; diff --git a/src/test/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinatorTest.java b/src/test/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinatorTest.java index 484e03b..b0ecf06 100644 --- a/src/test/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinatorTest.java +++ b/src/test/java/top/ellan/mahjong/table/render/SparrowRayInteractionProxyCoordinatorTest.java @@ -123,7 +123,7 @@ void clearedViewerOwnershipMakesAnUnchangedRegionStaleForReconnect() { } @Test - void unchangedGeometryReusesClientProxyWithoutMorePackets() { + void unchangedGeometryReusesClientProxyWithoutMorePacketsOrViewerLookups() { TableSessionContext session = mock(TableSessionContext.class); SparrowRayInteractionProxyCoordinator.Backend backend = mock( SparrowRayInteractionProxyCoordinator.Backend.class @@ -153,6 +153,56 @@ void unchangedGeometryReusesClientProxyWithoutMorePackets() { verify(backend, times(1)).create(eq(viewer), any()); verify(backend, times(1)).spawn(viewer, List.of(proxy)); verify(backend, never()).destroy(viewer, List.of(proxy)); + verify(session, times(1)).onlinePlayer(VIEWER_ID); + assertEquals(1, coordinator.entityCount()); + } + + @Test + void offlineCachedViewerUsesFreshSessionPlayer() { + TableSessionContext session = mock(TableSessionContext.class); + SparrowRayInteractionProxyCoordinator.Backend backend = mock( + SparrowRayInteractionProxyCoordinator.Backend.class + ); + SparrowRayInteractionProxyCoordinator.ClientProxy firstProxy = mock( + SparrowRayInteractionProxyCoordinator.ClientProxy.class + ); + SparrowRayInteractionProxyCoordinator.ClientProxy reconnectedProxy = mock( + SparrowRayInteractionProxyCoordinator.ClientProxy.class + ); + Player firstViewer = mock(Player.class); + Player reconnectedViewer = mock(Player.class); + when(session.id()).thenReturn("table-a"); + when(session.onlinePlayer(VIEWER_ID)).thenReturn(firstViewer).thenReturn(reconnectedViewer); + when(firstViewer.isOnline()).thenReturn(true); + when(reconnectedViewer.isOnline()).thenReturn(true); + when(backend.available()).thenReturn(true); + when(backend.create(eq(firstViewer), any())).thenReturn(List.of(firstProxy)); + when(backend.create(eq(reconnectedViewer), any())).thenReturn(List.of(reconnectedProxy)); + when(firstProxy.entityId()).thenReturn(1205); + when(reconnectedProxy.entityId()).thenReturn(1206); + doAnswer(invocation -> { + invocation.getArgument(1).run(); + return null; + }).when(session).runForViewer(any(Player.class), any(Runnable.class)); + SparrowRayInteractionProxyCoordinator coordinator = new SparrowRayInteractionProxyCoordinator( + session, + backend + ); + Map> interactions = Map.of( + VIEWER_ID, + List.of(interaction()) + ); + + coordinator.replace("actions", interactions); + when(firstViewer.isOnline()).thenReturn(false); + coordinator.replace("actions", interactions); + + verify(backend).spawn(firstViewer, List.of(firstProxy)); + verify(backend).spawn(reconnectedViewer, List.of(reconnectedProxy)); + verify(backend, never()).destroy(firstViewer, List.of(firstProxy)); + verify(session, times(2)).onlinePlayer(VIEWER_ID); + assertNull(ClientInteractionProxyRegistry.tableIdFor(1205, VIEWER_ID)); + assertEquals("table-a", ClientInteractionProxyRegistry.tableIdFor(1206, VIEWER_ID)); assertEquals(1, coordinator.entityCount()); }