forked from SteamWar/SteamWar
Calculate hull visibility on block faces
Instead of a block occluding as a whole, the hull stores which of its six faces are full, using the occlusion shapes the client culls faces with. A ray inside a block can only leave it through an open face and enter the next block through its open opposite face; if only that face is full, the ray reaches its surface, so the block is visible, but stops there. Slabs, stairs and stacked slabs now hide what is behind them; double slabs need no special case anymore. Faces only open up on block updates, after which the rays that could not get into, out of or past the block are continued. Hidden blocks occlude by the same decision as the techhider, so waterlogged blocks count as well when water is hidden. The hull therefore takes the ObfuscationConfig, which the fight shares with its TechHiderWrapper.
This commit is contained in:
@@ -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<Material> 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<IntVector, BitSet> visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block
|
||||
private final HashMap<IntVector, BitSet> visibilityDirections = new HashMap<>(); // The blocks the rays of each direction got inside
|
||||
private final HashMap<IntVector, IntVector[]> branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into
|
||||
private final HashSet<IntVector> 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<Material> 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<IntVector> uncovered = new ArrayList<>();
|
||||
if (visibility.get(id)) {
|
||||
for (Map.Entry<IntVector, BitSet> 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<IntVector, BitSet> 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<IntVector> uncovered, IntVector root) {
|
||||
for (IntVector diagonal : directions) {
|
||||
if (diagonal.sqLength() < 2) continue;
|
||||
private void uncoverBeside(List<IntVector> 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<IntVector> 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<IntVector, IntVector> 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<IntVector> 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<IntVector> 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<IntVector> 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<IntVector> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user