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); + } }