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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -114,10 +121,29 @@ void handleClick(Player player, int slot, Click click, ItemStack stack, Consumer

/**
* Opens the inventory for a specific {@link Player}.
* <p>
* 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.
* <p>
* 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();
}
Comment on lines +145 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a hotfix for the inventory update and does not address the underlying issues.

With this approach, every time a player opens the inventory, all items aligned with the background and data layers are recalculated. A proper fix would be to identify the specific positions where the data layout needs to be invalidated and only recalculate those positions when necessary, rather than triggering a full recalculation on every inventory open.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading