mirror of https://github.com/Minestom/Minestom.git
Use Krystilize pathfinder
This commit is contained in:
parent
d553dec4e1
commit
a43fc980c1
|
@ -63,8 +63,6 @@ dependencies {
|
|||
// Libraries
|
||||
api(libs.gson)
|
||||
implementation(libs.jcTools)
|
||||
// Path finding
|
||||
api(libs.hydrazine)
|
||||
|
||||
// Adventure, for user-interface
|
||||
api(libs.bundles.adventure)
|
||||
|
|
|
@ -5,7 +5,6 @@ metadata.format.version = "1.1"
|
|||
# Important dependencies
|
||||
adventure = "4.9.3"
|
||||
kotlin = "1.6.10"
|
||||
hydrazine = "1.7.2"
|
||||
dependencyGetter = "v1.0.1"
|
||||
minestomData = "895581d464"
|
||||
hephaistos = "2.4.2"
|
||||
|
@ -51,7 +50,6 @@ kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", vers
|
|||
kotlin-stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" }
|
||||
|
||||
# Miscellaneous
|
||||
hydrazine = { group = "com.github.MadMartian", name = "hydrazine-path-finding", version.ref = "hydrazine" }
|
||||
dependencyGetter = { group = "com.github.Minestom", name = "DependencyGetter", version.ref = "dependencyGetter" }
|
||||
minestomData = { group = "com.github.Minestom", name = "MinestomDataGenerator", version.ref = "minestomData" }
|
||||
jetbrainsAnnotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrainsAnnotations" }
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package net.minestom.server.entity;
|
||||
|
||||
import com.extollit.gaming.ai.path.HydrazinePathFinder;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.ai.EntityAI;
|
||||
import net.minestom.server.entity.ai.EntityAIGroup;
|
||||
import net.minestom.server.entity.pathfinding.NavigableEntity;
|
||||
import net.minestom.server.entity.pathfinding.Navigator;
|
||||
import net.minestom.server.event.EventDispatcher;
|
||||
import net.minestom.server.event.entity.EntityAttackEvent;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.utils.time.TimeUnit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
@ -17,7 +14,6 @@ import java.time.Duration;
|
|||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public class EntityCreature extends LivingEntity implements NavigableEntity, EntityAI {
|
||||
|
@ -54,13 +50,6 @@ public class EntityCreature extends LivingEntity implements NavigableEntity, Ent
|
|||
super.update(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
|
||||
this.navigator.setPathFinder(new HydrazinePathFinder(navigator.getPathingEntity(), instance.getInstanceSpace()));
|
||||
|
||||
return super.setInstance(instance, spawnPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kill() {
|
||||
super.kill();
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package net.minestom.server.entity.fakeplayer;
|
||||
|
||||
import com.extollit.gaming.ai.path.HydrazinePathFinder;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.NavigableEntity;
|
||||
import net.minestom.server.entity.pathfinding.Navigator;
|
||||
import net.minestom.server.event.EventListener;
|
||||
import net.minestom.server.event.player.PlayerSpawnEvent;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.network.ConnectionManager;
|
||||
import net.minestom.server.network.player.FakePlayerConnection;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
|
@ -17,7 +14,6 @@ import org.jetbrains.annotations.NotNull;
|
|||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
|
@ -121,13 +117,6 @@ public class FakePlayer extends Player implements NavigableEntity {
|
|||
this.navigator.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
|
||||
this.navigator.setPathFinder(new HydrazinePathFinder(navigator.getPathingEntity(), instance.getInstanceSpace()));
|
||||
|
||||
return super.setInstance(instance, spawnPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNewViewer(@NotNull Player player) {
|
||||
player.getPlayerConnection().sendPacket(getAddPlayerToList());
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.HydrazinePathFinder;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.WorldBorder;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class HydraPathImpl implements Pathfinder {
|
||||
private final Entity entity;
|
||||
final PFPathingEntity pathingEntity;
|
||||
HydrazinePathFinder pathFinder;
|
||||
|
||||
|
||||
volatile Point pathPosition;
|
||||
volatile List<Point> path;
|
||||
|
||||
HydraPathImpl(Navigator navigator) {
|
||||
this.entity = navigator.getEntity();
|
||||
this.pathingEntity = new PFPathingEntity(navigator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point nextPoint(Point currentPoint) {
|
||||
var path = this.path;
|
||||
if (path == null || path.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the closest point to `pathPosition` in the path that is also closer to `currentPoint`
|
||||
var closestPoint = path.get(0);
|
||||
var closestDistance = currentPoint.distance(closestPoint);
|
||||
int index = 0;
|
||||
for (var point : path) {
|
||||
var distance = currentPoint.distance(point);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
index = path.indexOf(point);
|
||||
}
|
||||
}
|
||||
if (index < path.size() - 1) {
|
||||
return path.get(index + 1);
|
||||
} else return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(Point point) {
|
||||
if (point != null && pathPosition != null && point.samePoint(pathPosition)) {
|
||||
// Tried to set path to the same target position
|
||||
return;
|
||||
}
|
||||
final Instance instance = entity.getInstance();
|
||||
if (pathFinder == null) {
|
||||
// Unexpected error
|
||||
return;
|
||||
}
|
||||
this.pathFinder.reset();
|
||||
if (point == null) {
|
||||
return;
|
||||
}
|
||||
// Can't path with a null instance.
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
// Can't path outside the world border
|
||||
final WorldBorder worldBorder = instance.getWorldBorder();
|
||||
if (!worldBorder.isInside(point)) {
|
||||
return;
|
||||
}
|
||||
// Can't path in an unloaded chunk
|
||||
final Chunk chunk = instance.getChunkAt(point);
|
||||
if (!ChunkUtils.isLoaded(chunk)) {
|
||||
return;
|
||||
}
|
||||
var path = pathFinder.computePathTo(point.x(), point.y(), point.z());
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Point> points = new ArrayList<>();
|
||||
for (int i = 0; i < path.length(); i++) {
|
||||
var node = path.at(i);
|
||||
var coordinates = node.coordinates();
|
||||
points.add(new Vec(coordinates.x, coordinates.y, coordinates.z));
|
||||
}
|
||||
|
||||
this.pathPosition = point;
|
||||
this.path = points;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Point> forcePath(Point target) {
|
||||
updatePath(target);
|
||||
return path;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
this.pathPosition = null;
|
||||
this.pathFinder.reset();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.HydrazinePathFinder;
|
||||
import net.minestom.server.attribute.Attribute;
|
||||
import net.minestom.server.collision.CollisionUtils;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
|
@ -20,11 +19,11 @@ import org.jetbrains.annotations.Nullable;
|
|||
*/
|
||||
public final class Navigator {
|
||||
private final Entity entity;
|
||||
private final HydraPathImpl pathfinder;
|
||||
private final Pathfinder pathfinder;
|
||||
|
||||
public Navigator(@NotNull Entity entity) {
|
||||
this.entity = entity;
|
||||
this.pathfinder = new HydraPathImpl(this);
|
||||
this.pathfinder = new PathfinderImpl(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +69,6 @@ public final class Navigator {
|
|||
if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead())
|
||||
return; // No pathfinding tick for dead entities
|
||||
final Point next = this.pathfinder.nextPoint(entity.getPosition());
|
||||
//System.out.println("move " + next);
|
||||
if (next != null) {
|
||||
moveTowards(next, getAttributeValue(Attribute.MOVEMENT_SPEED));
|
||||
final double entityY = entity.getPosition().y();
|
||||
|
@ -86,23 +84,14 @@ public final class Navigator {
|
|||
* @return the target pathfinder position, null if there is no one
|
||||
*/
|
||||
public @Nullable Point getPathPosition() {
|
||||
return pathfinder.pathPosition;
|
||||
return null; // TODO
|
||||
//return pathfinder.pathPosition;
|
||||
}
|
||||
|
||||
public @NotNull Entity getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public @NotNull PFPathingEntity getPathingEntity() {
|
||||
return pathfinder.pathingEntity;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void setPathFinder(@Nullable HydrazinePathFinder pathFinder) {
|
||||
pathfinder.pathFinder = pathFinder;
|
||||
}
|
||||
|
||||
private float getAttributeValue(@NotNull Attribute attribute) {
|
||||
if (entity instanceof LivingEntity) {
|
||||
return ((LivingEntity) entity).getAttributeValue(attribute);
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.model.IBlockDescription;
|
||||
import com.extollit.gaming.ai.path.model.IBlockObject;
|
||||
import com.extollit.linalg.immutable.AxisAlignedBBox;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.vectrix.flare.fastutil.Short2ObjectSyncMap;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public final class PFBlock implements IBlockDescription, IBlockObject {
|
||||
private static final Short2ObjectSyncMap<PFBlock> BLOCK_DESCRIPTION_MAP = Short2ObjectSyncMap.hashmap();
|
||||
|
||||
/**
|
||||
* Gets the {@link PFBlock} linked to the block state id.
|
||||
* <p>
|
||||
* Cache the result if it is not already.
|
||||
*
|
||||
* @param block the block
|
||||
* @return the {@link PFBlock} linked to {@code blockStateId}
|
||||
*/
|
||||
public static @NotNull PFBlock get(@NotNull Block block) {
|
||||
return BLOCK_DESCRIPTION_MAP.computeIfAbsent(block.stateId(), state -> new PFBlock(block));
|
||||
}
|
||||
|
||||
private final Block block;
|
||||
|
||||
PFBlock(Block block) {
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AxisAlignedBBox bounds() {
|
||||
return new AxisAlignedBBox(
|
||||
0, 0, 0,
|
||||
1, 1, 1
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFenceLike() {
|
||||
// TODO: Use Hitbox
|
||||
// Return fences, fencegates and walls.
|
||||
// It just so happens that their namespace IDs contain "fence".
|
||||
if (block.namespace().asString().contains("fence")) {
|
||||
return true;
|
||||
}
|
||||
// Return all walls
|
||||
// It just so happens that their namespace IDs all end with "door".
|
||||
return block.namespace().asString().endsWith("wall");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClimbable() {
|
||||
// Return ladders and vines (including weeping and twisting vines)
|
||||
// Note that no other Namespace IDs contain "vine" except vines.
|
||||
return block.compare(Block.LADDER) || block.namespace().asString().contains("vine");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDoor() {
|
||||
// Return all normal doors and trap doors.
|
||||
// It just so happens that their namespace IDs all end with "door".
|
||||
return block.namespace().asString().endsWith("door");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIntractable() {
|
||||
// TODO: Interactability of blocks.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImpeding() {
|
||||
return block.isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFullyBounded() {
|
||||
// TODO: Use Hitbox (would probably be faster as well)
|
||||
// Return false for anything that does not have a full hitbox but impedes
|
||||
// e.g. Anvils, Lilypads, Ladders, Walls, Fences, EnchantmentTables
|
||||
// Fences & Walls
|
||||
if (isFenceLike()) {
|
||||
return false;
|
||||
}
|
||||
// Ladders and Vines
|
||||
if (isClimbable()) {
|
||||
return false;
|
||||
}
|
||||
// All doors/trapdoors.
|
||||
if (isDoor()) {
|
||||
return false;
|
||||
}
|
||||
if (block.name().startsWith("potted")) {
|
||||
return false;
|
||||
}
|
||||
// Skulls & Heads
|
||||
if (block.name().contains("skull") || block.name().contains("head")) {
|
||||
// NOTE: blocks.getName().contains("head") also matches Piston_Head
|
||||
// I could not find out by documentation if piston_head is fully bounded, I would presume it is NOT.
|
||||
return false;
|
||||
}
|
||||
// Carpets
|
||||
if (block.name().endsWith("carpet")) {
|
||||
return false;
|
||||
}
|
||||
// Slabs
|
||||
if (block.name().contains("slab")) {
|
||||
return false;
|
||||
}
|
||||
// Beds
|
||||
if (block.name().endsWith("bed")) {
|
||||
return false;
|
||||
}
|
||||
// Glass Panes
|
||||
if (block.name().endsWith("pane")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !Block.CHORUS_FLOWER.compare(block) && !Block.CHORUS_PLANT.compare(block) && !Block.BAMBOO.compare(block)
|
||||
&& !Block.BAMBOO_SAPLING.compare(block) && !Block.SEA_PICKLE.compare(block)
|
||||
&& !Block.TURTLE_EGG.compare(block) && !Block.SNOW.compare(block) && !Block.FLOWER_POT.compare(block)
|
||||
&& !Block.LILY_PAD.compare(block) && !Block.ANVIL.compare(block) && !Block.CHIPPED_ANVIL.compare(block)
|
||||
&& !Block.DAMAGED_ANVIL.compare(block) && !Block.CAKE.compare(block) && !Block.CACTUS.compare(block)
|
||||
&& !Block.BREWING_STAND.compare(block) && !Block.LECTERN.compare(block)
|
||||
&& !Block.DAYLIGHT_DETECTOR.compare(block) && !Block.CAMPFIRE.compare(block)
|
||||
&& !Block.SOUL_CAMPFIRE.compare(block) && !Block.ENCHANTING_TABLE.compare(block)
|
||||
&& !Block.CHEST.compare(block) && !Block.ENDER_CHEST.compare(block) && !Block.GRINDSTONE.compare(block)
|
||||
&& !Block.TRAPPED_CHEST.compare(block) && !Block.SOUL_SAND.compare(block)
|
||||
&& !Block.SOUL_SOIL.compare(block) && !Block.LANTERN.compare(block) && !Block.COCOA.compare(block)
|
||||
&& !Block.CONDUIT.compare(block) && !Block.DIRT_PATH.compare(block) && !Block.FARMLAND.compare(block)
|
||||
&& !Block.END_ROD.compare(block) && !Block.STONECUTTER.compare(block) && !Block.BELL.compare(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLiquid() {
|
||||
return block.isLiquid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncinerating() {
|
||||
return block == Block.LAVA || block == Block.FIRE || block == Block.SOUL_FIRE;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList;
|
||||
import com.extollit.gaming.ai.path.model.IBlockDescription;
|
||||
import com.extollit.gaming.ai.path.model.IColumnarSpace;
|
||||
import com.extollit.gaming.ai.path.model.IInstanceSpace;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public final class PFColumnarSpace implements IColumnarSpace {
|
||||
private final ColumnarOcclusionFieldList occlusionFieldList = new ColumnarOcclusionFieldList(this);
|
||||
private final PFInstanceSpace instanceSpace;
|
||||
private final Chunk chunk;
|
||||
|
||||
PFColumnarSpace(PFInstanceSpace instanceSpace, Chunk chunk) {
|
||||
this.instanceSpace = instanceSpace;
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBlockDescription blockAt(int x, int y, int z) {
|
||||
final Block block = chunk.getBlock(x, y, z);
|
||||
return PFBlock.get(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int metaDataAt(int x, int y, int z) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnarOcclusionFieldList occlusionFields() {
|
||||
return occlusionFieldList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IInstanceSpace instance() {
|
||||
return instanceSpace;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.model.IBlockObject;
|
||||
import com.extollit.gaming.ai.path.model.IColumnarSpace;
|
||||
import com.extollit.gaming.ai.path.model.IInstanceSpace;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class PFInstanceSpace implements IInstanceSpace {
|
||||
private final Instance instance;
|
||||
private final Map<Chunk, PFColumnarSpace> chunkSpaceMap = new ConcurrentHashMap<>();
|
||||
|
||||
public PFInstanceSpace(Instance instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBlockObject blockObjectAt(int x, int y, int z) {
|
||||
final Block block = instance.getBlock(x, y, z);
|
||||
return PFBlock.get(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IColumnarSpace columnarSpaceAt(int cx, int cz) {
|
||||
final Chunk chunk = instance.getChunk(cx, cz);
|
||||
if (chunk == null) return null;
|
||||
return chunkSpaceMap.computeIfAbsent(chunk, c -> {
|
||||
final PFColumnarSpace cs = new PFColumnarSpace(this, c);
|
||||
c.setColumnarSpace(cs);
|
||||
return cs;
|
||||
});
|
||||
}
|
||||
|
||||
public Instance getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import com.extollit.gaming.ai.path.model.Gravitation;
|
||||
import com.extollit.gaming.ai.path.model.IPathingEntity;
|
||||
import com.extollit.gaming.ai.path.model.Passibility;
|
||||
import com.extollit.linalg.immutable.Vec3d;
|
||||
import net.minestom.server.attribute.Attribute;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public final class PFPathingEntity implements IPathingEntity {
|
||||
private final Navigator navigator;
|
||||
private final Entity entity;
|
||||
|
||||
private float searchRange;
|
||||
|
||||
// Capacities
|
||||
private boolean fireResistant;
|
||||
private boolean cautious;
|
||||
private boolean climber;
|
||||
private boolean swimmer;
|
||||
private boolean aquatic;
|
||||
private boolean avian;
|
||||
private boolean aquaphobic;
|
||||
private boolean avoidsDoorways;
|
||||
private boolean opensDoors;
|
||||
|
||||
public PFPathingEntity(Navigator navigator) {
|
||||
this.navigator = navigator;
|
||||
this.entity = navigator.getEntity();
|
||||
|
||||
this.searchRange = getAttributeValue(Attribute.FOLLOW_RANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int age() {
|
||||
return (int) entity.getAliveTicks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean bound() {
|
||||
return entity.hasVelocity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float searchRange() {
|
||||
return searchRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the search range of the entity
|
||||
*
|
||||
* @param searchRange the new entity's search range
|
||||
*/
|
||||
public void setSearchRange(float searchRange) {
|
||||
this.searchRange = searchRange;
|
||||
}
|
||||
|
||||
public boolean isFireResistant() {
|
||||
return fireResistant;
|
||||
}
|
||||
|
||||
public void setFireResistant(boolean fireResistant) {
|
||||
this.fireResistant = fireResistant;
|
||||
}
|
||||
|
||||
public boolean isCautious() {
|
||||
return cautious;
|
||||
}
|
||||
|
||||
public void setCautious(boolean cautious) {
|
||||
this.cautious = cautious;
|
||||
}
|
||||
|
||||
public boolean isClimber() {
|
||||
return climber;
|
||||
}
|
||||
|
||||
public void setClimber(boolean climber) {
|
||||
this.climber = climber;
|
||||
}
|
||||
|
||||
public boolean isSwimmer() {
|
||||
return swimmer;
|
||||
}
|
||||
|
||||
public void setSwimmer(boolean swimmer) {
|
||||
this.swimmer = swimmer;
|
||||
}
|
||||
|
||||
public boolean isAquatic() {
|
||||
return aquatic;
|
||||
}
|
||||
|
||||
public void setAquatic(boolean aquatic) {
|
||||
this.aquatic = aquatic;
|
||||
}
|
||||
|
||||
public boolean isAvian() {
|
||||
return avian;
|
||||
}
|
||||
|
||||
public void setAvian(boolean avian) {
|
||||
this.avian = avian;
|
||||
}
|
||||
|
||||
public boolean isAquaphobic() {
|
||||
return aquaphobic;
|
||||
}
|
||||
|
||||
public void setAquaphobic(boolean aquaphobic) {
|
||||
this.aquaphobic = aquaphobic;
|
||||
}
|
||||
|
||||
public boolean isAvoidsDoorways() {
|
||||
return avoidsDoorways;
|
||||
}
|
||||
|
||||
public void setAvoidsDoorways(boolean avoidsDoorways) {
|
||||
this.avoidsDoorways = avoidsDoorways;
|
||||
}
|
||||
|
||||
public boolean isOpensDoors() {
|
||||
return opensDoors;
|
||||
}
|
||||
|
||||
public void setOpensDoors(boolean opensDoors) {
|
||||
this.opensDoors = opensDoors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Capabilities capabilities() {
|
||||
return new Capabilities() {
|
||||
@Override
|
||||
public float speed() {
|
||||
return getAttributeValue(Attribute.MOVEMENT_SPEED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fireResistant() {
|
||||
return fireResistant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cautious() {
|
||||
return cautious;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean climber() {
|
||||
return climber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean swimmer() {
|
||||
return swimmer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean aquatic() {
|
||||
return aquatic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean avian() {
|
||||
return avian;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean aquaphobic() {
|
||||
return aquaphobic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean avoidsDoorways() {
|
||||
return avoidsDoorways;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean opensDoors() {
|
||||
return opensDoors;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Vec3d position, Passibility passibility, Gravitation gravitation) {
|
||||
final Point targetPosition = new Vec(position.x, position.y, position.z);
|
||||
this.navigator.moveTowards(targetPosition, getAttributeValue(Attribute.MOVEMENT_SPEED));
|
||||
final double entityY = entity.getPosition().y();
|
||||
if (entityY < targetPosition.y()) {
|
||||
this.navigator.jump(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3d coordinates() {
|
||||
final var position = entity.getPosition();
|
||||
return new Vec3d(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float width() {
|
||||
return (float) entity.getBoundingBox().width();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float height() {
|
||||
return (float) entity.getBoundingBox().height();
|
||||
}
|
||||
|
||||
private float getAttributeValue(@NotNull Attribute attribute) {
|
||||
if (entity instanceof LivingEntity) {
|
||||
return ((LivingEntity) entity).getAttributeValue(attribute);
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import net.minestom.server.collision.BoundingBox;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
final class PathfindUtils {
|
||||
public static boolean isBlocked(@NotNull Point point, @NotNull BoundingBox box,
|
||||
@NotNull Block.Getter getter, double entityPadding) {
|
||||
Point relStart = box.relativeStart();
|
||||
Point relEnd = box.relativeEnd();
|
||||
relStart = relStart.mul(2, 0, 2).sub(entityPadding, 0, entityPadding);
|
||||
relEnd = relEnd.mul(2, 0, 2).add(entityPadding, 0, entityPadding);
|
||||
return getter.getBlock(point.add(relStart.x(), relStart.y(), relStart.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relStart.x(), relStart.y(), relEnd.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relStart.x(), relEnd.y(), relStart.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relStart.x(), relEnd.y(), relEnd.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relEnd.x(), relStart.y(), relStart.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relEnd.x(), relStart.y(), relEnd.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relEnd.x(), relEnd.y(), relStart.z())).isSolid() ||
|
||||
getter.getBlock(point.add(relEnd.x(), relEnd.y(), relEnd.z())).isSolid();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package net.minestom.server.entity.pathfinding;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.network.packet.server.ServerPacket;
|
||||
import net.minestom.server.particle.Particle;
|
||||
import net.minestom.server.particle.ParticleCreator;
|
||||
import net.minestom.server.utils.PacketUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
final class PathfinderImpl implements Pathfinder {
|
||||
private static final double DELTA = 0.01;
|
||||
private final Entity entity;
|
||||
|
||||
volatile Point pathPosition;
|
||||
volatile List<Point> path;
|
||||
|
||||
public PathfinderImpl(Entity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point nextPoint(Point currentPoint) {
|
||||
var path = this.path;
|
||||
if (path == null || path.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the closest point to `pathPosition` in the path that is also closer to `currentPoint`
|
||||
var closestPoint = path.get(0);
|
||||
var closestDistance = currentPoint.distance(closestPoint);
|
||||
int index = 0;
|
||||
for (var point : path) {
|
||||
var distance = currentPoint.distance(point);
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
index = path.indexOf(point);
|
||||
}
|
||||
}
|
||||
if (index < path.size() - 1) {
|
||||
return path.get(index + 1);
|
||||
} else return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(Point target) {
|
||||
var start = entity.getPosition();
|
||||
var result = findPath(start, target, 1);
|
||||
this.path = result.stream().toList();
|
||||
this.pathPosition = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Point> forcePath(Point target) {
|
||||
updatePath(target);
|
||||
return path;
|
||||
}
|
||||
|
||||
@Nullable Queue<Point> findPath(Point start, Point goal, double step) {
|
||||
Comparator<Point> distanceCost = Comparator.comparingDouble(p ->
|
||||
p.distance(start) +
|
||||
p.distance(goal) +
|
||||
getCost(p, p)
|
||||
);
|
||||
// The queue of nodes to be evaluated next
|
||||
Queue<Point> next = new PriorityQueue<>(distanceCost);
|
||||
Set<Point> nextSet = new HashSet<>();
|
||||
next.add(start);
|
||||
|
||||
// The set of nodes already evaluated
|
||||
Set<Point> closedSet = new HashSet<>();
|
||||
|
||||
// The map from each node to its parent node
|
||||
Map<Point, Point> cameFrom = new HashMap<>();
|
||||
|
||||
while (!next.isEmpty()) {
|
||||
Point current = next.remove();
|
||||
nextSet.remove(current);
|
||||
|
||||
// TODO: Remove this debug
|
||||
ServerPacket packet = ParticleCreator.createParticlePacket(
|
||||
Particle.FLAME,
|
||||
current.x(), current.y(), current.z(),
|
||||
0, 0, 0,
|
||||
1
|
||||
);
|
||||
PacketUtils.sendGroupedPacket(MinecraftServer.getConnectionManager().getOnlinePlayers(), packet);
|
||||
|
||||
// Return if the current node is the goal
|
||||
if (current.distance(goal) - DELTA <= step) {
|
||||
return reconstructPath(cameFrom, current);
|
||||
}
|
||||
|
||||
// Else, look at the neighbors
|
||||
for (Point neighbor : neighbors(current, step)) {
|
||||
// If the neighbor is already evaluated, or scheduled for evaluation, skip it
|
||||
if (closedSet.contains(neighbor) || nextSet.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the neighbor is not walkable, skip it
|
||||
if (isBlocked(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Else, add it to the queue
|
||||
next.add(neighbor);
|
||||
nextSet.add(neighbor);
|
||||
cameFrom.put(neighbor, current);
|
||||
}
|
||||
|
||||
// Mark the current node as visited
|
||||
closedSet.add(current);
|
||||
}
|
||||
|
||||
// No path found
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Queue<Point> reconstructPath(Map<Point, Point> cameFrom, Point current) {
|
||||
Deque<Point> path = new ArrayDeque<>();
|
||||
path.add(current);
|
||||
while (cameFrom.containsKey(current)) {
|
||||
current = cameFrom.get(current);
|
||||
path.addFirst(current);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static Point[] neighbors(Point point, double step) {
|
||||
return new Point[]{
|
||||
// Direct neighbors
|
||||
point.add(step, 0, 0),
|
||||
point.add(-step, 0, 0),
|
||||
point.add(0, step, 0),
|
||||
point.add(0, -step, 0),
|
||||
point.add(0, 0, step),
|
||||
point.add(0, 0, -step),
|
||||
|
||||
// Diagonal neighbors
|
||||
point.add(step, step, 0),
|
||||
point.add(-step, -step, 0),
|
||||
point.add(step, -step, 0),
|
||||
point.add(-step, step, 0),
|
||||
point.add(0, step, step),
|
||||
point.add(0, -step, -step),
|
||||
point.add(step, 0, step),
|
||||
point.add(-step, 0, -step),
|
||||
|
||||
// Diagonal Diagonal neighbors
|
||||
point.add(step, step, step),
|
||||
point.add(-step, -step, -step),
|
||||
point.add(step, -step, -step),
|
||||
point.add(-step, step, -step),
|
||||
point.add(step, step, -step),
|
||||
point.add(-step, -step, step),
|
||||
point.add(step, -step, step),
|
||||
point.add(-step, step, step)
|
||||
};
|
||||
}
|
||||
|
||||
public double getCost(Point from, Point to) {
|
||||
// TODO: Implement line intersection algorithm to determine the cost
|
||||
// The current algorithm is flawed and may tell the navigator to move through very
|
||||
// specific corners that are not actually possible
|
||||
Instance instance = entity.getInstance();
|
||||
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
|
||||
Block block = instance.getBlock(to);
|
||||
if (block.isSolid()) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
return block.registry().speedFactor();
|
||||
}
|
||||
|
||||
public boolean isBlocked(Point point) {
|
||||
Instance instance = entity.getInstance();
|
||||
Objects.requireNonNull(instance, "The navigator must be in an instance while pathfinding.");
|
||||
return PathfindUtils.isBlocked(point, entity.getBoundingBox(), instance, 0.1);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import net.minestom.server.Viewable;
|
|||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.PFColumnarSpace;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
|
||||
import net.minestom.server.snapshot.Snapshotable;
|
||||
|
@ -54,9 +53,6 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
|
|||
protected volatile boolean loaded = true;
|
||||
private final ChunkView viewers;
|
||||
|
||||
// Path finding
|
||||
protected PFColumnarSpace columnarSpace;
|
||||
|
||||
// Data
|
||||
private final MutableNBTCompound nbt = new MutableNBTCompound();
|
||||
|
||||
|
@ -245,15 +241,6 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
|
|||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes this chunk columnar space.
|
||||
*
|
||||
* @param columnarSpace the new columnar space
|
||||
*/
|
||||
public void setColumnarSpace(PFColumnarSpace columnarSpace) {
|
||||
this.columnarSpace = columnarSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to verify if the chunk should still be kept in memory.
|
||||
*
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package net.minestom.server.instance;
|
||||
|
||||
import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.PFBlock;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.instance.block.BlockHandler;
|
||||
import net.minestom.server.network.packet.server.CachedPacket;
|
||||
|
@ -63,12 +61,6 @@ public class DynamicChunk extends Chunk {
|
|||
this.lastChange = System.currentTimeMillis();
|
||||
this.chunkCache.invalidate();
|
||||
this.lightCache.invalidate();
|
||||
// Update pathfinder
|
||||
if (columnarSpace != null) {
|
||||
final ColumnarOcclusionFieldList columnarOcclusionFieldList = columnarSpace.occlusionFields();
|
||||
final var blockDescription = PFBlock.get(block);
|
||||
columnarOcclusionFieldList.onBlockChanged(x, y, z, blockDescription, 0);
|
||||
}
|
||||
Section section = getSectionAt(y);
|
||||
section.blockPalette()
|
||||
.set(toSectionRelativeCoordinate(x), toSectionRelativeCoordinate(y), toSectionRelativeCoordinate(z), block.stateId());
|
||||
|
|
|
@ -11,7 +11,6 @@ import net.minestom.server.entity.Entity;
|
|||
import net.minestom.server.entity.EntityCreature;
|
||||
import net.minestom.server.entity.ExperienceOrb;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.PFInstanceSpace;
|
||||
import net.minestom.server.event.EventDispatcher;
|
||||
import net.minestom.server.event.instance.InstanceTickEvent;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
|
@ -93,9 +92,6 @@ public abstract class Instance implements Block.Getter, Block.Setter, Tickable,
|
|||
// the explosion supplier
|
||||
private ExplosionSupplier explosionSupplier;
|
||||
|
||||
// Pathfinder
|
||||
private final PFInstanceSpace instanceSpace = new PFInstanceSpace(this);
|
||||
|
||||
// Adventure
|
||||
private final Pointers pointers;
|
||||
|
||||
|
@ -676,18 +672,6 @@ public abstract class Instance implements Block.Getter, Block.Setter, Tickable,
|
|||
this.explosionSupplier = supplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the instance space.
|
||||
* <p>
|
||||
* Used by the pathfinder for entities.
|
||||
*
|
||||
* @return the instance space
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public @NotNull PFInstanceSpace getInstanceSpace() {
|
||||
return instanceSpace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Pointers pointers() {
|
||||
return this.pointers;
|
||||
|
|
Loading…
Reference in New Issue