Add NavMesh

This commit is contained in:
2026-04-16 12:10:33 +02:00
parent b466216b3a
commit 395303d04f
@@ -0,0 +1,388 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.ai;
import de.steamwar.entity.RArmorStand;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.OneShotStateDependent;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.event.Listener;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.bukkit.util.VoxelShape;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
public class NavMesh implements Listener {
private static final World WORLD = Bukkit.getWorlds().get(0);
private static final double PLAYER_JUMP_HEIGHT = 1.25;
private static final double PLAYER_HEIGHT = 1.8125;
private static final BoundingBox PLAYER_SHADOW = new BoundingBox(0.2, 0, 0.2, 0.8, 1, 0.8);
private static final Set<Pos> RELATIVE_BLOCKS_TO_CHECK = new HashSet<>();
static {
for (int y = -2; y <= 2; y++) {
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.NORTH.getDirection().setY(y)));
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.SOUTH.getDirection().setY(y)));
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.EAST.getDirection().setY(y)));
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.WEST.getDirection().setY(y)));
}
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.UP.getDirection()));
RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.DOWN.getDirection()));
}
private FightTeam fightTeam;
private List<REntity> rEntities = new ArrayList<>();
private REntityServer entityServer;
public NavMesh(FightTeam fightTeam, REntityServer entityServer) {
this.fightTeam = fightTeam;
this.entityServer = entityServer;
new OneShotStateDependent(ArenaMode.All, FightState.PostSchemSetup, () -> {
Bukkit.getScheduler().runTaskLater(FightSystem.getPlugin(), () -> {
long time = System.currentTimeMillis();
fightTeam.getExtendRegion().forEach((x, y, z) -> {
if (y < fightTeam.getSchemRegion().getMinY()) return;
checkWalkable(x, y, z);
});
floorBlock.forEach(this::checkNeighbouring);
System.out.println(System.currentTimeMillis() - time + " ms");
/*
iterateWalkableBlocks((vector, ceilingOffset) -> {
RArmorStand armorStand = new RArmorStand(entityServer, vector.toLocation(WORLD), RArmorStand.Size.MARKER);
armorStand.setNoGravity(true);
armorStand.setInvisible(true);
armorStand.setDisplayName("+" + (ceilingOffset == null ? "∞" : ceilingOffset));
});
*/
}, 20);
});
new OneShotStateDependent(ArenaMode.All, FightState.Spectate, () -> {
floorBlock.clear();
});
}
private static class Pos {
private int x;
private int y;
private int z;
public Pos(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public Pos(Vector vector) {
this.x = vector.getBlockX();
this.y = vector.getBlockY();
this.z = vector.getBlockZ();
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Pos)) return false;
Pos pos = (Pos) o;
return x == pos.x && y == pos.y && z == pos.z;
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
public Vector toVector() {
return new Vector(x, y, z);
}
public Pos add(Pos pos) {
return new Pos(x + pos.x, y + pos.y, z + pos.z);
}
public Pos subtract(Pos pos) {
return new Pos(x - pos.x, y - pos.y, z - pos.z);
}
}
private Map<Pos, Double> floorBlock = new HashMap<>();
private Map<Pos, Double> ceilingOffset = new HashMap<>();
private Map<Pos, Set<Pos>> neighbourConnections = new HashMap<>();
private Map<Pos, Set<Pos>> reverseNeighbourConnections = new HashMap<>();
private void checkWalkable(int x, int y, int z) {
Pos pos = new Pos(x, y, z);
floorBlock.remove(pos);
ceilingOffset.remove(pos);
Block block = WORLD.getBlockAt(x, y, z);
VoxelShape floor = block.getCollisionShape();
if (block.getType() != Material.LADDER && !overlaps(floor, 0, 0, 0)) return;
Double floorHeight = null;
for (BoundingBox box : floor.getBoundingBoxes()) {
double by = box.getMaxY();
if (!overlaps(floor, 0, by, 0)) {
if (floorHeight == null) {
floorHeight = by;
} else {
floorHeight = Math.min(floorHeight, by);
}
}
}
if (floorHeight == null) return;
Double ceilingOffset = null;
for (int cy = y + 1; cy < fightTeam.getExtendRegion().getMaxY(); cy++) {
VoxelShape current = WORLD.getBlockAt(x, cy, z).getCollisionShape();
if (!overlaps(current, 0, 0, 0)) continue;
Double ceilingHeight = null;
for (BoundingBox box : current.getBoundingBoxes()) {
double by = box.getMinY();
if (!overlaps(current, 0, -(1 - by), 0)) {
if (ceilingHeight == null) {
ceilingHeight = by;
} else {
ceilingHeight = Math.max(ceilingHeight, by);
}
}
}
if (ceilingHeight != null) {
ceilingOffset = cy - y + ceilingHeight - floorHeight;
}
break;
}
if (ceilingOffset != null && ceilingOffset < PLAYER_HEIGHT) return;
floorBlock.put(pos, y + floorHeight);
this.ceilingOffset.put(pos, ceilingOffset);
}
private void checkNeighbouring(Pos pos, double posFloorHeight) {
Set<Pos> connections = neighbourConnections.remove(pos);
if (connections != null) {
connections.forEach(p -> {
reverseNeighbourConnections.getOrDefault(pos.add(p), Collections.emptySet()).remove(pos);
});
}
for (Pos relativeCheck : RELATIVE_BLOCKS_TO_CHECK) {
Pos other = new Pos(pos.x + relativeCheck.x, pos.y + relativeCheck.y, pos.z + relativeCheck.z);
Double otherFloorHeight = floorBlock.get(other);
if (otherFloorHeight == null) continue;
if (otherFloorHeight > posFloorHeight && otherFloorHeight - posFloorHeight > PLAYER_JUMP_HEIGHT) continue;
// double floorDiff = Math.abs(posFloorHeight - otherFloorHeight);
// if (floorDiff > PLAYER_JUMP_HEIGHT) continue;
Double posCeilingOffset = ceilingOffset.get(pos);
Double otherCeilingOffset = ceilingOffset.get(other);
if (posCeilingOffset == null && otherCeilingOffset == null) {
neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
continue;
}
if (posCeilingOffset != null && otherCeilingOffset == null) {
if (posFloorHeight + posCeilingOffset - otherFloorHeight >= PLAYER_HEIGHT) {
neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
}
continue;
}
if (otherCeilingOffset != null && posCeilingOffset == null) {
if (otherFloorHeight + otherCeilingOffset - posFloorHeight >= PLAYER_HEIGHT) {
neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
}
continue;
}
if (posFloorHeight + posCeilingOffset - otherFloorHeight >= PLAYER_HEIGHT && otherFloorHeight + otherCeilingOffset - posFloorHeight >= PLAYER_HEIGHT) {
neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
}
}
}
private boolean overlaps(VoxelShape voxelShape, double x, double y, double z) {
PLAYER_SHADOW.shift(x, y, z);
boolean overlaps = voxelShape.overlaps(PLAYER_SHADOW);
PLAYER_SHADOW.shift(-x, -y, -z);
return overlaps;
}
private void iterateWalkableBlocks(BiConsumer<Vector, Double> consumer) {
floorBlock.forEach((pos, aDouble) -> {
Vector vector = new Vector(pos.x + 0.5, aDouble, pos.z + 0.5);
consumer.accept(vector, ceilingOffset.get(pos));
});
}
private Pos toPos(Vector vector) {
Pos pos = new Pos(vector);
if (floorBlock.containsKey(pos)) return pos;
pos = new Pos(pos.x, pos.y - 1, pos.z);
if (floorBlock.containsKey(pos)) return pos;
return null;
}
public void update(Vector posVector) {
Pos pos = toPos(posVector);
if (pos == null) return;
for (int x = -2; x <= 2; x++) {
for (int z = -2; z <= 2; z++) {
for (int y = fightTeam.getSchemRegion().getMinY(); y <= pos.y + 2; y++) {
checkWalkable(pos.x + x, y, pos.z + z);
}
}
}
floorBlock.forEach(this::checkNeighbouring);
}
public List<Vector> getAllWalkableBlocks() {
return floorBlock.keySet().stream().map(Pos::toVector).collect(Collectors.toList());
}
public List<Vector> getWalkableBlocks(Vector fromVector) {
Pos from = toPos(fromVector);
if (from == null) {
return Collections.emptyList();
}
Set<Pos> checked = new HashSet<>();
List<Pos> checking = new ArrayList<>();
checking.add(from);
while (!checking.isEmpty()) {
Pos pos = checking.remove(0);
checked.add(pos);
neighbourConnections.getOrDefault(pos, new HashSet<>()).forEach(p -> {
Pos n = pos.add(p);
if (checked.contains(n)) return;
if (checking.contains(n)) return;
checking.add(n);
});
}
return checked.stream().map(Pos::toVector).collect(Collectors.toList());
}
public List<Vector> path(Vector fromVector, Vector toVector) {
rEntities.forEach(REntity::die);
rEntities.clear();
Pos from = toPos(fromVector);
Pos to = toPos(toVector);
if (from == null || to == null) {
return Collections.emptyList();
}
if (from.equals(to)) {
return Collections.emptyList();
}
List<Pos> checking = new ArrayList<>(Arrays.asList(to));
Map<Pos, Pos> route = new HashMap<>();
while (!checking.isEmpty()) {
Set<Pos> toCheck = new HashSet<>();
for (Pos pos : checking) {
boolean foundFrom = false;
Set<Pos> successors = reverseNeighbourConnections.get(pos);
for (Pos p : successors) {
if (route.containsKey(p)) continue;
route.put(p, pos);
toCheck.add(p);
foundFrom = p.equals(from);
if (foundFrom) break;
}
if (foundFrom) {
List<Pos> path = new ArrayList<>();
path.add(from);
while (path.get(path.size() - 1) != to) {
path.add(route.get(path.get(path.size() - 1)));
}
for (int i = path.size() - 1; i > 0; i--) {
Pos current = path.get(i);
Pos last = path.get(i - 1);
if (last.y > current.y) {
path.set(i, new Pos(current.x, last.y, current.z));
}
}
List<Vector> vectors = path.stream().map(p -> {
Double floorHeight = floorBlock.get(p);
if (p.y > (floorHeight == null ? p.y : floorHeight)) floorHeight = null;
return new Vector(p.x + 0.5, floorHeight == null ? p.y : floorHeight, p.z + 0.5);
}).collect(Collectors.toList());
AtomicReference<Vector> last = new AtomicReference<>();
vectors.forEach(vector -> {
RArmorStand armorStand = new RArmorStand(entityServer, vector.toLocation(WORLD), RArmorStand.Size.MARKER);
armorStand.setInvisible(true);
armorStand.setNoGravity(true);
armorStand.setDisplayName("+");
rEntities.add(armorStand);
if (true) return;
Vector lastVector = last.getAndSet(vector);
if (lastVector == null) return;
lastVector = lastVector.clone().add(vector).divide(new Vector(2, 2, 2));
armorStand = new RArmorStand(entityServer, lastVector.toLocation(WORLD), RArmorStand.Size.MARKER);
armorStand.setInvisible(true);
armorStand.setNoGravity(true);
armorStand.setDisplayName("+");
rEntities.add(armorStand);
});
return vectors;
}
}
checking.clear();
checking.addAll(toCheck);
}
return Collections.emptyList();
}
}