From 395303d04f03636696095c21d48fdc55697dc538 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 16 Apr 2026 12:10:33 +0200 Subject: [PATCH] Add NavMesh --- .../de/steamwar/fightsystem/ai/NavMesh.java | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java new file mode 100644 index 00000000..021ce215 --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java @@ -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 . + */ + +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 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 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 floorBlock = new HashMap<>(); + private Map ceilingOffset = new HashMap<>(); + private Map> neighbourConnections = new HashMap<>(); + private Map> 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 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 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 getAllWalkableBlocks() { + return floorBlock.keySet().stream().map(Pos::toVector).collect(Collectors.toList()); + } + + public List getWalkableBlocks(Vector fromVector) { + Pos from = toPos(fromVector); + if (from == null) { + return Collections.emptyList(); + } + + Set checked = new HashSet<>(); + List 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 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 checking = new ArrayList<>(Arrays.asList(to)); + Map route = new HashMap<>(); + while (!checking.isEmpty()) { + Set toCheck = new HashSet<>(); + for (Pos pos : checking) { + boolean foundFrom = false; + Set 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 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 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 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(); + } +}