From 49704f8783dd9b85c88b5602a9dc6d1e41be4144 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sun, 6 Sep 2026 15:10:10 +0200 Subject: [PATCH] fix(spectator): rebuild the overview instead of showing an empty grid The overview listed no survivors at all. Its inventory is built once, at server start, when the survivor team is still empty, and the head list was never rebuilt afterwards: invalidating the data layout only clears a flag while nobody has the inventory open, and the following getInventory skips the rebuild because it retrieves a data layout only while it applies the design layout as well. Invalidating both layouts on open and on every roster change fixes that and also clears the inventory, which a data layout cannot do on its own - the head of a player who left the round would otherwise stay in place and stay clickable. --- .../cygnus/spectator/SpectatorInventory.java | 26 +++++++++++ .../cygnus/spectator/SpectatorService.java | 6 +-- .../spectator/SpectatorInventoryTest.java | 46 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorInventory.java b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorInventory.java index 85459d22..3c84e802 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorInventory.java +++ b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorInventory.java @@ -23,6 +23,13 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +/** + * The overview a spectator uses to jump to a player who is still in the round. + * + * @author theEvilReaper + * @version 1.1.0 + * @since 2.7.0 + */ public class SpectatorInventory extends GlobalInventoryBuilder { private static final ItemStack DECORATION_PANE = ItemStack.builder(Material.BLACK_STAINED_GLASS_PANE) @@ -114,10 +121,29 @@ void handleClick(Player player, int slot, Click click, ItemStack stack, Consumer /** * Opens the inventory for a specific {@link Player}. + *

+ * Refreshes first, so the spectator always sees the survivors that are alive right now. * * @param player who should get it */ public void open(Player player) { + this.refresh(); player.openInventory(getInventory()); } + + /** + * Marks both layouts as stale so the next inventory update rebuilds the head list from the survivor team. + *

+ * Invalidating the data layout alone is not enough for two reasons. The inventory is built once, at + * server start, when the survivor team is still empty, and {@code invalidateDataLayout} only clears the + * valid flag while nobody is looking at the inventory - the following {@code getInventory} then skips the + * rebuild, because it only retrieves a data layout while it is applying the design layout as well. The + * head list therefore stayed at the empty one from server start for the whole round. Invalidating the + * design layout too is also what clears the inventory: a data layout can only write items, so the head of + * a player who left the round would otherwise stay in place. + */ + public void refresh() { + this.invalidateLayout(); + this.invalidateDataLayout(); + } } diff --git a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java index c4ef942a..6cd6e059 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java +++ b/game/src/main/java/net/onelitefeather/cygnus/spectator/SpectatorService.java @@ -24,7 +24,7 @@ * the spectate-overview GUI, and leaving spectator mode. * * @author theEvilReaper - * @version 1.0.0 + * @version 1.1.0 * @since 2.7.0 */ public final class SpectatorService { @@ -110,9 +110,9 @@ public boolean isSpectator(Player player) { } /** - * Invalidates the spectator inventory's data layout. + * Rebuilds the spectator inventory so its head list matches the players still in the round. */ public void updateInventory() { - this.spectatorInventory.invalidateDataLayout(); + this.spectatorInventory.refresh(); } } diff --git a/game/src/test/java/net/onelitefeather/cygnus/spectator/SpectatorInventoryTest.java b/game/src/test/java/net/onelitefeather/cygnus/spectator/SpectatorInventoryTest.java index 733c9350..d72525aa 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/spectator/SpectatorInventoryTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/spectator/SpectatorInventoryTest.java @@ -98,4 +98,50 @@ void testOpenPopulatesPlayerHeadsEvenWhenSkinIsNull(@NotNull Env env) { env.destroyInstance(instance, true); } + + @Test + void testOpenPicksUpSurvivorsThatJoinedAfterTheInventoryWasBuilt(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player spectator = env.createPlayer(instance); + Team survivorTeam = Team.of(GameConfig.SURVIVOR_KEY, 5); + + // The production instance is built at server start, when nobody is on the survivor team yet. + SpectatorInventory inventory = new SpectatorInventory(survivorTeam, (_, _) -> {}); + env.tick(); + + Player survivor = env.createPlayer(instance); + survivorTeam.addPlayer(survivor); + + inventory.open(spectator); + env.tick(); + + ItemStack item = inventory.getInventory().getItemStack(9); + assertEquals(Material.PLAYER_HEAD, item.material()); + assertEquals(survivor.getUuid(), item.getTag(SpectatorInventory.TARGET_TAG)); + + env.destroyInstance(instance, true); + } + + @Test + void testOpenDropsHeadsOfPlayersThatLeftTheSurvivorTeam(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player spectator = env.createPlayer(instance); + Player survivor = env.createPlayer(instance); + Team survivorTeam = Team.of(GameConfig.SURVIVOR_KEY, 5); + survivorTeam.addPlayer(survivor); + + SpectatorInventory inventory = new SpectatorInventory(survivorTeam, (_, _) -> {}); + inventory.open(spectator); + env.tick(); + assertEquals(Material.PLAYER_HEAD, inventory.getInventory().getItemStack(9).material()); + + survivorTeam.removePlayer(survivor); + + inventory.open(spectator); + env.tick(); + + assertTrue(inventory.getInventory().getItemStack(9).isAir(), "a player who left the round must not stay listed"); + + env.destroyInstance(instance, true); + } }