forked from SteamWar/SteamWar
Stop hull rays from passing between occluding blocks
A ray only moves diagonally if it could also reach the block by moving along the single axes of the direction one after another without passing an occluding block, so it no longer squeezes between blocks touching at an edge or corner. When a block stops occluding, the diagonal rays it blocked are continued, so incremental updates match a full initialisation. Double slabs now occlude as well, as slab materials do not; block updates pass the block data instead of the material for that. To keep the initialisation fast, a ray only checks a diagonal step after the cheaper checks, and the branch directions are computed once per hull.
This commit is contained in:
@@ -37,8 +37,11 @@ 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.block.data.CraftBlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -64,6 +67,7 @@ public class Hull {
|
||||
private final BitSet occluding;
|
||||
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, IntVector[]> branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into
|
||||
private final HashSet<IntVector> uncoveredSurface = new HashSet<>();
|
||||
|
||||
private final Viewers viewers;
|
||||
@@ -91,6 +95,9 @@ public class Hull {
|
||||
|
||||
for (IntVector direction : directions) {
|
||||
visibilityDirections.put(direction, new BitSet(region.volume()));
|
||||
branches.put(direction, direction.sqLength() == 1
|
||||
? Arrays.stream(directions).filter(v -> v.sqLength() >= 2 && dot(v, direction) == 1).toArray(IntVector[]::new)
|
||||
: NO_BRANCHES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +105,15 @@ public class Hull {
|
||||
return material.isOccluding() || hiddenBlocks.contains(material);
|
||||
}
|
||||
|
||||
private boolean isOccluding(BlockData data) {
|
||||
return isOccluding(data.getMaterial()) || isDoubleSlab(data);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public boolean isBlockHidden(Player player, int x, int y, int z) {
|
||||
return region.contains(x, y, z) && !isViewer(player) && !visibility.get(region.toIndex(x, y, z));
|
||||
}
|
||||
@@ -248,37 +264,88 @@ public class Hull {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
region.forEach((x, y, z) -> {
|
||||
IntVector block = new IntVector(x, y, z);
|
||||
if (isOccluding(world.getBlockAt(x, y, z).getType())) {
|
||||
occluding.set(block.toId(region));
|
||||
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));
|
||||
plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality());
|
||||
}
|
||||
|
||||
public void updateBlockVisibility(Block b, Material changedType) {
|
||||
public void updateBlockVisibility(Block b, BlockData changedData) {
|
||||
IntVector root = new IntVector(b.getX(), b.getY(), b.getZ());
|
||||
if (root.notInRegion(region)) return;
|
||||
|
||||
int id = root.toId(region);
|
||||
if (!occluding.get(id) || isOccluding(changedType)) return;
|
||||
if (!occluding.get(id) || isOccluding(changedData)) return;
|
||||
|
||||
occluding.clear(id);
|
||||
if (!visibility.get(id)) return;
|
||||
|
||||
List<IntVector> uncovered = new ArrayList<>();
|
||||
for (Map.Entry<IntVector, BitSet> directionalVisibility : visibilityDirections.entrySet()) {
|
||||
if (directionalVisibility.getValue().get(id)) {
|
||||
directionalVisibility.getValue().clear(id);
|
||||
uncoverBlocks(uncovered, root, directionalVisibility.getKey());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also if the block was not visible itself, it might have blocked diagonal rays passing it
|
||||
uncoverDiagonalsBeside(uncovered, root);
|
||||
|
||||
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.
|
||||
*/
|
||||
private void uncoverDiagonalsBeside(List<IntVector> uncovered, IntVector root) {
|
||||
for (IntVector diagonal : directions) {
|
||||
if (diagonal.sqLength() < 2) continue;
|
||||
|
||||
for (IntVector part : directions) {
|
||||
if (part.sqLength() == 0 || part.sqLength() >= diagonal.sqLength() || !isPartOf(part, diagonal)) 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPartOf(IntVector part, IntVector direction) {
|
||||
return (part.x == 0 || part.x == direction.x)
|
||||
&& (part.y == 0 || part.y == direction.y)
|
||||
&& (part.z == 0 || part.z == direction.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether a ray moving in the diagonal direction reached the block: a diagonal ray or a branch of a cardinal ray
|
||||
*/
|
||||
private boolean reachedBy(int id, IntVector diagonal) {
|
||||
if (visibilityDirections.get(diagonal).get(id)) return true;
|
||||
|
||||
for (IntVector cardinal : directions) {
|
||||
if (cardinal.sqLength() == 1 && dot(cardinal, diagonal) == 1 && visibilityDirections.get(cardinal).get(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int dot(IntVector a, IntVector b) {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
|
||||
public void sendUncoveredBlocks() {
|
||||
Map<IntVector, List<IntVector>> sectionWise = new HashMap<>();
|
||||
|
||||
@@ -346,14 +413,7 @@ public class Hull {
|
||||
}
|
||||
|
||||
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction) {
|
||||
uncoverBlocks(
|
||||
uncovered, block, direction, visibilityDirections.get(direction),
|
||||
direction.sqLength() == 1
|
||||
? Arrays.stream(directions)
|
||||
.filter(v -> v.sqLength() >= 2 && v.x * direction.x + v.y * direction.y + v.z * direction.z == 1)
|
||||
.toArray(IntVector[]::new)
|
||||
: NO_BRANCHES
|
||||
);
|
||||
uncoverBlocks(uncovered, block, direction, visibilityDirections.get(direction), branches.get(direction));
|
||||
}
|
||||
|
||||
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
|
||||
@@ -370,12 +430,42 @@ public class Hull {
|
||||
|
||||
if (occluding.get(id)) return;
|
||||
|
||||
uncoverBlocks(uncovered, block.add(direction), direction, directionalVisibility, branchDirections);
|
||||
uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections);
|
||||
for (IntVector branchDirection : branchDirections) {
|
||||
uncoverBlocks(uncovered, block.add(branchDirection), branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES);
|
||||
uncoverNext(uncovered, block, branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES);
|
||||
}
|
||||
}
|
||||
|
||||
private void uncoverNext(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
|
||||
int x = block.x + direction.x;
|
||||
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;
|
||||
|
||||
uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
|
||||
return canPass(block.x, block.y, block.z, direction.x, direction.y, direction.z);
|
||||
}
|
||||
|
||||
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 (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));
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
|
||||
@@ -23,6 +23,7 @@ import de.steamwar.entity.REntity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
@@ -148,13 +149,13 @@ public class HullHider<K> implements Listener {
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockPhysic(BlockPhysicsEvent e) {
|
||||
if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) {
|
||||
blockUpdate(e.getBlock(), e.getChangedType());
|
||||
blockUpdate(e.getBlock(), e.getChangedBlockData());
|
||||
}
|
||||
}
|
||||
|
||||
public void blockUpdate(Block block, Material changedType) {
|
||||
public void blockUpdate(Block block, BlockData changedData) {
|
||||
for (Hull hull : hulls) {
|
||||
hull.updateBlockVisibility(block, changedType);
|
||||
hull.updateBlockVisibility(block, changedData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user