Merge pull request 'Hull hider fixes' (#517) from HullHiderFixes into main

Reviewed-on: SteamWar/SteamWar#517
This commit is contained in:
2026-10-31 12:53:16 +01:00
9 changed files with 302 additions and 185 deletions
@@ -34,9 +34,11 @@ import org.bukkit.util.Vector;
import java.io.File; import java.io.File;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
public class Config { public class Config {
@@ -80,7 +82,7 @@ public class Config {
//event parameter //event parameter
public static final EventFight EventKampf; public static final EventFight EventKampf;
private static final Set<Integer> Referees; private static final Set<UUID> Referees;
public static final int EventTeamBlueID; public static final int EventTeamBlueID;
public static final int EventTeamRedID; public static final int EventTeamRedID;
public static final boolean BothTeamsPublic; public static final boolean BothTeamsPublic;
@@ -237,7 +239,11 @@ public class Config {
BothTeamsPublic = EventTeamRedID == 0 && EventTeamBlueID == 0; BothTeamsPublic = EventTeamRedID == 0 && EventTeamBlueID == 0;
SpectatePort = EventKampf.getSpectatePort() == null ? 0 : EventKampf.getSpectatePort(); SpectatePort = EventKampf.getSpectatePort() == null ? 0 : EventKampf.getSpectatePort();
LiveReplay = SpectatePort != 0; LiveReplay = SpectatePort != 0;
Referees = Referee.get(Config.EventKampf.getEventID()); Referees = Referee.get(Config.EventKampf.getEventID()).stream()
.map(SteamwarUser::byId)
.filter(Objects::nonNull)
.map(SteamwarUser::getUUID)
.collect(Collectors.toUnmodifiableSet());
Event event = Event.byId(EventKampf.getEventID()); Event event = Event.byId(EventKampf.getEventID());
if (BothTeamsPublic) { if (BothTeamsPublic) {
@@ -305,6 +311,6 @@ public class Config {
} }
public static boolean isReferee(Player player) { public static boolean isReferee(Player player) {
return Referees.contains(SteamwarUser.get(player.getUniqueId()).getId()); return Referees.contains(player.getUniqueId());
} }
} }
@@ -60,7 +60,7 @@ public class TechhiderbugCommand extends FightSWCommand {
writer.append('\n'); writer.append('\n');
writer.append("Hidden regions: "); 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('\n');
writer.append("TinyProtocol: "); writer.append("TinyProtocol: ");
@@ -69,8 +69,8 @@ public class FightPlayer {
public void setOut() { public void setOut() {
isOut = true; isOut = true;
stopEnternCountdown(); // before the event, so listeners already see canEntern() == false
Bukkit.getPluginManager().callEvent(new TeamDeathEvent(this)); Bukkit.getPluginManager().callEvent(new TeamDeathEvent(this));
stopEnternCountdown();
ifAI(AI::stop); 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.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Bukkit;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
@@ -43,6 +44,7 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.IntStream; 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 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<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<Entity> entities = new HashSet<>();
private final HashSet<REntity> rentities = new HashSet<>(); private final HashSet<REntity> rentities = new HashSet<>();
@@ -75,10 +78,8 @@ public class Hull {
this.occluding = new BitSet(region.volume()); this.occluding = new BitSet(region.volume());
this.visibility = new BitSet(region.volume()); this.visibility = new BitSet(region.volume());
IntVector primaryDirection = new IntVector(0, 0, team.isBlue() == (Config.BlueToRedZ > 0) ? -1 : 1);
directions = IntStream.range(0, 27) directions = IntStream.range(0, 27)
.mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1)) .mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1))
.filter(v -> v.sqLength() == 1 || v.x * primaryDirection.x + v.y * primaryDirection.y + v.z * primaryDirection.z == 1) // Not pointing away from primary direction
.toArray(IntVector[]::new); .toArray(IntVector[]::new);
for (IntVector direction : directions) { for (IntVector direction : directions) {
@@ -86,28 +87,29 @@ public class Hull {
} }
} }
public boolean blockPrecise(Player player, int chunkX, int chunkY, int chunkZ) {
return players.contains(player) && !region.chunkSectionOutside(chunkX, chunkY, chunkZ);
}
public boolean isBlockHidden(Player player, int x, int y, int z) { 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) { public boolean isViewer(Player player) {
return players.contains(player) && region.inRegion(location) && !visibility.get(new IntVector(location).toId(region)); return player != null && viewers.contains(player.getUniqueId());
} }
public void addPlayer(Player player) { /**
if (players.add(player)) { * Hides the hull from this player (removes the viewer), also hiding all entities currently inside it.
for (Entity entity : entities) { */
untrackEntity(player, entity); 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) { for (Entity entity : entities) {
trackEntity(player, entity); trackEntity(player, entity);
} }
@@ -115,18 +117,22 @@ public class Hull {
} }
} }
public void forgetViewer(Player player) {
viewers.remove(player.getUniqueId());
}
public void checkEntity(Entity entity) { public void checkEntity(Entity entity) {
Location location = entity.getLocation(); Location location = entity.getLocation();
if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) { if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
if (entities.add(entity)) { if (entities.add(entity)) {
for (Player player : players) { for (Player player : Bukkit.getOnlinePlayers()) {
untrackEntity(player, entity); if (!isViewer(player) && !player.equals(entity)) untrackEntity(player, entity);
} }
} }
} else { } else {
if (entities.remove(entity)) { if (entities.remove(entity)) {
for (Player player : players) { for (Player player : Bukkit.getOnlinePlayers()) {
trackEntity(player, entity); if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity);
} }
} }
} }
@@ -252,7 +258,7 @@ public class Hull {
result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]); result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]);
} }
Object packet = result; 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.entity.REntity;
import de.steamwar.fightsystem.Config; 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.Fight;
import de.steamwar.fightsystem.fight.FightPlayer; import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.fight.FightTeam;
@@ -29,6 +31,7 @@ import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.fightsystem.states.StateDependentTask;
import lombok.Getter; import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -61,13 +64,22 @@ public class HullHider implements Listener {
hulls = hullMap.values().toArray(new Hull[0]); hulls = hullMap.values().toArray(new Hull[0]);
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener());
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, this); new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, this);
new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1); new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1);
} }
public void initialize(FightTeam team) { public void initialize(FightTeam team) {
if (!TechHiderWrapper.ENABLED) return; if (!TechHiderWrapper.ENABLED) return;
hullMap.get(team).initialize(); Hull hull = hullMap.get(team);
hull.initialize();
// Chunks encoded while the hull was filled as hidden contain obfuscated blocks that are visible now, but no block update is generated for them
for (Player player : Bukkit.getOnlinePlayers()) {
if (!hull.isViewer(player)) {
FightSystem.getTechHider().reloadChunks(player, team.getExtendRegion(), Region.EMPTY);
}
}
} }
public void fill(FightTeam team, boolean visible) { public void fill(FightTeam team, boolean visible) {
@@ -75,14 +87,26 @@ public class HullHider implements Listener {
hullMap.get(team).fill(visible); hullMap.get(team).fill(visible);
} }
@EventHandler(priority = EventPriority.HIGH) // Registered in every state: players must be hidden from the hulls by default, no matter when they join.
public void onJoin(PlayerJoinEvent e) { private class MembershipListener implements Listener {
updatePlayer(e.getPlayer()); @EventHandler(priority = EventPriority.HIGH)
} public void onJoin(PlayerJoinEvent e) {
updatePlayer(e.getPlayer());
}
@EventHandler @EventHandler
public void onLeave(PlayerQuitEvent e) { public void onLeave(PlayerQuitEvent e) {
removePlayer(e.getPlayer(), false); 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) { public void updatePlayer(Player player) {
@@ -92,16 +116,16 @@ public class HullHider implements Listener {
FightPlayer fp = Fight.getFightPlayer(player); FightPlayer fp = Fight.getFightPlayer(player);
for (Map.Entry<FightTeam, Hull> hull : hullMap.entrySet()) { for (Map.Entry<FightTeam, Hull> hull : hullMap.entrySet()) {
if (Config.isReferee(player) || hull.getKey() == team || (fp != null && fp.canEntern())) { if (Config.isReferee(player) || hull.getKey() == team || (fp != null && fp.canEntern())) {
hull.getValue().removePlayer(player, true); hull.getValue().addViewer(player);
} else { } 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) { for (Hull hull : hulls) {
hull.removePlayer(player, activeRemoval); hull.forgetViewer(player);
} }
} }
@@ -25,11 +25,13 @@ import de.steamwar.fightsystem.events.BoardingEvent;
import de.steamwar.fightsystem.events.TeamLeaveEvent; import de.steamwar.fightsystem.events.TeamLeaveEvent;
import de.steamwar.fightsystem.events.TeamSpawnEvent; import de.steamwar.fightsystem.events.TeamSpawnEvent;
import de.steamwar.fightsystem.fight.Fight; import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState; import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependent; import de.steamwar.fightsystem.states.StateDependent;
import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.techhider.AccessPrivilegeProvider; import de.steamwar.techhider.AccessPrivilegeProvider;
import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer;
import de.steamwar.techhider.TechHider; import de.steamwar.techhider.TechHider;
import lombok.Getter; import lombok.Getter;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
@@ -49,6 +51,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@@ -60,8 +63,10 @@ public class TechHiderWrapper extends StateDependent implements Listener {
public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.GameModeConfig.Techhider.Active; public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.GameModeConfig.Techhider.Active;
private static final Region[] NO_REGIONS = new Region[0];
@Getter @Getter
private final ConcurrentHashMap<Player, Region> hiddenRegion = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Player, Region[]> hiddenRegion = new ConcurrentHashMap<>();
private final HullHider hullHider; private final HullHider hullHider;
public TechHiderWrapper(HullHider hullHider) { public TechHiderWrapper(HullHider hullHider) {
@@ -90,49 +95,90 @@ public class TechHiderWrapper extends StateDependent implements Listener {
.map(Holder.Reference::value) .map(Holder.Reference::value)
.collect(Collectors.toUnmodifiableSet()); .collect(Collectors.toUnmodifiableSet());
new TechHider(CraftMagicNumbers.getBlock(Config.GameModeConfig.Techhider.ObfuscateWith), new AccessPrivilegeProvider() { new TechHider(CraftMagicNumbers.getBlock(Config.GameModeConfig.Techhider.ObfuscateWith), newProvider(blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate));
@Override }
public boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ) {
return Fight.teams().stream().map(FightTeam::getExtendRegion).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
}
private AccessPrivilegeProvider newProvider(Set<BlockState> blockStatesToObfuscate, Set<Block> blocksToObfuscate, Set<BlockEntityType<?>> blockEntityTypeToObfuscate) {
return new AccessPrivilegeProvider() {
@Override @Override
public boolean isPlayerPrivilegedToAccessPosition(Player p, int blockX, int blockY, int blockZ) { public ForPlayer forPlayer(Player p) {
return !hullHider.isBlockHidden(p, blockX, blockY, blockZ); return new FightPlayerAccess(p, getHiddenRegions(p), blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate);
}
@Override
public boolean isPlayerPrivilegedToAccessBlock(Player p, int blockX, int blockY, int blockZ, Block block) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
}
@Override
public boolean isPlayerPrivilegedToAccessBlockState(Player p, int blockX, int blockY, int blockZ, BlockState blockState) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
}
// TODO will require entity tracking on the netty thread to prevent future race conditions
@Override
public boolean isPlayerPrivilegedToAccessEntity(Player p, int entityId) {
net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) p.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId);
if (nmsEntity != null) {
return !hullHider.isBlockHidden(p, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ());
} else {
return true;
}
}
@Override
public boolean isPlayerPrivilegedToAccessBlockEntity(Player p, int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
} }
@Override @Override
public boolean isPlayerPrivilegedToPerformAction(Player p) { public boolean isPlayerPrivilegedToPerformAction(Player p) {
return p.getGameMode() != GameMode.SPECTATOR; return p.getGameMode() != GameMode.SPECTATOR;
} }
}); };
}
/**
* Snapshot of what one player may access, taken once per outgoing packet on the netty thread.
*/
private class FightPlayerAccess implements ForPlayer {
private final Player player;
private final Region[] hiddenRegions;
private final Set<BlockState> blockStatesToObfuscate;
private final Set<Block> blocksToObfuscate;
private final Set<BlockEntityType<?>> blockEntityTypeToObfuscate;
private FightPlayerAccess(Player player, Region[] hiddenRegions, Set<BlockState> blockStatesToObfuscate, Set<Block> blocksToObfuscate, Set<BlockEntityType<?>> blockEntityTypeToObfuscate) {
this.player = player;
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);
}
@Override
public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) {
return !isHidden(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
}
@Override
public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) {
return !isHidden(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
}
// TODO will require entity tracking on the netty thread to prevent future race conditions
@Override
public boolean canAccessEntity(int entityId) {
net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) player.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId);
if (nmsEntity != null) {
return !hullHider.isBlockHidden(player, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ());
} else {
return true;
}
}
@Override
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !isHidden(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
}
} }
@Override @Override
@@ -142,19 +188,19 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@EventHandler @EventHandler
public void teamJoin(TeamSpawnEvent e) { public void teamJoin(TeamSpawnEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player))); e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
} }
@EventHandler @EventHandler
public void boarding(BoardingEvent e) { public void boarding(BoardingEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player))); e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
} }
@EventHandler @EventHandler
public void teamLeave(TeamLeaveEvent e) { public void teamLeave(TeamLeaveEvent e) {
e.getFightPlayer().ifPlayer(player -> { e.getFightPlayer().ifPlayer(player -> {
if (player.isOnline()) { if (player.isOnline()) {
hiddenRegion.put(player, getHiddenRegion(player)); hiddenRegion.put(player, getHiddenRegions(player));
} }
}); });
} }
@@ -175,15 +221,19 @@ public class TechHiderWrapper extends StateDependent implements Listener {
}); });
} }
private Region getHiddenRegion(Player player) { private Region[] getHiddenRegions(Player player) {
if (Config.isReferee(player)) return Region.EMPTY; if (Config.isReferee(player)) return NO_REGIONS;
FightTeam team = Fight.getPlayerTeam(player); // Single lookup: runs on netty threads, the player can leave the team at any time
if (team == null) return Config.ArenaRegion; FightPlayer fightPlayer = Fight.getFightPlayer(player);
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 (team.canPlayerEntern(player)) return Region.EMPTY; if (fightPlayer.canEntern()) return NO_REGIONS;
return Fight.getOpposite(team).getExtendRegion(); return new Region[]{Fight.getOpposite(fightPlayer.getTeam()).getExtendRegion()};
} }
private Stream<BlockState> getWaterloggedBlockStates() { private Stream<BlockState> getWaterloggedBlockStates() {
@@ -25,17 +25,33 @@ import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
public interface AccessPrivilegeProvider { public interface AccessPrivilegeProvider {
boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ); /**
* Resolves everything that can change concurrently about this player once.
boolean isPlayerPrivilegedToAccessPosition(Player p, int blockX, int blockY, int blockZ); * The result is used for all decisions of one outgoing packet (including all packets of a bundle).
*/
boolean isPlayerPrivilegedToAccessBlock(Player p, int blockX, int blockY, int blockZ, Block block); ForPlayer forPlayer(Player p);
boolean isPlayerPrivilegedToAccessBlockState(Player p, int blockX, int blockY, int blockZ, BlockState blockState);
boolean isPlayerPrivilegedToAccessEntity(Player p, int entityId);
boolean isPlayerPrivilegedToAccessBlockEntity(Player p, int blockX, int blockY, int blockZ, BlockEntityType<?> type);
boolean isPlayerPrivilegedToPerformAction(Player p); boolean isPlayerPrivilegedToPerformAction(Player p);
/**
* What a single player may access, valid for the processing of one outgoing packet.
*/
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);
boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState);
boolean canAccessEntity(int entityId);
boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type);
}
} }
@@ -19,6 +19,7 @@
package de.steamwar.techhider; package de.steamwar.techhider;
import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
@@ -44,11 +45,9 @@ public class ChunkHider {
private final int BIOMES_PER_SECTION = 64; private final int BIOMES_PER_SECTION = 64;
private final int blockIdUsedForHiding; private final int blockIdUsedForHiding;
private final AccessPrivilegeProvider accessPrivilegeProvider;
public ChunkHider(Block blockUsedForObfuscation, AccessPrivilegeProvider accessPrivilegeProvider) { public ChunkHider(Block blockUsedForObfuscation) {
blockIdUsedForHiding = Block.BLOCK_STATE_REGISTRY.getId(blockUsedForObfuscation.defaultBlockState()); blockIdUsedForHiding = Block.BLOCK_STATE_REGISTRY.getId(blockUsedForObfuscation.defaultBlockState());
this.accessPrivilegeProvider = accessPrivilegeProvider;
} }
private int getLongsRequiredToEncodeEntries(int bitsPerEntry, int entryCount) { private int getLongsRequiredToEncodeEntries(int bitsPerEntry, int entryCount) {
@@ -69,7 +68,8 @@ public class ChunkHider {
return dataArray; return dataArray;
} }
public ClientboundLevelChunkWithLightPacket processLevelChunkWithLightPacket(Player player, ClientboundLevelChunkWithLightPacket packet) { public ClientboundLevelChunkWithLightPacket processLevelChunkWithLightPacket(ForPlayer access, ClientboundLevelChunkWithLightPacket packet) {
Player player = access.getPlayer();
int chunkX = packet.getX(); int chunkX = packet.getX();
int chunkZ = packet.getZ(); int chunkZ = packet.getZ();
ClientboundLevelChunkPacketData chunkData = packet.getChunkData(); ClientboundLevelChunkPacketData chunkData = packet.getChunkData();
@@ -104,7 +104,7 @@ public class ChunkHider {
resolvedData[i] = pallet[palletReference]; resolvedData[i] = pallet[palletReference];
} }
int[] obfuscatedBlockData = obfuscateBlockDataArray(player, chunkX, chunkZ, yOffset, resolvedData); int[] obfuscatedBlockData = obfuscateBlockDataArray(access, chunkX, chunkZ, yOffset, resolvedData);
long[] reEncodedData = encodeDirectBlockDataArray(obfuscatedBlockData); long[] reEncodedData = encodeDirectBlockDataArray(obfuscatedBlockData);
@@ -149,7 +149,7 @@ public class ChunkHider {
int[] blockData = resolveDirectBlockDataArray(rawData, bitsPerBlock); int[] blockData = resolveDirectBlockDataArray(rawData, bitsPerBlock);
int[] obfuscatedBlockData = obfuscateBlockDataArray(player, chunkX, chunkZ, yOffset, blockData); int[] obfuscatedBlockData = obfuscateBlockDataArray(access, chunkX, chunkZ, yOffset, blockData);
long[] reEncodedData = encodeDirectBlockDataArray(obfuscatedBlockData); long[] reEncodedData = encodeDirectBlockDataArray(obfuscatedBlockData);
@@ -172,13 +172,13 @@ public class ChunkHider {
out.readBytes(data); out.readBytes(data);
List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntities = chunkData.blockEntitiesData; List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntities = chunkData.blockEntitiesData;
List<ClientboundLevelChunkPacketData.BlockEntityInfo> filteredBlockEntities = filterBlockEntities(player, blockEntities, chunkX, chunkZ); List<ClientboundLevelChunkPacketData.BlockEntityInfo> filteredBlockEntities = filterBlockEntities(access, blockEntities, chunkX, chunkZ);
return buildNewChunkPacket(packet, data, filteredBlockEntities); return buildNewChunkPacket(packet, data, filteredBlockEntities);
} }
private int[] obfuscateBlockDataArray(Player player, int chunkX, int chunkZ, int yOffset, int[] blockDataArray) { private int[] obfuscateBlockDataArray(ForPlayer access, int chunkX, int chunkZ, int yOffset, int[] blockDataArray) {
int[] obfuscatedData = new int[BLOCKS_PER_SECTION]; int[] obfuscatedData = new int[BLOCKS_PER_SECTION];
for (int sectionY = 0; sectionY < SECTION_SPAN_SIZE; sectionY++) { for (int sectionY = 0; sectionY < SECTION_SPAN_SIZE; sectionY++) {
for (int sectionZ = 0; sectionZ < SECTION_SPAN_SIZE; sectionZ++) { for (int sectionZ = 0; sectionZ < SECTION_SPAN_SIZE; sectionZ++) {
@@ -194,7 +194,7 @@ public class ChunkHider {
Block block = blockState.getBlock(); Block block = blockState.getBlock();
if (accessPrivilegeProvider.isPlayerPrivilegedToAccessPosition(player, worldX, worldY, worldZ) && accessPrivilegeProvider.isPlayerPrivilegedToAccessBlock(player, worldX, worldY, worldZ, block) && accessPrivilegeProvider.isPlayerPrivilegedToAccessBlockState(player, worldX, worldY, worldZ, blockState)) { if (access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlock(worldX, worldY, worldZ, block) && access.canAccessBlockState(worldX, worldY, worldZ, blockState)) {
obfuscatedData[blockDataIndex] = blockId; obfuscatedData[blockDataIndex] = blockId;
} else { } else {
obfuscatedData[blockDataIndex] = blockIdUsedForHiding; obfuscatedData[blockDataIndex] = blockIdUsedForHiding;
@@ -241,7 +241,7 @@ public class ChunkHider {
return clonedPacket; return clonedPacket;
} }
private List<ClientboundLevelChunkPacketData.BlockEntityInfo> filterBlockEntities(Player player, List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntities, int chunkX, int chunkZ) { private List<ClientboundLevelChunkPacketData.BlockEntityInfo> filterBlockEntities(ForPlayer access, List<ClientboundLevelChunkPacketData.BlockEntityInfo> blockEntities, int chunkX, int chunkZ) {
int fourBitBitmask = 0b0000_1111; int fourBitBitmask = 0b0000_1111;
return blockEntities.stream() return blockEntities.stream()
@@ -257,7 +257,7 @@ public class ChunkHider {
int worldY = blockEntityInfo.y; int worldY = blockEntityInfo.y;
return accessPrivilegeProvider.isPlayerPrivilegedToAccessPosition(player, worldX, worldY, worldZ) && accessPrivilegeProvider.isPlayerPrivilegedToAccessBlockEntity(player, worldX, worldY, worldZ, type); return access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlockEntity(worldX, worldY, worldZ, type);
}).toList(); }).toList();
} }
@@ -19,14 +19,14 @@
package de.steamwar.techhider; package de.steamwar.techhider;
import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer;
import com.comphenix.tinyprotocol.TinyProtocol; import com.comphenix.tinyprotocol.TinyProtocol;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.shorts.ShortArraySet; import it.unimi.dsi.fastutil.shorts.ShortArraySet;
import it.unimi.dsi.fastutil.shorts.ShortSets; import it.unimi.dsi.fastutil.shorts.ShortSets;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos; import net.minecraft.core.SectionPos;
import net.minecraft.network.PacketListener; import net.minecraft.network.PacketListener;
import net.minecraft.util.Mth;
import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.common.*; import net.minecraft.network.protocol.common.*;
import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket; import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket;
@@ -54,7 +54,7 @@ import java.util.function.ToIntFunction;
*/ */
public class TechHider { public class TechHider {
private final Set<Class<?>> bypassingPackets; private final Set<Class<?>> bypassingPackets;
private final Map<Class<? extends Packet<? extends PacketListener>>, BiFunction<Player, Packet<? extends PacketListener>, Packet<? extends PacketListener>>> packetProcessors; private final Map<Class<? extends Packet<? extends PacketListener>>, BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>>> packetProcessors;
private final BlockState blockStateUsedForObfuscation; private final BlockState blockStateUsedForObfuscation;
private final ChunkHider chunkHider; private final ChunkHider chunkHider;
@@ -65,7 +65,7 @@ public class TechHider {
public TechHider(Block blockUsedForObfuscation, AccessPrivilegeProvider privilegeProvider) { public TechHider(Block blockUsedForObfuscation, AccessPrivilegeProvider privilegeProvider) {
this.blockStateUsedForObfuscation = blockUsedForObfuscation.defaultBlockState(); this.blockStateUsedForObfuscation = blockUsedForObfuscation.defaultBlockState();
this.chunkHider = new ChunkHider(blockUsedForObfuscation, privilegeProvider); this.chunkHider = new ChunkHider(blockUsedForObfuscation);
this.privilegeProvider = privilegeProvider; this.privilegeProvider = privilegeProvider;
this.bypassingPackets = new HashSet<>(List.of( this.bypassingPackets = new HashSet<>(List.of(
@@ -181,6 +181,9 @@ public class TechHider {
ClientboundSetChunkCacheCenterPacket.class, // 7.1.93 Set Center Chunk ClientboundSetChunkCacheCenterPacket.class, // 7.1.93 Set Center Chunk
ClientboundForgetLevelChunkPacket.class, // 7.1.38 Unload Chunk ClientboundForgetLevelChunkPacket.class, // 7.1.38 Unload Chunk
ClientboundUpdateTagsPacket.class, // 7.1.133 Update Tags (play) ClientboundUpdateTagsPacket.class, // 7.1.133 Update Tags (play)
// 7.1.76 Remove Entities: must not be filtered, otherwise Player#hideEntity leaves ghosts on the client.
// Entities that are already gone from the world pass the entity checks anyway, so no additional information is leaked.
ClientboundRemoveEntitiesPacket.class,
ClientboundPlayerInfoRemovePacket.class, // 7.1.68 Player Info Remove ClientboundPlayerInfoRemovePacket.class, // 7.1.68 Player Info Remove
ClientboundPlayerInfoUpdatePacket.class, // 7.1.69 Player Info Update ClientboundPlayerInfoUpdatePacket.class, // 7.1.69 Player Info Update
ClientboundMoveVehiclePacket.class, // 7.1.56 Move Vehicle (vehicle the player is in) ClientboundMoveVehiclePacket.class, // 7.1.56 Move Vehicle (vehicle the player is in)
@@ -193,16 +196,16 @@ public class TechHider {
ClientboundContainerSetSlotPacket.class // 7.1.21 Set Container Slot ClientboundContainerSetSlotPacket.class // 7.1.21 Set Container Slot
)); ));
BiFunction<Player, Packet<? extends PacketListener>, Packet<? extends PacketListener>> tossPacket = (p, packet) -> null; BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>> tossPacket = (access, packet) -> null;
Map<Class<? extends Packet<? extends PacketListener>>, BiFunction<Player, Packet<? extends PacketListener>, Packet<? extends PacketListener>>> processors = new HashMap<>(); Map<Class<? extends Packet<? extends PacketListener>>, BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>>> processors = new HashMap<>();
// --- Entity packets - // --- Entity packets -
// 7.1.2 Spawn Entity: entity type and position can reveal hidden contraptions. // 7.1.2 Spawn Entity: entity type and position can reveal hidden contraptions.
processors.put(ClientboundAddEntityPacket.class, (p, packet) -> this.processAddEntityPacket(p, (ClientboundAddEntityPacket) packet)); processors.put(ClientboundAddEntityPacket.class, (access, packet) -> this.processAddEntityPacket(access, (ClientboundAddEntityPacket) packet));
// 7.1.3 Entity Animation: entity id based signal, keep blocked until entity visibility is modeled. // 7.1.3 Entity Animation: entity id based signal, keep blocked until entity visibility is modeled.
processors.put(ClientboundAnimatePacket.class, this.buildEntityPacketProcessor(ClientboundAnimatePacket::getId)); processors.put(ClientboundAnimatePacket.class, this.buildEntityPacketProcessor(ClientboundAnimatePacket::getId));
// 7.1.35 Entity Event: entity id based signal. // 7.1.35 Entity Event: entity id based signal.
processors.put(ClientboundEntityEventPacket.class, this.buildEntityPacketProcessor(ClientboundEntityEventPacket::getEventId)); processors.put(ClientboundEntityEventPacket.class, this.<ClientboundEntityEventPacket>buildEntityPacketProcessor(packet -> packet.entityId));
// 7.1.36 Teleport Entity: entity id and absolute position. // 7.1.36 Teleport Entity: entity id and absolute position.
processors.put(ClientboundTeleportEntityPacket.class, this.buildEntityPacketProcessor(ClientboundTeleportEntityPacket::id)); processors.put(ClientboundTeleportEntityPacket.class, this.buildEntityPacketProcessor(ClientboundTeleportEntityPacket::id));
// 7.1.42 Hurt Animation: entity id based signal. // 7.1.42 Hurt Animation: entity id based signal.
@@ -234,43 +237,41 @@ public class TechHider {
// 7.1.67 Combat Death: entity/player ids and death message context. // 7.1.67 Combat Death: entity/player ids and death message context.
processors.put(ClientboundPlayerCombatKillPacket.class, this.buildEntityPacketProcessor(ClientboundPlayerCombatKillPacket::playerId)); processors.put(ClientboundPlayerCombatKillPacket.class, this.buildEntityPacketProcessor(ClientboundPlayerCombatKillPacket::playerId));
// 7.1.52/53/55 Update Entity Position/Rotation: entity id and movement signal. // 7.1.52/53/55 Update Entity Position/Rotation: entity id and movement signal.
processors.put(ClientboundMoveEntityPacket.class, (p, packet) -> this.processMoveEntityPacket(p, (ClientboundMoveEntityPacket) packet)); processors.put(ClientboundMoveEntityPacket.class, (access, packet) -> this.processMoveEntityPacket(access, (ClientboundMoveEntityPacket) packet));
processors.put(ClientboundMoveEntityPacket.Pos.class, (p, packet) -> this.processMoveEntityPacket(p, (ClientboundMoveEntityPacket.Pos) packet)); processors.put(ClientboundMoveEntityPacket.Pos.class, (access, packet) -> this.processMoveEntityPacket(access, (ClientboundMoveEntityPacket.Pos) packet));
processors.put(ClientboundMoveEntityPacket.Rot.class, (p, packet) -> this.processMoveEntityPacket(p, (ClientboundMoveEntityPacket.Rot) packet)); processors.put(ClientboundMoveEntityPacket.Rot.class, (access, packet) -> this.processMoveEntityPacket(access, (ClientboundMoveEntityPacket.Rot) packet));
processors.put(ClientboundMoveEntityPacket.PosRot.class, (p, packet) -> this.processMoveEntityPacket(p, (ClientboundMoveEntityPacket.PosRot) packet)); processors.put(ClientboundMoveEntityPacket.PosRot.class, (access, packet) -> this.processMoveEntityPacket(access, (ClientboundMoveEntityPacket.PosRot) packet));
// 7.1.82 Set Head Rotation: entity id and rotation. // 7.1.82 Set Head Rotation: entity id and rotation.
processors.put(ClientboundRotateHeadPacket.class, (p, packet) -> this.processRotateHeadPacket(p, (ClientboundRotateHeadPacket) packet)); processors.put(ClientboundRotateHeadPacket.class, (access, packet) -> this.processRotateHeadPacket(access, (ClientboundRotateHeadPacket) packet));
// 7.1.76 Remove Entities: entity id visibility side channel.
processors.put(ClientboundRemoveEntitiesPacket.class, (p, packet) -> this.processRemoveEntitiesPacket(p, (ClientboundRemoveEntitiesPacket) packet));
// 7.1.99 Link Entities: entity relationship signal. // 7.1.99 Link Entities: entity relationship signal.
processors.put(ClientboundSetEntityLinkPacket.class, (p, packet) -> this.processSetEntityLinkPacket(p, (ClientboundSetEntityLinkPacket) packet)); processors.put(ClientboundSetEntityLinkPacket.class, (access, packet) -> this.processSetEntityLinkPacket(access, (ClientboundSetEntityLinkPacket) packet));
// --- Block entity packets --- // --- Block entity packets ---
// 7.1.7 Block Entity Data: currently filtered by block position and allowed tile-entity actions. // 7.1.7 Block Entity Data: currently filtered by block position and allowed tile-entity actions.
processors.put(ClientboundBlockEntityDataPacket.class, (p, packet) -> processBlockEntityDataPacket(p, (ClientboundBlockEntityDataPacket) packet)); processors.put(ClientboundBlockEntityDataPacket.class, (access, packet) -> processBlockEntityDataPacket(access, (ClientboundBlockEntityDataPacket) packet));
// 7.1.122 Tag Query Response: block-entity query result. // 7.1.122 Tag Query Response: block-entity query result.
processors.put(ClientboundTagQueryPacket.class, tossPacket); processors.put(ClientboundTagQueryPacket.class, tossPacket);
// --- Block packets --- // --- Block packets ---
// 7.1.6 Set Block Destroy Stage: block position and mining progress can reveal activity. // 7.1.6 Set Block Destroy Stage: block position and mining progress can reveal activity.
processors.put(ClientboundBlockDestructionPacket.class, (p, packet) -> proccessBlockDestructionPacket(p, (ClientboundBlockDestructionPacket) packet)); processors.put(ClientboundBlockDestructionPacket.class, (access, packet) -> proccessBlockDestructionPacket(access, (ClientboundBlockDestructionPacket) packet));
// 7.1.8 Block Action: currently filtered by block position. // 7.1.8 Block Action: currently filtered by block position.
processors.put(ClientboundBlockEventPacket.class, (p, packet) -> processBlockEventPacket(p, (ClientboundBlockEventPacket) packet)); processors.put(ClientboundBlockEventPacket.class, (access, packet) -> processBlockEventPacket(access, (ClientboundBlockEventPacket) packet));
// 7.1.9 Block Update: currently filtered/obfuscated by block position and block id. // 7.1.9 Block Update: currently filtered/obfuscated by block position and block id.
processors.put(ClientboundBlockUpdatePacket.class, (p, packet) -> processBlockUpdatePacket(p, (ClientboundBlockUpdatePacket) packet)); processors.put(ClientboundBlockUpdatePacket.class, (access, packet) -> processBlockUpdatePacket(access, (ClientboundBlockUpdatePacket) packet));
// --- Chunk packets --- // --- Chunk packets ---
// 7.1.45 Chunk Data and Update Light: currently filtered/obfuscated by chunk data processor. // 7.1.45 Chunk Data and Update Light: currently filtered/obfuscated by chunk data processor.
processors.put(ClientboundLevelChunkWithLightPacket.class, (p, packet) -> processChunkWithLight(p, (ClientboundLevelChunkWithLightPacket) packet)); processors.put(ClientboundLevelChunkWithLightPacket.class, (access, packet) -> processChunkWithLight(access, (ClientboundLevelChunkWithLightPacket) packet));
// --- Section packets --- // --- Section packets ---
// 7.1.83 Update Section Blocks: currently filtered/obfuscated by section block processor. // 7.1.83 Update Section Blocks: currently filtered/obfuscated by section block processor.
processors.put(ClientboundSectionBlocksUpdatePacket.class, (p, packet) -> processSectionUpdate(p, (ClientboundSectionBlocksUpdatePacket) packet)); processors.put(ClientboundSectionBlocksUpdatePacket.class, (access, packet) -> processSectionUpdate(access, (ClientboundSectionBlocksUpdatePacket) packet));
// --- Particle packets --- // --- Particle packets ---
// 7.1.47 Particle: particle type and position can reveal hidden machinery. // 7.1.47 Particle: particle type and position can reveal hidden machinery.
processors.put(ClientboundLevelParticlesPacket.class, (p, packet) -> processLevelParticlesPacket(p, (ClientboundLevelParticlesPacket) packet)); processors.put(ClientboundLevelParticlesPacket.class, (access, packet) -> processLevelParticlesPacket(access, (ClientboundLevelParticlesPacket) packet));
// --- Sound packets --- // --- Sound packets ---
// 7.1.115 Entity Sound Effect: entity id and sound. // 7.1.115 Entity Sound Effect: entity id and sound.
@@ -290,26 +291,29 @@ public class TechHider {
// 7.1.59 Open Sign Editor: block position. // 7.1.59 Open Sign Editor: block position.
processors.put(ClientboundOpenSignEditorPacket.class, buildPositionBasedPacketProcessor(ClientboundOpenSignEditorPacket::getPos)); processors.put(ClientboundOpenSignEditorPacket.class, buildPositionBasedPacketProcessor(ClientboundOpenSignEditorPacket::getPos));
// 7.1.37 Explosion: position, affected blocks, and knockback can reveal TNT/tech. // 7.1.37 Explosion: position, affected blocks, and knockback can reveal TNT/tech.
processors.put(ClientboundExplodePacket.class, (p, rawPacket) -> { processors.put(ClientboundExplodePacket.class, (access, rawPacket) -> {
ClientboundExplodePacket packet = (ClientboundExplodePacket) rawPacket; ClientboundExplodePacket packet = (ClientboundExplodePacket) rawPacket;
Vec3 pos = packet.center(); Vec3 pos = packet.center();
int blockX = (int) pos.x; int blockX = Mth.floor(pos.x);
int blockY = (int) pos.y; int blockY = Mth.floor(pos.y);
int blockZ = (int) pos.z; int blockZ = Mth.floor(pos.z);
return proccessPositionBasedPacket(p, blockX, blockY, blockZ, packet); return proccessPositionBasedPacket(access, blockX, blockY, blockZ, packet);
}); });
this.packetProcessors = processors; this.packetProcessors = processors;
TinyProtocol.instance.addGlobalClientboundFilter((player, rawPacket) -> { TinyProtocol.instance.addGlobalClientboundFilter((player, rawPacket) -> {
try { try {
// Resolved lazily (login packets arrive before the player is known) and once per outer packet
AccessHolder access = new AccessHolder(player);
if (rawPacket instanceof ClientboundBundlePacket bundlePacket) { if (rawPacket instanceof ClientboundBundlePacket bundlePacket) {
List<Packet<? super ClientGamePacketListener>> processedPackets = new ArrayList<>(); List<Packet<? super ClientGamePacketListener>> processedPackets = new ArrayList<>();
for (Packet<? super ClientGamePacketListener> packet : bundlePacket.subPackets()) { for (Packet<? super ClientGamePacketListener> packet : bundlePacket.subPackets()) {
Packet<? super ClientGamePacketListener> processedPacket = (Packet<? super ClientGamePacketListener>) processPacket(player, packet); Packet<? super ClientGamePacketListener> processedPacket = (Packet<? super ClientGamePacketListener>) processPacket(access, packet);
if (processedPacket != null) { if (processedPacket != null) {
processedPackets.add(processedPacket); processedPackets.add(processedPacket);
@@ -318,7 +322,7 @@ public class TechHider {
return new ClientboundBundlePacket(processedPackets); return new ClientboundBundlePacket(processedPackets);
} else { } else {
return processPacket(player, rawPacket); return processPacket(access, rawPacket);
} }
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
@@ -330,89 +334,100 @@ public class TechHider {
TinyProtocol.instance.addFilter(ServerboundInteractPacket.class, (p, packet) -> privilegeProvider.isPlayerPrivilegedToPerformAction(p) ? packet : null); TinyProtocol.instance.addFilter(ServerboundInteractPacket.class, (p, packet) -> privilegeProvider.isPlayerPrivilegedToPerformAction(p) ? packet : null);
} }
private Packet<?> processPacket(Player player, Packet<?> packet) { private final class AccessHolder {
private final Player player;
private ForPlayer access;
private AccessHolder(Player player) {
this.player = player;
}
private ForPlayer get() {
if (access == null) {
access = privilegeProvider.forPlayer(player);
}
return access;
}
}
private Packet<?> processPacket(AccessHolder access, Packet<?> packet) {
if (bypassingPackets.contains(packet.getClass())) { if (bypassingPackets.contains(packet.getClass())) {
return packet; return packet;
} else if (packetProcessors.containsKey(packet.getClass())) { }
return packetProcessors.get(packet.getClass()).apply(player, packet);
BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>> processor = packetProcessors.get(packet.getClass());
if (processor != null) {
return processor.apply(access.get(), packet);
} else { } else {
return null; return null;
} }
} }
private Packet<? extends PacketListener> processAddEntityPacket(Player player, ClientboundAddEntityPacket packet) { private Packet<? extends PacketListener> processAddEntityPacket(ForPlayer access, ClientboundAddEntityPacket packet) {
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, packet.getId()) && privilegeProvider.isPlayerPrivilegedToAccessPosition(player, (int) packet.getX(), (int) packet.getY(), (int) packet.getZ())) { if (access.canAccessEntity(packet.getId()) && access.canAccessPosition(Mth.floor(packet.getX()), Mth.floor(packet.getY()), Mth.floor(packet.getZ()))) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<? extends PacketListener> processEntityPacket(Player player, int entityId, Packet<? extends PacketListener> packet) { private Packet<? extends PacketListener> processEntityPacket(ForPlayer access, int entityId, Packet<? extends PacketListener> packet) {
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, entityId)) { if (access.canAccessEntity(entityId)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private <TTargetPacket extends Packet<?>> BiFunction<Player, Packet<? extends PacketListener>, Packet<? extends PacketListener>> buildEntityPacketProcessor(ToIntFunction<TTargetPacket> entityIdExtractor) { private <TTargetPacket extends Packet<?>> BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>> buildEntityPacketProcessor(ToIntFunction<TTargetPacket> entityIdExtractor) {
return (p, rawPacket) -> { return (access, rawPacket) -> {
TTargetPacket packet = (TTargetPacket) rawPacket; TTargetPacket packet = (TTargetPacket) rawPacket;
return processEntityPacket(p, entityIdExtractor.applyAsInt(packet), packet); return processEntityPacket(access, entityIdExtractor.applyAsInt(packet), packet);
}; };
} }
private Packet<?> processMoveEntityPacket(Player player, ClientboundMoveEntityPacket packet) { private Packet<?> processMoveEntityPacket(ForPlayer access, ClientboundMoveEntityPacket packet) {
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, packet.entityId)) { if (access.canAccessEntity(packet.entityId)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<?> processRotateHeadPacket(Player player, ClientboundRotateHeadPacket packet) { private Packet<?> processRotateHeadPacket(ForPlayer access, ClientboundRotateHeadPacket packet) {
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, packet.entityId)) { if (access.canAccessEntity(packet.entityId)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<?> processRemoveEntitiesPacket(Player player, ClientboundRemoveEntitiesPacket packet) { public Packet<?> processSetEntityLinkPacket(ForPlayer access, ClientboundSetEntityLinkPacket packet) {
IntList entityIdsToRemove = packet.getEntityIds();
IntList filteredEntitiesToRemove = entityIdsToRemove.intStream().filter((id) -> privilegeProvider.isPlayerPrivilegedToAccessEntity(player, id)).collect(IntArrayList::new, IntList::add, IntList::addAll);
return new ClientboundRemoveEntitiesPacket(filteredEntitiesToRemove);
}
public Packet<?> processSetEntityLinkPacket(Player player, ClientboundSetEntityLinkPacket packet) {
int fromEntityId = packet.getSourceId(); int fromEntityId = packet.getSourceId();
int toEntityId = packet.getDestId(); int toEntityId = packet.getDestId();
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, fromEntityId) && privilegeProvider.isPlayerPrivilegedToAccessEntity(player, toEntityId)) { if (access.canAccessEntity(fromEntityId) && access.canAccessEntity(toEntityId)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<?> processBlockEventPacket(Player player, ClientboundBlockEventPacket packet) { private Packet<?> processBlockEventPacket(ForPlayer access, ClientboundBlockEventPacket packet) {
BlockPos blockPos = packet.getPos(); BlockPos blockPos = packet.getPos();
Block block = packet.getBlock(); Block block = packet.getBlock();
int blockX = blockPos.getX(); int blockX = blockPos.getX();
int blockY = blockPos.getY(); int blockY = blockPos.getY();
int blockZ = blockPos.getZ(); int blockZ = blockPos.getZ();
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ) && privilegeProvider.isPlayerPrivilegedToAccessBlock(player, blockX, blockY, blockZ, block)) { if (access.canAccessPosition(blockX, blockY, blockZ) && access.canAccessBlock(blockX, blockY, blockZ, block)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<?> processBlockUpdatePacket(Player player, ClientboundBlockUpdatePacket packet) { private Packet<?> processBlockUpdatePacket(ForPlayer access, ClientboundBlockUpdatePacket packet) {
BlockPos blockPos = packet.getPos(); BlockPos blockPos = packet.getPos();
BlockState blockState = packet.getBlockState(); BlockState blockState = packet.getBlockState();
Block block = blockState.getBlock(); Block block = blockState.getBlock();
@@ -420,41 +435,41 @@ public class TechHider {
int blockY = blockPos.getY(); int blockY = blockPos.getY();
int blockZ = blockPos.getZ(); int blockZ = blockPos.getZ();
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ) && privilegeProvider.isPlayerPrivilegedToAccessBlock(player, blockX, blockY, blockZ, block) && privilegeProvider.isPlayerPrivilegedToAccessBlockState(player, blockX, blockY, blockZ, blockState)) { if (access.canAccessPosition(blockX, blockY, blockZ) && access.canAccessBlock(blockX, blockY, blockZ, block) && access.canAccessBlockState(blockX, blockY, blockZ, blockState)) {
return packet; return packet;
} else if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ)) { } else if (access.canAccessPosition(blockX, blockY, blockZ)) {
return new ClientboundBlockUpdatePacket(blockPos, blockStateUsedForObfuscation); return new ClientboundBlockUpdatePacket(blockPos, blockStateUsedForObfuscation);
} else { } else {
return null; return null;
} }
} }
private Packet<?> processBlockEntityDataPacket(Player player, ClientboundBlockEntityDataPacket packet) { private Packet<?> processBlockEntityDataPacket(ForPlayer access, ClientboundBlockEntityDataPacket packet) {
BlockPos blockPos = packet.getPos(); BlockPos blockPos = packet.getPos();
BlockEntityType<?> blockEntityType = packet.getType(); BlockEntityType<?> blockEntityType = packet.getType();
int blockX = blockPos.getX(); int blockX = blockPos.getX();
int blockY = blockPos.getY(); int blockY = blockPos.getY();
int blockZ = blockPos.getZ(); int blockZ = blockPos.getZ();
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ) && privilegeProvider.isPlayerPrivilegedToAccessBlockEntity(player, blockX, blockY, blockZ, blockEntityType)) { if (access.canAccessPosition(blockX, blockY, blockZ) && access.canAccessBlockEntity(blockX, blockY, blockZ, blockEntityType)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private Packet<?> proccessBlockDestructionPacket(Player player, ClientboundBlockDestructionPacket packet) { private Packet<?> proccessBlockDestructionPacket(ForPlayer access, ClientboundBlockDestructionPacket packet) {
BlockPos blockPos = packet.getPos(); BlockPos blockPos = packet.getPos();
int entityBreakingBlockId = packet.getId(); int entityBreakingBlockId = packet.getId();
if (privilegeProvider.isPlayerPrivilegedToAccessEntity(player, entityBreakingBlockId) && privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockPos.getX(), blockPos.getY(), blockPos.getZ())) { if (access.canAccessEntity(entityBreakingBlockId) && access.canAccessPosition(blockPos.getX(), blockPos.getY(), blockPos.getZ())) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private ClientboundSectionBlocksUpdatePacket processSectionUpdate(Player p, ClientboundSectionBlocksUpdatePacket packet) { private ClientboundSectionBlocksUpdatePacket processSectionUpdate(ForPlayer access, ClientboundSectionBlocksUpdatePacket packet) {
SectionPos sectionPos = packet.sectionPos; SectionPos sectionPos = packet.sectionPos;
short[] oldPos = packet.positions; short[] oldPos = packet.positions;
BlockState[] oldStates = packet.states; BlockState[] oldStates = packet.states;
@@ -471,10 +486,10 @@ public class TechHider {
int worldY = sectionPos.relativeToBlockY(posShort); int worldY = sectionPos.relativeToBlockY(posShort);
int worldZ = sectionPos.relativeToBlockZ(posShort); int worldZ = sectionPos.relativeToBlockZ(posShort);
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(p, worldX, worldY, worldZ) && privilegeProvider.isPlayerPrivilegedToAccessBlock(p, worldX, worldY, worldZ, block) && privilegeProvider.isPlayerPrivilegedToAccessBlockState(p, worldX, worldY, worldZ, state)) { if (access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlock(worldX, worldY, worldZ, block) && access.canAccessBlockState(worldX, worldY, worldZ, state)) {
filteredPos.add(posShort); filteredPos.add(posShort);
filteredStates.add(state); filteredStates.add(state);
} else if (privilegeProvider.isPlayerPrivilegedToAccessPosition(p, worldX, worldY, worldZ)) { } else if (access.canAccessPosition(worldX, worldY, worldZ)) {
filteredPos.add(posShort); filteredPos.add(posShort);
filteredStates.add(blockStateUsedForObfuscation); filteredStates.add(blockStateUsedForObfuscation);
} }
@@ -494,43 +509,43 @@ public class TechHider {
return new ClientboundSectionBlocksUpdatePacket(sectionPos, ShortSets.unmodifiable(new ShortArraySet(newPos)), newStates); return new ClientboundSectionBlocksUpdatePacket(sectionPos, ShortSets.unmodifiable(new ShortArraySet(newPos)), newStates);
} }
private Packet<?> processLevelParticlesPacket(Player player, ClientboundLevelParticlesPacket packet) { private Packet<?> processLevelParticlesPacket(ForPlayer access, ClientboundLevelParticlesPacket packet) {
int blockX = (int) packet.getX(); int blockX = Mth.floor(packet.getX());
int blockY = (int) packet.getY(); int blockY = Mth.floor(packet.getY());
int blockZ = (int) packet.getZ(); int blockZ = Mth.floor(packet.getZ());
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ)) { if (access.canAccessPosition(blockX, blockY, blockZ)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private ClientboundLevelChunkWithLightPacket processChunkWithLight(Player p, ClientboundLevelChunkWithLightPacket packet) { private ClientboundLevelChunkWithLightPacket processChunkWithLight(ForPlayer access, ClientboundLevelChunkWithLightPacket packet) {
if (privilegeProvider.isEveryonePrivilegedToAccessAllDataWithinChunk(packet.getX(), packet.getZ())) { if (access.canAccessAllDataWithinChunk(packet.getX(), packet.getZ())) {
return packet; return packet;
} else { } else {
return chunkHider.processLevelChunkWithLightPacket(p, packet); return chunkHider.processLevelChunkWithLightPacket(access, packet);
} }
} }
private Packet<? extends PacketListener> proccessPositionBasedPacket(Player player, int blockX, int blockY, int blockZ, Packet<? extends PacketListener> packet) { private Packet<? extends PacketListener> proccessPositionBasedPacket(ForPlayer access, int blockX, int blockY, int blockZ, Packet<? extends PacketListener> packet) {
if (privilegeProvider.isPlayerPrivilegedToAccessPosition(player, blockX, blockY, blockZ)) { if (access.canAccessPosition(blockX, blockY, blockZ)) {
return packet; return packet;
} else { } else {
return null; return null;
} }
} }
private <TTargetPacket extends Packet<?>> BiFunction<Player, Packet<?>, Packet<?>> buildPositionBasedPacketProcessor(Function<TTargetPacket, BlockPos> positionExtractor) { private <TTargetPacket extends Packet<?>> BiFunction<ForPlayer, Packet<?>, Packet<?>> buildPositionBasedPacketProcessor(Function<TTargetPacket, BlockPos> positionExtractor) {
return (p, rawPacket) -> { return (access, rawPacket) -> {
TTargetPacket packet = (TTargetPacket) rawPacket; TTargetPacket packet = (TTargetPacket) rawPacket;
BlockPos pos = positionExtractor.apply(packet); BlockPos pos = positionExtractor.apply(packet);
int blockX = pos.getX(); int blockX = pos.getX();
int blockY = pos.getY(); int blockY = pos.getY();
int blockZ = pos.getZ(); int blockZ = pos.getZ();
return proccessPositionBasedPacket(p, blockX, blockY, blockZ, packet); return proccessPositionBasedPacket(access, blockX, blockY, blockZ, packet);
}; };
} }
} }