diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index f0ee5ecf..08669a80 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -103,7 +103,7 @@ public class HullHiderCommand extends SWCommand implements Listener { // The techhider of the gamemode is applied as well, its hidden blocks occlude as they are replaced by the obfuscation block ObfuscationConfig obfuscation = ObfuscationConfig.of(region.getGameModeConfig().Techhider); - Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, region.getGameModeConfig().Techhider.HiddenBlocks, new Viewers.VisibleByDefault()); + Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, obfuscation, new Viewers.VisibleByDefault()); hullHider.addHull(region, hull); hulls.put(region, new HiddenHull(hull, obfuscation.getObfuscationState())); hidingRules.put(region, HidingRule.of(region.getBuildArea(), obfuscation)); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java index 8c7eee13..fd961d96 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -33,6 +33,8 @@ import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; import de.steamwar.hullhider.Viewers; +import de.steamwar.techhider.ObfuscationConfig; +import lombok.Getter; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; @@ -51,13 +53,16 @@ import java.util.function.BiConsumer; public class FightHullHider { private final HullHider hullHider = new HullHider<>(true); + // Shared with the TechHiderWrapper + @Getter + private final ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); public FightHullHider() { if (!TechHiderWrapper.ENABLED) return; Fight.teams().forEach(team -> { Region region = team.getSchemRegion(); - hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), Config.GameModeConfig.Techhider.HiddenBlocks, new Viewers.HiddenByDefault())); + hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), obfuscation, new Viewers.HiddenByDefault())); }); new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener()); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java index 6f0b1290..534e7cee 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -69,8 +69,7 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public void enable() { - ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); - new TechHider(newProvider(obfuscation)); + new TechHider(newProvider(hullHider.getObfuscation())); } private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) { diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index b39a215e..3c37ff26 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -24,24 +24,26 @@ import com.mojang.authlib.GameProfile; import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.core.ProtocolWrapper; import de.steamwar.entity.REntity; +import de.steamwar.techhider.ObfuscationConfig; import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap; import lombok.AllArgsConstructor; import lombok.Getter; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.VoxelShape; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Slab; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -49,24 +51,28 @@ import org.bukkit.plugin.Plugin; import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.stream.IntStream; public class Hull { private static final IntVector[] NO_BRANCHES = new IntVector[0]; + private static final int ALL_FACES = 0b111111; + private static final byte UNKNOWN_FACES = -1; private final Plugin plugin; private final World world; @Getter private final HullRegion region; private final boolean groundVisible; - private final Set hiddenBlocks; + private final ObfuscationConfig obfuscation; private final IntVector[] directions; - private final BitSet occluding; + private final byte[] fullFaces; // The faces of each block that are fully covered, see face() + private byte[] fullFacesByState; // Cache, per hull as the hidden blocks differ private final BitSet visibility; - private final HashMap visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block + private final HashMap visibilityDirections = new HashMap<>(); // The blocks the rays of each direction got inside private final HashMap branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into private final HashSet uncoveredSurface = new HashSet<>(); @@ -76,17 +82,17 @@ public class Hull { /** * @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground) - * @param hiddenBlocks blocks that occlude like solid blocks, as the techhider replaces them by an occluding block + * @param obfuscation the techhider applied to the hull, the blocks it hides occlude as they are replaced by an occluding block * @param viewers which players may see the hull contents, a new instance for every hull */ - public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, Set hiddenBlocks, Viewers viewers) { + public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, ObfuscationConfig obfuscation, Viewers viewers) { this.plugin = plugin; this.world = world; this.region = region; this.groundVisible = groundVisible; - this.hiddenBlocks = hiddenBlocks; + this.obfuscation = obfuscation; this.viewers = viewers; - this.occluding = new BitSet(region.volume()); + this.fullFaces = new byte[region.volume()]; this.visibility = new BitSet(region.volume()); directions = IntStream.range(0, 27) @@ -101,17 +107,42 @@ public class Hull { } } - private boolean isOccluding(Material material) { - return material.isOccluding() || hiddenBlocks.contains(material); + /** + * @return the bit of the face of a block in the single-axis direction + */ + private static int face(int dx, int dy, int dz) { + if (dx != 0) return dx > 0 ? 1 : 1 << 1; + if (dy != 0) return dy > 0 ? 1 << 2 : 1 << 3; + return dz > 0 ? 1 << 4 : 1 << 5; } - private boolean isOccluding(BlockData data) { - return isOccluding(data.getMaterial()) || isDoubleSlab(data); + private int fullFacesOf(BlockState state) { + if (fullFacesByState == null) { + fullFacesByState = new byte[net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY.size()]; + Arrays.fill(fullFacesByState, UNKNOWN_FACES); + } + + int id = net.minecraft.world.level.block.Block.getId(state); + if (fullFacesByState[id] == UNKNOWN_FACES) { + fullFacesByState[id] = (byte) computeFullFaces(state); + } + return fullFacesByState[id]; } - // Double slabs are full blocks, but slab materials do not occlude - private static boolean isDoubleSlab(BlockData data) { - return data instanceof Slab slab && slab.getType() == Slab.Type.DOUBLE; + // The faces that hide what is behind them, as the client culls them + private int computeFullFaces(BlockState state) { + // Hidden blocks are replaced by an occluding block, the same decision as the techhider's + if (obfuscation.hidesBlock(state.getBlock()) || obfuscation.hidesBlockState(state)) return ALL_FACES; + if (!state.canOcclude()) return 0; + + VoxelShape shape = state.getOcclusionShape(); + int faces = 0; + for (Direction direction : Direction.values()) { + if (net.minecraft.world.level.block.Block.isFaceFull(shape, direction)) { + faces |= face(direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + } + return faces; } public boolean isBlockHidden(Player player, int x, int y, int z) { @@ -247,7 +278,7 @@ public class Hull { public void fill(boolean visible) { visibility.set(0, visibility.size(), visible); - occluding.set(0, occluding.size(), !visible); + Arrays.fill(fullFaces, (byte) (visible ? 0 : ALL_FACES)); uncoveredSurface.clear(); for (BitSet directionalVisibility : visibilityDirections.values()) { directionalVisibility.set(0, directionalVisibility.size(), visible); @@ -256,67 +287,59 @@ public class Hull { public void initialize() { visibility.clear(); - occluding.clear(); uncoveredSurface.clear(); for (BitSet directionalVisibility : visibilityDirections.values()) { directionalVisibility.clear(); } long start = System.currentTimeMillis(); - region.forEach((x, y, z) -> { - Block block = world.getBlockAt(x, y, z); - Material type = block.getType(); - // Only slabs need their block data - if (isOccluding(type) || Tag.SLABS.isTagged(type) && isDoubleSlab(block.getBlockData())) { - occluding.set(region.toIndex(x, y, z)); - } - }); - forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction)); + ServerLevel level = ((CraftWorld) world).getHandle(); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + region.forEach((x, y, z) -> fullFaces[region.toIndex(x, y, z)] = (byte) fullFacesOf(level.getBlockState(pos.set(x, y, z)))); + forEachBorder((root, direction) -> startRay(new NullList<>(), root, direction)); plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality()); } public void updateBlockVisibility(Block b, BlockData changedData) { IntVector root = new IntVector(b.getX(), b.getY(), b.getZ()); - if (root.notInRegion(region)) return; + if (root.notInRegion(region)) return; int id = root.toId(region); - if (!occluding.get(id) || isOccluding(changedData)) return; - - occluding.clear(id); + // Faces only open up, the hull never hides anything again + int faces = fullFaces[id] & fullFacesOf(((CraftBlockData) changedData).getState()); + if (faces == fullFaces[id]) return; + fullFaces[id] = (byte) faces; List uncovered = new ArrayList<>(); - if (visibility.get(id)) { - for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { - if (directionalVisibility.getValue().get(id)) { - directionalVisibility.getValue().clear(id); - uncoverBlocks(uncovered, root, directionalVisibility.getKey()); - } + // Rays inside the block that could not leave it + for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { + if (directionalVisibility.getValue().get(id)) { + directionalVisibility.getValue().clear(id); + uncoverBlocks(uncovered, root, directionalVisibility.getKey()); } } - // Also if the block was not visible itself, it might have blocked diagonal rays passing it - uncoverDiagonalsBeside(uncovered, root); + // Rays that could not get into the block or past it + uncoverBeside(uncovered, root); + forEachBorderStart(root, direction -> startRay(uncovered, root, direction)); uncoveredSurface.addAll(new HashSet<>(uncovered)); uncoveredSurface.remove(root); } /** - * Continues the diagonal rays that could not pass the block while it occluded. - * The block lies between the start and the end of a diagonal step if it is the start moved along some, but not all, axes of the direction. + * Continues the rays inside the blocks in front of the block, or of which a diagonal ray moves past the block. + * The block lies in front of or beside the start of a step if it is the start moved along some or all axes of the direction. */ - private void uncoverDiagonalsBeside(List uncovered, IntVector root) { - for (IntVector diagonal : directions) { - if (diagonal.sqLength() < 2) continue; + private void uncoverBeside(List uncovered, IntVector root) { + for (IntVector direction : directions) { + if (direction.sqLength() == 0) continue; for (IntVector part : directions) { - if (part.sqLength() == 0 || part.sqLength() >= diagonal.sqLength() || !isPartOf(part, diagonal)) continue; + if (part.sqLength() == 0 || !isPartOf(part, direction)) continue; IntVector from = root.add(-part.x, -part.y, -part.z); - if (from.notInRegion(region)) continue; - - int fromId = from.toId(region); - if (!occluding.get(fromId) && reachedBy(fromId, diagonal) && canPass(from, diagonal)) { - uncoverBlocks(uncovered, from.add(diagonal), diagonal, visibilityDirections.get(diagonal), NO_BRANCHES); + if (!from.notInRegion(region) && isInside(from.toId(region), direction)) { + uncoverNext(uncovered, from, direction, visibilityDirections.get(direction), branches.get(direction)); } } } @@ -329,13 +352,13 @@ public class Hull { } /** - * @return whether a ray moving in the diagonal direction reached the block: a diagonal ray or a branch of a cardinal ray + * @return whether a ray moving in the direction is inside the block: a ray of this direction or a branch of a ray along a single axis */ - private boolean reachedBy(int id, IntVector diagonal) { - if (visibilityDirections.get(diagonal).get(id)) return true; + private boolean isInside(int id, IntVector direction) { + if (visibilityDirections.get(direction).get(id)) return true; for (IntVector cardinal : directions) { - if (cardinal.sqLength() == 1 && dot(cardinal, diagonal) == 1 && visibilityDirections.get(cardinal).get(id)) { + if (cardinal.sqLength() == 1 && dot(cardinal, direction) == 1 && visibilityDirections.get(cardinal).get(id)) { return true; } } @@ -387,6 +410,16 @@ public class Hull { } } + // The rays forEachBorder starts at the block + private void forEachBorderStart(IntVector block, Consumer f) { + if (groundVisible && block.y == region.getMinY()) f.accept(new IntVector(0, 1, 0)); + if (block.y == region.getMaxY() - 1) f.accept(new IntVector(0, -1, 0)); + if (block.z == region.getMinZ()) f.accept(new IntVector(0, 0, 1)); + if (block.z == region.getMaxZ() - 1) f.accept(new IntVector(0, 0, -1)); + if (block.x == region.getMinX()) f.accept(new IntVector(1, 0, 0)); + if (block.x == region.getMaxX() - 1) f.accept(new IntVector(-1, 0, 0)); + } + private void forEachBorder(BiConsumer f) { for (int x = region.getMinX(); x < region.getMaxX(); x++) { for (int z = region.getMinZ(); z < region.getMaxZ(); z++) { @@ -412,23 +445,31 @@ public class Hull { } } + /** + * Starts a ray at the border of the region, coming from outside. + */ + private void startRay(List uncovered, IntVector block, IntVector direction) { + if (faceFull(block.x, block.y, block.z, -direction.x, -direction.y, -direction.z)) { + reveal(uncovered, block, block.toId(region)); + } else { + uncoverBlocks(uncovered, block, direction); + } + } + private void uncoverBlocks(List uncovered, IntVector block, IntVector direction) { uncoverBlocks(uncovered, block, direction, visibilityDirections.get(direction), branches.get(direction)); } + /** + * Continues a ray that got inside the block. + */ private void uncoverBlocks(List uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) { if (block.notInRegion(region)) return; int id = block.toId(region); if (directionalVisibility.get(id)) return; directionalVisibility.set(id); - - if (!visibility.get(id)) { - visibility.set(id); - uncovered.add(block); - } - - if (occluding.get(id)) return; + reveal(uncovered, block, id); uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections); for (IntVector branchDirection : branchDirections) { @@ -441,29 +482,51 @@ public class Hull { int y = block.y + direction.y; int z = block.z + direction.z; // The cheap checks first: most blocks were already reached from this direction - if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z)) || !canPass(block, direction)) return; + if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z))) return; - uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections); + if (canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, true)) { + uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections); + return; + } + + // The ray might still reach the face of the next block, mostly it is visible already + int id = region.toIndex(x, y, z); + if (!visibility.get(id) && canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, false)) { + reveal(uncovered, new IntVector(x, y, z), id); + } + } + + private void reveal(List uncovered, IntVector block, int id) { + if (!visibility.get(id)) { + visibility.set(id); + uncovered.add(block); + } } /** - * A ray can only move diagonally if it could also get there by moving along the single axes of the direction one after another, - * in any order, without passing an occluding block. Otherwise, it would pass between blocks that only touch at an edge or corner. + * A ray moves along the single axes of its direction one after another, in any order that does not pass a full face. + * So a diagonal ray does not pass between blocks that only touch at an edge or corner. + * + * @param enter whether the ray has to get inside the target block, otherwise reaching its face suffices */ - private boolean canPass(IntVector block, IntVector direction) { - if (direction.sqLength() <= 1) return true; - // The ray ends there anyway; inside the region, all blocks in between are inside as well - if (!region.contains(block.x + direction.x, block.y + direction.y, block.z + direction.z)) return true; + private boolean canMove(int x, int y, int z, int dx, int dy, int dz, boolean enter) { + if (dx == 0 && dy == 0 && dz == 0) return true; - return canPass(block.x, block.y, block.z, direction.x, direction.y, direction.z); + // The blocks in between have to be entered + return (dx != 0 && canCross(x, y, z, dx, 0, 0, enter || dy != 0 || dz != 0) && canMove(x + dx, y, z, 0, dy, dz, enter)) + || (dy != 0 && canCross(x, y, z, 0, dy, 0, enter || dx != 0 || dz != 0) && canMove(x, y + dy, z, dx, 0, dz, enter)) + || (dz != 0 && canCross(x, y, z, 0, 0, dz, enter || dx != 0 || dy != 0) && canMove(x, y, z + dz, dx, dy, 0, enter)); } - private boolean canPass(int x, int y, int z, int dx, int dy, int dz) { - if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) <= 1) return true; + /** + * @return whether a ray inside the block can leave it to the neighbouring block in the single-axis direction (and enter it) + */ + private boolean canCross(int x, int y, int z, int dx, int dy, int dz, boolean enter) { + return !faceFull(x, y, z, dx, dy, dz) && (!enter || !faceFull(x + dx, y + dy, z + dz, -dx, -dy, -dz)); + } - return (dx != 0 && !occluding.get(region.toIndex(x + dx, y, z)) && canPass(x + dx, y, z, 0, dy, dz)) - || (dy != 0 && !occluding.get(region.toIndex(x, y + dy, z)) && canPass(x, y + dy, z, dx, 0, dz)) - || (dz != 0 && !occluding.get(region.toIndex(x, y, z + dz)) && canPass(x, y, z + dz, dx, dy, 0)); + private boolean faceFull(int x, int y, int z, int dx, int dy, int dz) { + return (fullFaces[region.toIndex(x, y, z)] & face(dx, dy, dz)) != 0; }