Skip to content
Closed
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
111 changes: 106 additions & 5 deletions src/main/java/net/tfminecraft/games/display/DisplayManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;

import io.papermc.paper.event.packet.PlayerChunkLoadEvent;
import io.papermc.paper.event.packet.PlayerChunkUnloadEvent;

import net.tfminecraft.games.Games;
import net.tfminecraft.games.Messages;
import net.tfminecraft.games.cache.Cache;
Expand Down Expand Up @@ -227,10 +232,11 @@ public void onChunkLoad(ChunkLoadEvent event) {
continue;
}
for (Player viewer : nearby(origin)) {
if (!token.viewers.contains(viewer.getUniqueId())) {
show(viewer, token);
// The server just loaded this chunk, so clients no longer have the packet entity.
if (token.viewers.contains(viewer.getUniqueId())) {
reshow(viewer, token);
} else {
sendUpdate(viewer, token, 0, 0);
show(viewer, token);
}
}
}
Expand All @@ -256,18 +262,113 @@ public void onInteract(PlayerInteractEntityEvent event) {
event.getPlayer().sendMessage(Messages.get("display.clicked", "token", tokenId));
}

/**
* The client drops packet entities when it unloads their chunk. A metadata update does not
* bring them back, so coming into the chunk has to spawn them again.
*/
@EventHandler
public void onPlayerChunkLoad(PlayerChunkLoadEvent event) {
syncChunk(event.getPlayer(), event.getWorld(), event.getChunk().getX(), event.getChunk().getZ(), true);
}

@EventHandler
public void onPlayerChunkUnload(PlayerChunkUnloadEvent event) {
syncChunk(event.getPlayer(), event.getWorld(), event.getChunk().getX(), event.getChunk().getZ(), false);
}

@EventHandler
public void onMove(PlayerMoveEvent event) {
Location to = event.getTo();
if (to == null) {
return;
}
Location from = event.getFrom();
if (from.getBlockX() == to.getBlockX() && from.getBlockY() == to.getBlockY()
&& from.getBlockZ() == to.getBlockZ()) {
return;
}
refreshViewer(event.getPlayer());
}

@EventHandler
public void onRespawn(PlayerRespawnEvent event) {
Bukkit.getScheduler().runTask(Games.plugin, () -> showNearby(event.getPlayer()));
}

private void showNearby(Player viewer) {
if (!viewer.isOnline() || !ProtocolLibBridge.isReady()) {
refreshViewer(viewer);
}

private void refreshViewer(Player viewer) {
if (viewer == null || !viewer.isOnline() || !ProtocolLibBridge.isReady()) {
return;
}
Location loc = viewer.getLocation();
for (Token token : tokens.values()) {
if (inRange(loc, token.origin) && !token.viewers.contains(viewer.getUniqueId())) {
// Do not destroy on the way out of display range. The client keeps the entity until
// the chunk unloads, and destroying it would blank the table for anyone still in view.
if (respawnFor(inRange(loc, token.origin), token.viewers.contains(viewer.getUniqueId()), false)) {
show(viewer, token);
}
}
}

private void syncChunk(Player viewer, World world, int chunkX, int chunkZ, boolean loaded) {
if (viewer == null || !viewer.isOnline() || world == null || !ProtocolLibBridge.isReady()) {
return;
}
for (Token token : tokens.values()) {
if (!inChunk(token, world, chunkX, chunkZ)) {
continue;
}
boolean tracking = token.viewers.contains(viewer.getUniqueId());
boolean near = inRange(viewer.getLocation(), token.origin);
if (!loaded || hideFor(near, tracking)) {
hide(viewer, token);
continue;
}
if (respawnFor(near, tracking, true)) {
reshow(viewer, token);
}
}
}

/** True when this viewer needs a fresh spawn packet, not a metadata tweak. */
static boolean respawnFor(boolean inRange, boolean tracking, boolean chunkReloaded) {
return inRange && (!tracking || chunkReloaded);
}

/** True when a tracked viewer is too far to be shown the chunk again. */
static boolean hideFor(boolean inRange, boolean tracking) {
return tracking && !inRange;
}

private static boolean inChunk(Token token, World world, int chunkX, int chunkZ) {
Location origin = token.origin;
if (origin == null || origin.getWorld() == null || !origin.getWorld().equals(world)) {
return false;
}
return (origin.getBlockX() >> 4) == chunkX && (origin.getBlockZ() >> 4) == chunkZ;
}

private void reshow(Player viewer, Token token) {
if (!ProtocolLibBridge.isReady()) {
return;
}
hide(viewer, token);
show(viewer, token);
}

private void hide(Player viewer, Token token) {
if (viewer == null || token == null || !token.viewers.remove(viewer.getUniqueId())) {
return;
}
if (!ProtocolLibBridge.isReady()) {
return;
}
ProtocolLibBridge.getPackets().destroy(viewer, List.of(token.entityId));
}

private void hideAll(Player viewer) {
List<Integer> ids = new ArrayList<>();
for (Token token : tokens.values()) {
Expand Down
26 changes: 25 additions & 1 deletion src/main/java/net/tfminecraft/games/game/BlackjackGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ private static String formatPileTotal(List<HandCard> cards) {

@Override
public void onSessionStart(Table table) {
cancelBetTimer(table);
if (table.shufflePolicy() == ShufflePolicy.ROUND) {
TableManager.get().reshuffleFull(table);
}
Expand Down Expand Up @@ -366,6 +367,23 @@ public void onChipIn(Table table, Player player) {
startBetTimer(table);
}

/**
* An auto table with a legal bet and no running clock never deals. A dealer claim, a leave,
* or a restart can cancel the countdown and leave the chips sitting there.
*/
static boolean needsBetClock(boolean live, boolean auto, boolean betOpen, boolean frozen,
boolean clockRunning, boolean legalBet) {
return !live && auto && betOpen && !frozen && !clockRunning && legalBet;
}

@Override
public void onClock(Table table) {
if (needsBetClock(table.live(), auto(table), table.betOpen(), GuildTables.frozen(table),
betTimers.containsKey(table.getId()), TableManager.get().hasLegalBlackjackBox(table))) {
startBetTimer(table);
}
}

@Override
public void onLeave(Table table, Player player) {
UUID id = player.getUniqueId();
Expand Down Expand Up @@ -667,9 +685,15 @@ private void prepareIdle(Table table) {
return;
}
table.setBetOpen(true);
table.setAutoCountdown(0);
if (!betTimers.containsKey(table.getId())) {
table.setAutoCountdown(0);
}
TableManager.get().refreshLabel(table);
syncTrayHolo(table);
if (needsBetClock(false, true, true, false, betTimers.containsKey(table.getId()),
TableManager.get().hasLegalBlackjackBox(table))) {
startBetTimer(table);
}
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/net/tfminecraft/games/game/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ default void onDealerGone(Table table) {
onTableReady(table);
}

/**
* Once a second. Blackjack restarts a bet window whose clock was lost.
*/
default void onClock(Table table) {}

/**
* Whether this player may put money on the felt right now. Games that deal a hand to a fixed
* set of seats say no to anyone not in the live hand, so a late stake cannot buy into a hand
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/net/tfminecraft/games/table/TableManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ private void tickHands() {
handTicks++;
for (Table table : tables.values()) {
tickAway(table);
if (handTicks % 20 == 0) {
Game clock = gameOf(table);
if (clock != null) {
clock.onClock(table);
}
}
Map<UUID, List<HandCard>> hands = table.getHands();
if (hands.isEmpty()) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.inventory.EquipmentSlot;
Expand All @@ -26,6 +27,8 @@
import org.mockbukkit.mockbukkit.world.WorldMock;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import io.papermc.paper.event.packet.PlayerChunkLoadEvent;
import io.papermc.paper.event.packet.PlayerChunkUnloadEvent;
import net.tfminecraft.games.Games;
import net.tfminecraft.games.Messages;
import net.tfminecraft.games.cache.Cache;
Expand Down Expand Up @@ -212,10 +215,52 @@
ChunkLoadEvent event = new ChunkLoadEvent(world.getChunkAt(0, 0), false);
clearInvocations(packets);
manager.onChunkLoad(event);
verify(packets).update(eq(owner), anyInt(), eq(back), any(), eq(0), eq(0));
verify(packets).update(eq(observer), anyInt(), eq(back), any(), eq(0), eq(0));
verify(packets).destroy(eq(owner), anyList());
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(back), any());
verify(packets).destroy(eq(observer), anyList());
verify(packets).spawn(eq(observer), anyInt(), any(), eq(origin), eq(back), any());
verify(packets).spawn(eq(distant), anyInt(), any(), eq(origin), eq(back), any());
verify(packets, times(1)).spawn(any(), anyInt(), any(), any(), any(), any());
verify(packets, never()).update(any(), anyInt(), any(), any(), anyInt(), anyInt());
}

@Test
void returningToAChunkRespawnsCardsTheClientDropped() {
manager.spawn(token, origin, back, null);
clearInvocations(packets);
manager.onPlayerChunkLoad(new PlayerChunkLoadEvent(world.getChunkAt(0, 0), owner));
verify(packets).destroy(eq(owner), anyList());
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(back), any());
verify(packets, never()).update(eq(owner), anyInt(), any(), any(), anyInt(), anyInt());
}

@Test
void leavingAChunkForgetsTheViewerUntilTheyLoadItAgain() {
manager.spawn(token, origin, back, null);
clearInvocations(packets);
manager.onPlayerChunkUnload(new PlayerChunkUnloadEvent(world.getChunkAt(0, 0), owner));
verify(packets).destroy(eq(owner), anyList());
clearInvocations(packets);
manager.onPlayerChunkLoad(new PlayerChunkLoadEvent(world.getChunkAt(0, 0), owner));
verify(packets, never()).destroy(eq(owner), anyList());
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(back), any());
}

@Test
void walkingBackIntoRangeSpawnsACardThatWasNeverTracked() {
manager.spawn(token, origin, back, null);
distant.teleport(origin);
clearInvocations(packets);
manager.onMove(new PlayerMoveEvent(distant, origin.clone().add(11, 0, 0), origin));
verify(packets).spawn(eq(distant), anyInt(), any(), eq(origin), eq(back), any());
}

@Test
void walkingOutOfRangeLeavesTheClientEntityAlone() {
manager.spawn(token, origin, back, null);
clearInvocations(packets);
Location far = origin.clone().add(11, 0, 0);
manager.onMove(new PlayerMoveEvent(owner, origin, far));
verify(packets, never()).destroy(eq(owner), anyList());
}

@Test
Expand Down Expand Up @@ -253,10 +298,10 @@
manager.setItemFor(token, owner, new ItemStack(Material.DIAMOND));
clearInvocations(packets);
bridge.when(ProtocolLibBridge::isReady).thenReturn(false);
manager.onQuit(new PlayerQuitEvent(owner, "quit"));

Check warning on line 301 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal
verifyNoInteractions(packets);
bridge.when(ProtocolLibBridge::isReady).thenReturn(true);
manager.onJoin(new PlayerJoinEvent(owner, "join"));

Check warning on line 304 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
MockBukkit.getMock().getScheduler().performOneTick();
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(back), any());
assertEquals(origin, manager.worldLocation(token));
Expand All @@ -265,7 +310,7 @@
@Test
void tokenSpawnedByALaterQuitListenerIsShownAgainWhenThatPlayerReconnects() {
manager.spawn(token, origin, back, null);
manager.onQuit(new PlayerQuitEvent(owner, "quit"));

Check warning on line 313 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal
clearInvocations(packets);
// Another quit listener, such as a table mucking the leaving player's hand, rebuilds a pile while
// the player is still listed as online.
Expand All @@ -281,7 +326,7 @@
owner.reconnect();
owner.teleport(origin);
clearInvocations(packets);
manager.onJoin(new PlayerJoinEvent(owner, "join"));

Check warning on line 329 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
MockBukkit.getMock().getScheduler().performOneTick();
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(back), any());
verify(packets).spawn(eq(owner), anyInt(), any(), eq(origin), eq(new ItemStack(Material.BOOK)), any());
Expand All @@ -289,7 +334,7 @@

@Test
void departedViewerIsSkippedWhenTheTokenIsDestroyed() {
manager.onQuit(new PlayerQuitEvent(owner, "quit"));

Check warning on line 337 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal
manager.spawn(token, origin, back, null);
ArgumentCaptor<Integer> entity = ArgumentCaptor.forClass(Integer.class);
verify(packets).spawn(eq(observer), entity.capture(), any(), any(), any(), any());
Expand All @@ -303,7 +348,7 @@
@Test
void tokenShownBeforeTheDelayedJoinRefreshIsNotSentTwice() {
distant.teleport(origin);
manager.onJoin(new PlayerJoinEvent(distant, "join"));

Check warning on line 351 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
manager.spawn(token, origin, back, null);
verify(packets).spawn(eq(distant), anyInt(), any(), eq(origin), eq(back), any());
clearInvocations(packets);
Expand All @@ -320,7 +365,7 @@
Location near = origin.clone().add(11, 0, 5);
manager.spawn(token, near, back, null);
clearInvocations(packets);
manager.onJoin(new PlayerJoinEvent(distant, "join"));

Check warning on line 368 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
MockBukkit.getMock().getScheduler().performOneTick();
verify(packets).spawn(eq(distant), anyInt(), any(), eq(near), any(), any());
verify(packets, times(1)).spawn(any(), anyInt(), any(), any(), any(), any());
Expand Down Expand Up @@ -363,12 +408,12 @@
void pendingJoinDoesNotSendPacketsAfterDisconnectOrProtocolShutdown() {
manager.spawn(token, origin, back, null);
distant.teleport(origin);
manager.onJoin(new PlayerJoinEvent(distant, "join"));

Check warning on line 411 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
distant.disconnect();
clearInvocations(packets);
MockBukkit.getMock().getScheduler().performOneTick();
verifyNoInteractions(packets);
manager.onJoin(new PlayerJoinEvent(owner, "join"));

Check warning on line 416 in src/test/java/net/tfminecraft/games/display/DisplayManagerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerJoinEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerJoinEvent has been deprecated and marked for removal
bridge.when(ProtocolLibBridge::isReady).thenReturn(false);
MockBukkit.getMock().getScheduler().performOneTick();
verifyNoInteractions(packets);
Expand Down
25 changes: 25 additions & 0 deletions src/test/java/net/tfminecraft/games/game/BlackjackGameTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,30 @@ void resultDelayDefersBustSettlementAndIgnoresRemovedTable() {
verify(wagers, never()).announceWins(any(), anyString());
}

@Test
void openAutomaticWindowWithALegalBetStartsItsClockWithoutAnotherChip() {
table.setDealerId(null);
table.setAutoDealer(true);
game.onTableReady(table);
assertEquals(2, table.autoCountdown());
server.getScheduler().performTicks(20);
assertEquals(1, table.autoCountdown());
}

@Test
void lostBetClockRestartsWhileALegalBetIsStillOpen() {
table.setDealerId(null);
table.setAutoDealer(true);
table.setBetOpen(true);
assertEquals(0, table.autoCountdown());
game.onClock(table);
assertEquals(2, table.autoCountdown());
server.getScheduler().performTicks(20);
assertEquals(1, table.autoCountdown());
game.onClock(table);
assertEquals(1, table.autoCountdown(), "a running window must not start over");
}

@Test
void automaticBetTimerStartsOnlyOnceAndClosesBeforeAutomaticDeal() {
table.setDealerId(null);
Expand Down Expand Up @@ -886,6 +910,7 @@ void uncoveredChipIsRefundedExactlyAndDoesNotStartCountdown() {
table.setDealerId(null);
table.setAutoDealer(true);
fundingAccepted = false;
bets.clear();
game.onTableReady(table);
bets.put(player.getUniqueId(), 27);
game.onChipIn(table, player, 7, new ItemStack(Material.GOLD_NUGGET));
Expand Down
Loading