Hide hulls from players by default in HullHider

Hull kept a set of affected players that was only filled by a join
listener active in the Schem states, so spectators joining earlier were
never hidden from the hull. The set was also a plain HashSet read on
netty threads, where a racing lookup meant "not hidden".

Invert it into a concurrent set of viewers (referees, own team, boarded
players); everyone else is hidden by default.

- Hull: viewer UUID set with addViewer/removeViewer/forgetViewer, a
  null player counts as hidden
- Hull: entities are hidden/shown when they enter/leave the hull for all
  non-viewer online players; uncovered blocks go to non-viewers
- HullHider: join/quit handled in a listener active in every state; quit
  clears the viewer entry
- HullHider: TeamDeathEvent re-hides the enemy hull and resends its chunks
- FightPlayer: stop the enter countdown before firing TeamDeathEvent so
  listeners see canEntern() == false
This commit is contained in:
2026-10-27 09:23:31 +01:00
parent 2ea09eadb2
commit 0ef702da9e
3 changed files with 59 additions and 28 deletions
@@ -69,8 +69,8 @@ public class FightPlayer {
public void setOut() {
isOut = true;
stopEnternCountdown(); // before the event, so listeners already see canEntern() == false
Bukkit.getPluginManager().callEvent(new TeamDeathEvent(this));
stopEnternCountdown();
ifAI(AI::stop);
}
@@ -34,6 +34,7 @@ import net.minecraft.core.SectionPos;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -43,6 +44,7 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.stream.IntStream;
@@ -65,7 +67,8 @@ public class Hull {
private final HashMap<IntVector, BitSet> visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block
private final HashSet<IntVector> uncoveredSurface = new HashSet<>();
private final HashSet<Player> players = new HashSet<>();
// Players that may see the hull contents (viewers). Everybody else is hidden from it (default deny). Concurrent as it is read on netty threads.
private final Set<UUID> viewers = ConcurrentHashMap.newKeySet();
private final HashSet<Entity> entities = new HashSet<>();
private final HashSet<REntity> rentities = new HashSet<>();
@@ -87,27 +90,36 @@ public class Hull {
}
public boolean blockPrecise(Player player, int chunkX, int chunkY, int chunkZ) {
return players.contains(player) && !region.chunkSectionOutside(chunkX, chunkY, chunkZ);
return !isViewer(player) && !region.chunkSectionOutside(chunkX, chunkY, chunkZ);
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
return region.inRegion(x, y, z) && players.contains(player) && !visibility.get(((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()));
return region.inRegion(x, y, z) && !isViewer(player) && !visibility.get(((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()));
}
public boolean isLocationHidden(Player player, Location location) {
return players.contains(player) && region.inRegion(location) && !visibility.get(new IntVector(location).toId(region));
return !isViewer(player) && region.inRegion(location) && !visibility.get(new IntVector(location).toId(region));
}
public void addPlayer(Player player) {
if (players.add(player)) {
for (Entity entity : entities) {
untrackEntity(player, entity);
}
private boolean isViewer(Player player) {
return player != null && viewers.contains(player.getUniqueId());
}
/**
* Hides the hull from this player (removes the viewer), also hiding all entities currently inside it.
*/
public void removeViewer(Player player) {
viewers.remove(player.getUniqueId());
for (Entity entity : entities) {
if (!player.equals(entity)) untrackEntity(player, entity);
}
}
public void removePlayer(Player player, boolean activeRemoval) {
if (players.remove(player) && activeRemoval) {
/**
* Allows this player to see the hull (adds a viewer).
*/
public void addViewer(Player player) {
if (viewers.add(player.getUniqueId())) {
for (Entity entity : entities) {
trackEntity(player, entity);
}
@@ -115,18 +127,22 @@ public class Hull {
}
}
public void forgetViewer(Player player) {
viewers.remove(player.getUniqueId());
}
public void checkEntity(Entity entity) {
Location location = entity.getLocation();
if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
if (entities.add(entity)) {
for (Player player : players) {
untrackEntity(player, entity);
for (Player player : Bukkit.getOnlinePlayers()) {
if (!isViewer(player) && !player.equals(entity)) untrackEntity(player, entity);
}
}
} else {
if (entities.remove(entity)) {
for (Player player : players) {
trackEntity(player, entity);
for (Player player : Bukkit.getOnlinePlayers()) {
if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity);
}
}
}
@@ -252,7 +268,7 @@ public class Hull {
result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]);
}
Object packet = result;
players.forEach(player -> TinyProtocol.instance.sendPacket(player, packet));
Bukkit.getOnlinePlayers().stream().filter(player -> !isViewer(player)).forEach(player -> TinyProtocol.instance.sendPacket(player, packet));
}
}
@@ -21,6 +21,8 @@ package de.steamwar.fightsystem.utils;
import de.steamwar.entity.REntity;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.events.TeamDeathEvent;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
@@ -61,6 +63,7 @@ public class HullHider implements Listener {
hulls = hullMap.values().toArray(new Hull[0]);
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener());
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, this);
new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1);
}
@@ -75,14 +78,26 @@ public class HullHider implements Listener {
hullMap.get(team).fill(visible);
}
@EventHandler(priority = EventPriority.HIGH)
public void onJoin(PlayerJoinEvent e) {
updatePlayer(e.getPlayer());
}
// Registered in every state: players must be hidden from the hulls by default, no matter when they join.
private class MembershipListener implements Listener {
@EventHandler(priority = EventPriority.HIGH)
public void onJoin(PlayerJoinEvent e) {
updatePlayer(e.getPlayer());
}
@EventHandler
public void onLeave(PlayerQuitEvent e) {
removePlayer(e.getPlayer(), false);
@EventHandler
public void onLeave(PlayerQuitEvent e) {
forgetViewer(e.getPlayer());
}
// A dead player loses the right to see the enemy hull again
@EventHandler
public void onTeamDeath(TeamDeathEvent e) {
e.getFightPlayer().ifPlayer(player -> {
updatePlayer(player);
FightSystem.getTechHider().reloadChunks(player, Fight.getOpposite(e.getFightPlayer().getTeam()).getExtendRegion(), Region.EMPTY);
});
}
}
public void updatePlayer(Player player) {
@@ -92,16 +107,16 @@ public class HullHider implements Listener {
FightPlayer fp = Fight.getFightPlayer(player);
for (Map.Entry<FightTeam, Hull> hull : hullMap.entrySet()) {
if (Config.isReferee(player) || hull.getKey() == team || (fp != null && fp.canEntern())) {
hull.getValue().removePlayer(player, true);
hull.getValue().addViewer(player);
} else {
hull.getValue().addPlayer(player);
hull.getValue().removeViewer(player);
}
}
}
private void removePlayer(Player player, boolean activeRemoval) {
private void forgetViewer(Player player) {
for (Hull hull : hulls) {
hull.removePlayer(player, activeRemoval);
hull.forgetViewer(player);
}
}