Make TechHider hidden regions consistent for spectators

Spectators hid HiddenBlocks in the whole arena for block updates, but
full chunk packets outside both extend regions were sent unfiltered, so
the same blocks were hidden or visible depending on the packet type.

Hidden regions are now a list: spectators hide both team extend
regions, team members the opposite one. Block checks and the per-chunk
bypass use the same regions, so everything outside the extend regions
is visible and unfiltered.
This commit is contained in:
2026-10-27 09:58:16 +01:00
parent 7cd2e540ef
commit b47cff2805
4 changed files with 42 additions and 25 deletions
@@ -60,7 +60,7 @@ public class TechhiderbugCommand extends FightSWCommand {
writer.append('\n');
writer.append("Hidden regions: ");
FightSystem.getTechHider().getHiddenRegion().forEach((p, r) -> writer.append(p.getName()).append(':').append(r.toString()).append(' '));
FightSystem.getTechHider().getHiddenRegion().forEach((p, r) -> writer.append(p.getName()).append(':').append(Arrays.toString(r)).append(' '));
writer.append('\n');
writer.append("TinyProtocol: ");
@@ -51,6 +51,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
@@ -62,8 +63,10 @@ public class TechHiderWrapper extends StateDependent implements Listener {
public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.GameModeConfig.Techhider.Active;
private static final Region[] NO_REGIONS = new Region[0];
@Getter
private final ConcurrentHashMap<Player, Region> hiddenRegion = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Player, Region[]> hiddenRegion = new ConcurrentHashMap<>();
private final HullHider hullHider;
public TechHiderWrapper(HullHider hullHider) {
@@ -97,14 +100,9 @@ public class TechHiderWrapper extends StateDependent implements Listener {
private AccessPrivilegeProvider newProvider(Set<BlockState> blockStatesToObfuscate, Set<Block> blocksToObfuscate, Set<BlockEntityType<?>> blockEntityTypeToObfuscate) {
return new AccessPrivilegeProvider() {
@Override
public boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ) {
return Fight.teams().stream().map(FightTeam::getExtendRegion).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
}
@Override
public ForPlayer forPlayer(Player p) {
return new FightPlayerAccess(p, getHiddenRegion(p), blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate);
return new FightPlayerAccess(p, getHiddenRegions(p), blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate);
}
@Override
@@ -119,24 +117,37 @@ public class TechHiderWrapper extends StateDependent implements Listener {
*/
private class FightPlayerAccess implements ForPlayer {
private final Player player;
private final Region hiddenRegion;
private final Region[] hiddenRegions;
private final Set<BlockState> blockStatesToObfuscate;
private final Set<Block> blocksToObfuscate;
private final Set<BlockEntityType<?>> blockEntityTypeToObfuscate;
private FightPlayerAccess(Player player, Region hiddenRegion, Set<BlockState> blockStatesToObfuscate, Set<Block> blocksToObfuscate, Set<BlockEntityType<?>> blockEntityTypeToObfuscate) {
private FightPlayerAccess(Player player, Region[] hiddenRegions, Set<BlockState> blockStatesToObfuscate, Set<Block> blocksToObfuscate, Set<BlockEntityType<?>> blockEntityTypeToObfuscate) {
this.player = player;
this.hiddenRegion = hiddenRegion;
this.hiddenRegions = hiddenRegions;
this.blockStatesToObfuscate = blockStatesToObfuscate;
this.blocksToObfuscate = blocksToObfuscate;
this.blockEntityTypeToObfuscate = blockEntityTypeToObfuscate;
}
private boolean isHidden(int blockX, int blockY, int blockZ) {
for (Region region : hiddenRegions) {
if (region.inRegion(blockX, blockY, blockZ)) return true;
}
return false;
}
@Override
public Player getPlayer() {
return player;
}
@Override
public boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ) {
// Hulls the player may not see lie within the hidden regions, everything outside of them is not critical
return Arrays.stream(hiddenRegions).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
}
@Override
public boolean canAccessPosition(int blockX, int blockY, int blockZ) {
return !hullHider.isBlockHidden(player, blockX, blockY, blockZ);
@@ -144,12 +155,12 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@Override
public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) {
return !hiddenRegion.inRegion(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
return !isHidden(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
}
@Override
public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) {
return !hiddenRegion.inRegion(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
return !isHidden(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
}
// TODO will require entity tracking on the netty thread to prevent future race conditions
@@ -166,7 +177,7 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@Override
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !hiddenRegion.inRegion(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
return !isHidden(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
}
}
@@ -177,19 +188,19 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@EventHandler
public void teamJoin(TeamSpawnEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
}
@EventHandler
public void boarding(BoardingEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
}
@EventHandler
public void teamLeave(TeamLeaveEvent e) {
e.getFightPlayer().ifPlayer(player -> {
if (player.isOnline()) {
hiddenRegion.put(player, getHiddenRegion(player));
hiddenRegion.put(player, getHiddenRegions(player));
}
});
}
@@ -210,16 +221,19 @@ public class TechHiderWrapper extends StateDependent implements Listener {
});
}
private Region getHiddenRegion(Player player) {
if (Config.isReferee(player)) return Region.EMPTY;
private Region[] getHiddenRegions(Player player) {
if (Config.isReferee(player)) return NO_REGIONS;
// Single lookup: runs on netty threads, the player can leave the team at any time
FightPlayer fightPlayer = Fight.getFightPlayer(player);
if (fightPlayer == null) return Config.ArenaRegion;
if (fightPlayer == null) {
// Spectators: both teams, the rest of the arena contains nothing critical
return Fight.teams().stream().map(FightTeam::getExtendRegion).toArray(Region[]::new);
}
if (fightPlayer.canEntern()) return Region.EMPTY;
if (fightPlayer.canEntern()) return NO_REGIONS;
return Fight.getOpposite(fightPlayer.getTeam()).getExtendRegion();
return new Region[]{Fight.getOpposite(fightPlayer.getTeam()).getExtendRegion()};
}
private Stream<BlockState> getWaterloggedBlockStates() {
@@ -25,8 +25,6 @@ import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
public interface AccessPrivilegeProvider {
boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ);
/**
* Resolves everything that can change concurrently about this player once.
* The result is used for all decisions of one outgoing packet (including all packets of a bundle).
@@ -41,6 +39,11 @@ public interface AccessPrivilegeProvider {
interface ForPlayer {
Player getPlayer();
/**
* @return true if nothing within this chunk is restricted for this player, so the chunk may be sent unmodified
*/
boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ);
boolean canAccessPosition(int blockX, int blockY, int blockZ);
boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block);
@@ -530,7 +530,7 @@ public class TechHider {
}
private ClientboundLevelChunkWithLightPacket processChunkWithLight(ForPlayer access, ClientboundLevelChunkWithLightPacket packet) {
if (privilegeProvider.isEveryonePrivilegedToAccessAllDataWithinChunk(packet.getX(), packet.getZ())) {
if (access.canAccessAllDataWithinChunk(packet.getX(), packet.getZ())) {
return packet;
} else {
return chunkHider.processLevelChunkWithLightPacket(access, packet);