Pathfinding

This commit is contained in:
iam4722202468 2024-02-10 13:02:02 -05:00
parent d03466e5a3
commit 17ed34196f
No known key found for this signature in database
GPG Key ID: 4D8728E97DA5B8F9
23 changed files with 1054 additions and 582 deletions

View File

@ -69,7 +69,6 @@ dependencies {
api(libs.slf4j)
api(libs.jetbrainsAnnotations)
api(libs.bundles.adventure)
api(libs.hydrazine)
api(libs.bundles.kotlin)
api(libs.bundles.hephaistos)
implementation(libs.minestomData)

View File

@ -9,10 +9,7 @@ import net.minestom.server.adventure.MinestomAdventure;
import net.minestom.server.adventure.audience.Audiences;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.GameMode;
import net.minestom.server.entity.ItemEntity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.*;
import net.minestom.server.entity.damage.Damage;
import net.minestom.server.entity.fakeplayer.FakePlayer;
import net.minestom.server.event.Event;
@ -76,18 +73,18 @@ public class PlayerInit {
})
.addListener(ItemDropEvent.class, event -> {
final Player player = event.getPlayer();
ItemStack droppedItem = event.getItemStack();
Pos playerPos = player.getPosition();
ItemEntity itemEntity = new ItemEntity(droppedItem);
itemEntity.setPickupDelay(Duration.of(500, TimeUnit.MILLISECOND));
itemEntity.setInstance(player.getInstance(), playerPos.withY(y -> y + 1.5));
Vec velocity = playerPos.direction().mul(6);
itemEntity.setVelocity(velocity);
for (int i = 0; i < 1; ++i) {
EntityCreature zombie = new EntityCreature(EntityType.ZOMBIE) {
@Override
public void update(long time) {
super.update(time);
this.getNavigator().setPathTo(player.getPosition());
}
};
FakePlayer.initPlayer(UUID.randomUUID(), "fake123", fp -> {
System.out.println("fp = " + fp);
});
zombie.setInstance(player.getInstance(), player.getPosition());
}
})
.addListener(PlayerDisconnectEvent.class, event -> System.out.println("DISCONNECTION " + event.getPlayer().getUsername()))
.addListener(AsyncPlayerConfigurationEvent.class, event -> {
@ -177,6 +174,11 @@ public class PlayerInit {
instanceContainer.setTimeRate(0);
instanceContainer.setTime(18000);
// var i2 = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null, NamespaceID.from("minestom:demo"));
// instanceManager.registerInstance(i2);
// i2.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.GRASS_BLOCK));
// i2.setChunkSupplier(LightingChunk::new);
// var i2 = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null, NamespaceID.from("minestom:demo"));
// instanceManager.registerInstance(i2);
// i2.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.GRASS_BLOCK));

View File

@ -9,7 +9,6 @@ public final class SweepResult {
double res;
double normalX, normalY, normalZ;
Point collidedPosition;
Point collidedPos;
Shape collidedShape;
/**
@ -26,6 +25,6 @@ public final class SweepResult {
this.normalY = normalY;
this.normalZ = normalZ;
this.collidedShape = collidedShape;
this.collidedPos = collidedPos;
this.collidedPosition = collidedPos;
}
}

View File

@ -55,7 +55,6 @@ import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.block.BlockIterator;
import net.minestom.server.utils.chunk.ChunkCache;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.entity.EntityUtils;
import net.minestom.server.utils.player.PlayerUtils;
import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit;
@ -278,7 +277,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
}
public boolean isOnGround() {
return onGround || EntityUtils.isOnGround(this) /* backup for levitating entities */;
return onGround;
}
/**

View File

@ -1,6 +1,5 @@
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;
@ -56,8 +55,7 @@ public class EntityCreature extends LivingEntity implements NavigableEntity, Ent
@Override
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
this.navigator.setPathFinder(new HydrazinePathFinder(navigator.getPathingEntity(), instance.getInstanceSpace()));
this.navigator.reset();
return super.setInstance(instance, spawnPosition);
}

View File

@ -75,7 +75,7 @@ public class FollowTargetGoal extends GoalSelector {
return;
}
final Pos targetPos = entityCreature.getTarget() != null ? entityCreature.getTarget().getPosition() : null;
if (targetPos != null && !targetPos.samePoint(lastTargetPos)) {
if (targetPos != null && !targetPos.sameBlock(lastTargetPos)) {
this.lastUpdateTime = time;
this.lastTargetPos = targetPos;
this.entityCreature.getNavigator().setPathTo(targetPos);

View File

@ -1,6 +1,5 @@
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;
@ -134,8 +133,7 @@ public class FakePlayer extends Player implements NavigableEntity {
@Override
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
this.navigator.setPathFinder(new HydrazinePathFinder(navigator.getPathingEntity(), instance.getInstanceSpace()));
this.navigator.reset();
return super.setInstance(instance, spawnPosition);
}

View File

@ -1,10 +1,8 @@
package net.minestom.server.entity.pathfinding;
import com.extollit.gaming.ai.path.HydrazinePathFinder;
import com.extollit.gaming.ai.path.PathOptions;
import com.extollit.gaming.ai.path.model.IPath;
import net.minestom.server.attribute.Attribute;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.collision.PhysicsResult;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
@ -13,27 +11,40 @@ import net.minestom.server.entity.LivingEntity;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.position.PositionUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
// TODO all pathfinding requests could be processed in another thread
/**
* Necessary object for all {@link NavigableEntity}.
*/
public final class Navigator {
private final PFPathingEntity pathingEntity;
private HydrazinePathFinder pathFinder;
private Point pathPosition;
private Point goalPosition;
private final Entity entity;
// Essentially a double buffer. Wait until a path is done computing before replpacing the old one.
private PPath computingPath;
private PPath path;
private double minimumDistance;
private float movementSpeed = 0.1f;
public Navigator(@NotNull Entity entity) {
this.entity = entity;
this.pathingEntity = new PFPathingEntity(this);
}
public PPath.PathState getState() {
if (path == null && computingPath == null) return PPath.PathState.INVALID;
if (path == null) return computingPath.getState();
return path.getState();
}
/**
@ -41,29 +52,48 @@ public final class Navigator {
* Gravity is still applied but the entity will not attempt to jump
* Also update the yaw/pitch of the entity to look along 'direction'
*
* @param direction the targeted position
* @param speed define how far the entity will move
* @param direction the targeted position
* @param speed define how far the entity will move
* @param capabilities
*/
public PhysicsResult moveTowards(@NotNull Point direction, double speed) {
public void moveTowards(@NotNull Point direction, double speed, PPath.PathfinderCapabilities capabilities, Point lookAt) {
final Pos position = entity.getPosition();
final double dx = direction.x() - position.x();
final double dy = direction.y() - position.y();
final double dz = direction.z() - position.z();
final double dxLook = lookAt.x() - position.x();
final double dyLook = lookAt.y() - position.y();
final double dzLook = lookAt.z() - position.z();
// the purpose of these few lines is to slow down entities when they reach their destination
final double distSquared = dx * dx + dy * dy + dz * dz;
if (speed > distSquared) {
speed = distSquared;
}
boolean inWater = false;
var instance = entity.getInstance();
if (instance != null)
if (instance.getBlock(position).isLiquid()) {
speed *= capabilities.swimSpeedModifier();
inWater = true;
}
final double radians = Math.atan2(dz, dx);
final double speedX = Math.cos(radians) * speed;
final double speedY = dy * speed;
final double speedZ = Math.sin(radians) * speed;
final float yaw = PositionUtils.getLookYaw(dx, dz);
final float pitch = PositionUtils.getLookPitch(dx, dy, dz);
// Prevent ghosting
final float yaw = PositionUtils.getLookYaw(dxLook, dzLook);
final float pitch = PositionUtils.getLookPitch(dxLook, dyLook, dzLook);
final double speedY = (capabilities.type() == PPath.PathfinderType.AQUATIC
|| capabilities.type() == PPath.PathfinderType.FLYING
|| (capabilities.type() == PPath.PathfinderType.AMPHIBIOUS && inWater))
? Math.signum(dy) * 0.5 * speed
: 0;
final var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, speedY, speedZ));
this.entity.refreshPosition(physicsResult.newPosition().withView(yaw, pitch));
return physicsResult;
this.entity.refreshPosition(Pos.fromPoint(physicsResult.newPosition()).withView(yaw, pitch));
}
public void jump(float height) {
@ -71,37 +101,39 @@ public final class Navigator {
this.entity.setVelocity(new Vec(0, height * 2.5f, 0));
}
public synchronized boolean setPathTo(@Nullable Point point) {
BoundingBox bb = this.entity.getBoundingBox();
double centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2;
return setPathTo(point, centerToCorner, null);
}
public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance, Runnable onComplete) {
return setPathTo(point, minimumDistance, 50, 20, PPath.PathfinderType.LAND, onComplete);
}
/**
* Retrieves the path to {@code position} and ask the entity to follow the path.
* <p>
* Can be set to null to reset the pathfinder.
* <p>
* The position is cloned, if you want the entity to continually follow this position object
* you need to call this when you want the path to update.
* Sets the path to {@code position} and ask the entity to follow the path.
*
* @param point the position to find the path to, null to reset the pathfinder
* @param bestEffort whether to use the best-effort algorithm to the destination,
* if false then this method is more likely to return immediately
* @param point the position to find the path to, null to reset the pathfinder
* @param minimumDistance distance to target when completed
* @param maxDistance maximum search distance
* @param pathVariance how far to search off of the direct path. For open worlds, this can be low (around 20) and for large mazes this needs to be very high.
* @param onComplete called when the path has been completed
* @return true if a path has been found
*/
public synchronized boolean setPathTo(@Nullable Point point, boolean bestEffort) {
if (point != null && pathPosition != null && point.samePoint(pathPosition)) {
// Tried to set path to the same target position
return false;
}
public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance, double maxDistance, double pathVariance, PPath.PathfinderType type, Runnable onComplete) {
final Instance instance = entity.getInstance();
if (pathFinder == null) {
// Unexpected error
return false;
}
this.pathFinder.reset();
if (point == null) {
this.path = null;
return false;
}
// Can't path with a null instance.
if (instance == null) {
this.path = null;
return false;
}
// Can't path outside the world border
final WorldBorder worldBorder = instance.getWorldBorder();
if (!worldBorder.isInside(point)) {
@ -113,34 +145,109 @@ public final class Navigator {
return false;
}
final PathOptions pathOptions = new PathOptions()
.targetingStrategy(bestEffort ? PathOptions.TargetingStrategy.gravitySnap :
PathOptions.TargetingStrategy.none);
final IPath path = pathFinder.initiatePathTo(
point.x(),
point.y(),
point.z(),
pathOptions);
this.minimumDistance = minimumDistance;
if (this.entity.getPosition().distance(point) < minimumDistance) {
if (onComplete != null) onComplete.run();
return false;
}
final boolean success = path != null;
this.pathPosition = success ? point : null;
if (point.sameBlock(entity.getPosition())) {
if (onComplete != null) onComplete.run();
return false;
}
if (this.computingPath != null) this.computingPath.setState(PPath.PathState.TERMINATING);
this.computingPath = PathGenerator.generate(instance,
this.entity.getPosition(),
point,
minimumDistance, maxDistance,
pathVariance,
this.entity.getBoundingBox(),
new PPath.PathfinderCapabilities(type, true, true, 0.4f),
this.entity.isOnGround(),
onComplete);
final boolean success = computingPath != null;
this.goalPosition = success ? point : null;
return success;
}
/**
* @see #setPathTo(Point, boolean) with {@code bestEffort} sets to {@code true}.
*/
public boolean setPathTo(@Nullable Point position) {
return setPathTo(position, true);
}
@ApiStatus.Internal
public synchronized void tick() {
if (pathPosition == null) return; // No path
if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead())
return; // No pathfinding tick for dead entities
if (pathFinder.updatePathFor(pathingEntity) == null) {
reset();
if (goalPosition == null) return; // No path
if (entity instanceof LivingEntity && ((LivingEntity) entity).isDead()) return; // No pathfinding tick for dead entities
if (computingPath != null && computingPath.getState() == PPath.PathState.COMPUTED) {
path = computingPath;
computingPath = null;
}
if (path == null) return;
// If the path is computed start following it
if (path.getState() == PPath.PathState.COMPUTED) {
path.setState(PPath.PathState.FOLLOWING);
// Remove nodes that are too close to the start. Prevents doubling back to hit points that have already been hit
for (int i = 0; i < path.getNodes().size(); i++) {
if (path.getNodes().get(i).point.sameBlock(entity.getPosition())) {
path.getNodes().subList(0, i).clear();
break;
}
}
}
// If the state is not following, wait until it is
if (path.getState() != PPath.PathState.FOLLOWING) return;
// If we're near the entity, we're done
if (this.entity.getPosition().distance(goalPosition) < minimumDistance) {
path.runComplete();
path = null;
return;
}
Point currentTarget = path.getCurrent();
Point nextTarget = path.getNext();
// If we're at the end of the path, navigate directly to the entity
if (nextTarget == null) {
path.setState(PPath.PathState.INVALID);
return;
}
// Repath
if (currentTarget == null || path.getCurrentType() == PNode.NodeType.REPATH || path.getCurrentType() == null) {
if (computingPath != null && computingPath.getState() == PPath.PathState.CALCULATING) return;
computingPath = PathGenerator.generate(entity.getInstance(),
entity.getPosition(),
Pos.fromPoint(goalPosition),
minimumDistance, path.maxDistance(),
path.pathVariance(), entity.getBoundingBox(), path.capabilities(), this.entity.isOnGround(), null);
return;
}
if (entity instanceof LivingEntity living) {
movementSpeed = living.getAttribute(Attribute.MOVEMENT_SPEED).getBaseValue();
}
boolean nextIsRepath = nextTarget.sameBlock(Pos.ZERO);
// drawPath(path);
moveTowards(currentTarget, movementSpeed, path.capabilities(), nextIsRepath ? currentTarget : nextTarget);
if (entity.getPosition().sameBlock(currentTarget)) {
path.next();
return;
}
if ((path.getCurrentType() == PNode.NodeType.JUMP)
&& entity.isOnGround()
&& path.capabilities().canJump()
) {
jump(4f);
}
}
@ -149,26 +256,49 @@ public final class Navigator {
*
* @return the target pathfinder position, null if there is no one
*/
public @Nullable Point getPathPosition() {
return pathPosition;
public @Nullable Point getGoalPosition() {
return goalPosition;
}
public @NotNull Entity getEntity() {
return entity;
}
@ApiStatus.Internal
public @NotNull PFPathingEntity getPathingEntity() {
return pathingEntity;
public void reset() {
if (this.path != null) this.path.setState(PPath.PathState.TERMINATING);
this.goalPosition = null;
this.path = null;
if (this.computingPath != null) this.computingPath.setState(PPath.PathState.TERMINATING);
this.computingPath = null;
}
@ApiStatus.Internal
public void setPathFinder(@Nullable HydrazinePathFinder pathFinder) {
this.pathFinder = pathFinder;
public boolean isComplete() {
if (this.path == null) return true;
return goalPosition == null || entity.getPosition().sameBlock(goalPosition);
}
private void reset() {
this.pathPosition = null;
this.pathFinder.reset();
public List<PNode> getNodes() {
if (this.path == null && computingPath == null) return null;
if (this.path == null) return computingPath.getNodes();
return this.path.getNodes();
}
public Point getPathPosition() {
return goalPosition;
}
/**
* Visualise path for debugging
* @param path the path to draw
*/
private void drawPath(PPath path) {
if (path == null) return;
for (PNode point : path.getNodes()) {
Point pos = point.point();
var packet = ParticleCreator.createParticlePacket(Particle.COMPOSTER, pos.x(), pos.y() + 0.5, pos.z(), 0, 0, 0, 1);
entity.sendPacketToViewers(packet);
}
}
}

View File

@ -1,100 +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.collision.Shape;
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() {
Shape shape = this.block.registry().collisionShape();
return new AxisAlignedBBox(
shape.relativeStart().x(), shape.relativeStart().y(), shape.relativeStart().z(),
shape.relativeEnd().x(), shape.relativeEnd().y(), shape.relativeEnd().z()
);
}
@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 "wall".
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() {
Shape shape = block.registry().collisionShape();
return shape.relativeStart().isZero()
&& shape.relativeEnd().x() == 1.0d
&& shape.relativeEnd().y() == 1.0d
&& shape.relativeEnd().z() == 1.0d;
}
@Override
public boolean isLiquid() {
return block.isLiquid();
}
@Override
public boolean isIncinerating() {
return block == Block.LAVA || block == Block.FIRE || block == Block.SOUL_FIRE;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,226 +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() + 0.00001D; // After any negative y movement, entities will always be extremely
// slightly below floor level. This +0.00001D is here to offset this
// error and stop the entity from permanently jumping.
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;
}
}

View File

@ -0,0 +1,388 @@
package net.minestom.server.entity.pathfinding;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.collision.PhysicsResult;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
public class PNode {
public enum NodeType {
WALK,
JUMP,
FALL,
CLIMB,
CLIMB_WALL,
SWIM,
FLY, REPATH
}
double g;
double h;
PNode parent;
Pos point;
int hashCode;
int cantor(int a, int b) {
return (a + b + 1) * (a + b) / 2 + b;
}
private PNode tempNode = null;
private NodeType type;
void setType(NodeType newType) {
this.type = newType;
}
public NodeType getType() {
return type;
}
private void setPoint(Pos point) {
this.point = point;
this.hashCode = cantor(point.blockX(), cantor(point.blockY(), point.blockZ()));
}
public PNode(Pos point, double g, double h, PNode parent) {
this(point, g, h, NodeType.WALK, parent);
}
public PNode(Pos point, double g, double h, NodeType type, PNode parent) {
this.point = new Pos(point.x(), point.y(), point.z());
this.g = g;
this.h = h;
this.parent = parent;
this.hashCode = cantor(point.blockX(), cantor(point.blockY(), point.blockZ()));
this.type = type;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (!(obj instanceof PNode other)) return false;
return this.hashCode == other.hashCode;
}
public Collection<? extends PNode> getNearby(Instance instance, Set<PNode> closed, Point goal, @NotNull BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities) {
if (capabilities.type() == PPath.PathfinderType.FLYING) return getNearbyAir(instance, closed, goal, boundingBox, capabilities);
else if (capabilities.type() == PPath.PathfinderType.AQUATIC) return getNearbyWater(instance, closed, goal, boundingBox, capabilities);
else if (capabilities.type() == PPath.PathfinderType.AMPHIBIOUS) return getNearbyAmphibious(instance, closed, goal, boundingBox, capabilities);
return getNearbyGround(instance, closed, goal, boundingBox, capabilities);
}
public Collection<? extends PNode> getNearbyWater(Instance instance, Set<PNode> closed, Point goal, @NotNull BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities) {
Collection<PNode> nearby = new ArrayList<>();
tempNode = new PNode(Pos.ZERO, 0, 0, this);
int stepSize = (int) Math.max(Math.floor(boundingBox.width() / 2), 1);
if (stepSize < 1) stepSize = 1;
for (int x = -stepSize; x <= stepSize; ++x) {
for (int z = -stepSize; z <= stepSize; ++z) {
if (x == 0 && z == 0) continue;
double cost = Math.sqrt(x * x + z * z) * 0.98;
Pos currentLevelPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 0.5);
Pos upPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 1 + 0.5);
Pos downPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() - 1 + 0.5);
if (instance.getBlock(currentLevelPoint).compare(Block.WATER)) {
var nodeWalk = createFly(instance, currentLevelPoint, boundingBox, cost, point, goal, closed);
if (nodeWalk != null && !closed.contains(nodeWalk)) nearby.add(nodeWalk);
}
if (instance.getBlock(upPoint).compare(Block.WATER)) {
var nodeJump = createFly(instance, upPoint, boundingBox, cost, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
if (instance.getBlock(downPoint).compare(Block.WATER)) {
var nodeFall = createFly(instance, downPoint, boundingBox, cost, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
}
}
}
// Straight up
Pos upPoint = point.withY(point.blockY() + 1 + 0.5);
if (instance.getBlock(upPoint).compare(Block.WATER)) {
var nodeJump = createFly(instance, upPoint, boundingBox, 2, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
// Straight down
Pos downPoint = point.withY(point.blockY() - 1 + 0.5);
if (instance.getBlock(downPoint).compare(Block.WATER)) {
var nodeFall = createFly(instance, downPoint, boundingBox, 2, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
}
return nearby;
}
public Collection<? extends PNode> getNearbyAmphibious(Instance instance, Set<PNode> closed, Point goal, @NotNull BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities) {
Collection<PNode> nearby = new ArrayList<>();
tempNode = new PNode(Pos.ZERO, 0, 0, this);
int stepSize = (int) Math.max(Math.floor(boundingBox.width() / 2), 1);
if (stepSize < 1) stepSize = 1;
for (int x = -stepSize; x <= stepSize; ++x) {
for (int z = -stepSize; z <= stepSize; ++z) {
if (x == 0 && z == 0) continue;
double cost = Math.sqrt(x * x + z * z) * 0.98;
// Land
{
Pos floorPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z);
floorPoint = gravitySnap(instance, floorPoint, boundingBox, 5);
if (floorPoint == null) continue;
if (!instance.getBlock(floorPoint).compare(Block.WATER)) {
var nodeWalk = createWalk(instance, floorPoint, boundingBox, cost, point, goal, closed);
if (nodeWalk != null && !closed.contains(nodeWalk)) nearby.add(nodeWalk);
}
for (int i = 1; i <= 1; ++i) {
Pos jumpPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).add(0, i, 0);
jumpPoint = gravitySnap(instance, jumpPoint, boundingBox, 5);
if (jumpPoint == null) continue;
if (!floorPoint.sameBlock(jumpPoint) && !instance.getBlock(jumpPoint).compare(Block.WATER)) {
var nodeJump = createJump(instance, jumpPoint, boundingBox, cost + 0.2, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
}
}
// Water
{
Pos currentLevelPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 0.5);
Pos upPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 1 + 0.5);
Pos downPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() - 1 + 0.5);
if (instance.getBlock(currentLevelPoint).compare(Block.WATER)) {
var nodeWalk = createFly(instance, currentLevelPoint, boundingBox, cost, point, goal, closed);
if (nodeWalk != null && !closed.contains(nodeWalk)) nearby.add(nodeWalk);
}
if (instance.getBlock(upPoint).compare(Block.WATER)) {
var nodeJump = createFly(instance, upPoint, boundingBox, cost, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
if (instance.getBlock(downPoint).compare(Block.WATER)) {
var nodeFall = createFly(instance, downPoint, boundingBox, cost, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
}
}
}
}
// Straight up
Pos upPoint = point.withY(point.blockY() + 1 + 0.5);
if (instance.getBlock(upPoint).compare(Block.WATER)) {
var nodeJump = createFly(instance, upPoint, boundingBox, 2, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
// Straight down
Pos downPoint = point.withY(point.blockY() - 1 + 0.5);
if (instance.getBlock(downPoint).compare(Block.WATER)) {
var nodeFall = createFly(instance, downPoint, boundingBox, 2, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
}
return nearby;
}
public Collection<? extends PNode> getNearbyAir(Instance instance, Set<PNode> closed, Point goal, @NotNull BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities) {
Collection<PNode> nearby = new ArrayList<>();
tempNode = new PNode(Pos.ZERO, 0, 0, this);
int stepSize = (int) Math.max(Math.floor(boundingBox.width() / 2), 1);
if (stepSize < 1) stepSize = 1;
for (int x = -stepSize; x <= stepSize; ++x) {
for (int z = -stepSize; z <= stepSize; ++z) {
if (x == 0 && z == 0) continue;
double cost = Math.sqrt(x * x + z * z) * 0.98;
Pos currentLevelPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 0.5);
Pos upPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() + 1 + 0.5);
Pos downPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).withY(point.blockY() - 1 + 0.5);
var nodeWalk = createFly(instance, currentLevelPoint, boundingBox, cost, point, goal, closed);
if (nodeWalk != null && !closed.contains(nodeWalk)) nearby.add(nodeWalk);
var nodeJump = createFly(instance, upPoint, boundingBox, cost, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
var nodeFall = createFly(instance, downPoint, boundingBox, cost, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
}
}
// Straight up
Pos upPoint = point.withY(point.blockY() + 1 + 0.5);
var nodeJump = createFly(instance, upPoint, boundingBox, 2, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
// Straight down
Pos downPoint = point.withY(point.blockY() - 1 + 0.5);
var nodeFall = createFly(instance, downPoint, boundingBox, 2, point, goal, closed);
if (nodeFall != null && !closed.contains(nodeFall)) nearby.add(nodeFall);
return nearby;
}
public Collection<? extends PNode> getNearbyGround(Instance instance, Set<PNode> closed, Point goal, @NotNull BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities) {
Collection<PNode> nearby = new ArrayList<>();
tempNode = new PNode(Pos.ZERO, 0, 0, this);
int stepSize = (int) Math.max(Math.floor(boundingBox.width() / 2), 1);
if (stepSize < 1) stepSize = 1;
for (int x = -stepSize; x <= stepSize; ++x) {
for (int z = -stepSize; z <= stepSize; ++z) {
if (x == 0 && z == 0) continue;
double cost = Math.sqrt(x * x + z * z) * 0.98;
Pos floorPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z);
floorPoint = gravitySnap(instance, floorPoint, boundingBox, 5);
if (floorPoint == null) continue;
var nodeWalk = createWalk(instance, floorPoint, boundingBox, cost, point, goal, closed);
if (nodeWalk != null && !closed.contains(nodeWalk)) nearby.add(nodeWalk);
for (int i = 1; i <= 1; ++i) {
Pos jumpPoint = point.withX(point.blockX() + 0.5 + x).withZ(point.blockZ() + 0.5 + z).add(0, i, 0);
jumpPoint = gravitySnap(instance, jumpPoint, boundingBox, 5);
if (jumpPoint == null) continue;
if (!floorPoint.sameBlock(jumpPoint)) {
var nodeJump = createJump(instance, jumpPoint, boundingBox, cost + 0.2, point, goal, closed);
if (nodeJump != null && !closed.contains(nodeJump)) nearby.add(nodeJump);
}
}
}
}
return nearby;
}
private PNode createFly(Instance instance, Pos point, BoundingBox boundingBox, double cost, Pos start, Point goal, Set<PNode> closed) {
var n = newNode(cost, point, goal);
if (closed.contains(n)) return null;
if (!canMoveTowards(instance, start, point, boundingBox)) return null;
n.setType(NodeType.FLY);
return n;
}
private PNode createWalk(Instance instance, Pos point, BoundingBox boundingBox, double cost, Pos start, Point goal, Set<PNode> closed) {
var n = newNode(cost, point, goal);
if (closed.contains(n)) return null;
if (point.y() < start.y()) {
if (!canMoveTowards(instance, start, point.withY(start.y()), boundingBox)) return null;
n.setType(NodeType.FALL);
} else {
if (!canMoveTowards(instance, start, point, boundingBox)) return null;
}
return n;
}
private PNode createJump(Instance instance, Pos point, BoundingBox boundingBox, double cost, Pos start, Point goal, Set<PNode> closed) {
if (point.y() - start.y() == 0) return null;
if (point.y() - start.y() > 2) return null;
if (point.blockX() != start.blockX() && point.blockZ() != start.blockZ()) return null;
var n = newNode(cost, point, goal);
if (closed.contains(n)) return null;
if (pointInvalid(instance, point, boundingBox)) return null;
if (pointInvalid(instance, start.add(0, 1, 0), boundingBox)) return null;
n.setType(NodeType.JUMP);
return n;
}
private boolean pointInvalid(Instance instance, Pos point, BoundingBox boundingBox) {
var iterator = boundingBox.getBlocks(point);
while (iterator.hasNext()) {
var block = iterator.next();
if (instance.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) {
return true;
}
}
return false;
}
private PNode newNode(double cost, Pos point, Point goal) {
tempNode.g = g + cost;
tempNode.h = PathGenerator.heuristic(point, goal);
tempNode.setPoint(point);
var newNode = tempNode;
tempNode = new PNode(Pos.ZERO, 0, 0, NodeType.WALK, this);
return newNode;
}
static Pos gravitySnap(Instance instance, Point point, BoundingBox boundingBox, double maxFall) {
point = new Pos(point.blockX() + 0.5, point.blockY(), point.blockZ() + 0.5);
Chunk c = instance.getChunkAt(point);
if (c == null) return null;
for (int axis = 1; axis <= maxFall; ++axis) {
var iterator = boundingBox.getBlocks(point, BoundingBox.AxisMask.Y, -axis);
while (iterator.hasNext()) {
var block = iterator.next();
if (instance.getBlock(block, Block.Getter.Condition.TYPE).isSolid()) {
return Pos.fromPoint(point.withY(block.blockY() + 1));
}
}
}
return Pos.fromPoint(point.withY(point.y() - maxFall));
}
private static boolean canMoveTowards(Instance instance, Pos start, Point end, BoundingBox boundingBox) {
Point diff = end.sub(start);
PhysicsResult res = CollisionUtils.handlePhysics(instance, instance.getChunkAt(start), boundingBox, start, Vec.fromPoint(diff), null, false);
return !res.collisionZ() && !res.collisionY() && !res.collisionX();
}
@Override
public String toString() {
return "PNode{" +
"point=" + point +
", d=" + (g + h) +
", type=" + type +
'}';
}
public Point point() {
return point;
}
}

View File

@ -0,0 +1,97 @@
package net.minestom.server.entity.pathfinding;
import net.minestom.server.coordinate.Point;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class PPath {
private final Runnable onComplete;
private final List<PNode> nodes = new ArrayList<>();
private final double pathVariance;
private final double maxDistance;
private final PathfinderCapabilities capabilities;
private int index = 0;
private final AtomicReference<PathState> state = new AtomicReference<>(PathState.CALCULATING);
public PathfinderCapabilities capabilities() {
return capabilities;
}
public Point getNext() {
if (index + 1 >= nodes.size()) return null;
var current = nodes.get(index + 1);
return current.point;
}
public enum PathfinderType {
LAND, AQUATIC, FLYING, AMPHIBIOUS
}
public record PathfinderCapabilities (PathfinderType type, boolean canJump, boolean canClimbAnything, float swimSpeedModifier) {
}
public void setState(PathState newState) {
state.set(newState);
}
enum PathState {
CALCULATING,
FOLLOWING,
TERMINATING, TERMINATED, COMPUTED, BEST_EFFORT, INVALID
}
PathState getState() {
return state.get();
}
public List<PNode> getNodes() {
return nodes;
}
public PPath(double maxDistance, double pathVariance, PathfinderCapabilities capabilities, Runnable onComplete) {
this.onComplete = onComplete;
this.maxDistance = maxDistance;
this.pathVariance = pathVariance;
this.capabilities = capabilities;
}
void runComplete() {
if (onComplete != null) onComplete.run();
}
@Override
public String toString() {
return nodes.toString();
}
PNode.NodeType getCurrentType() {
if (index >= nodes.size()) return null;
var current = nodes.get(index);
return current.getType();
}
@Nullable
Point getCurrent() {
if (index >= nodes.size()) return null;
var current = nodes.get(index);
return current.point;
}
void next() {
if (index >= nodes.size()) return;
index++;
}
double maxDistance() {
return maxDistance;
}
double pathVariance() {
return pathVariance;
}
}

View File

@ -0,0 +1,140 @@
package net.minestom.server.entity.pathfinding;
import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashBigSet;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PathGenerator {
private static final ExecutorService pool = Executors.newWorkStealingPool();
private static final PNode repathNode = new PNode(Pos.ZERO, 0, 0, PNode.NodeType.REPATH, null);
public static double heuristic (Point node, Point target) {
return node.distance(target);
}
static Comparator<PNode> pNodeComparator = (s1, s2) -> (int) (((s1.g + s1.h) - (s2.g + s2.h)) * 1000);
public static PPath generate(Instance instance, Pos orgStart, Point orgTarget, double closeDistance, double maxDistance, double pathVariance, BoundingBox boundingBox, PPath.PathfinderCapabilities capabilities, boolean startOnGround, Runnable onComplete) {
Pos start = (capabilities.type() == PPath.PathfinderType.AQUATIC
|| capabilities.type() == PPath.PathfinderType.FLYING
|| (capabilities.type() == PPath.PathfinderType.AMPHIBIOUS && instance.getBlock(orgStart).compare(Block.WATER))
|| startOnGround)
? orgStart
: PNode.gravitySnap(instance, orgStart, boundingBox, 100);
Pos target = (capabilities.type() == PPath.PathfinderType.AQUATIC
|| capabilities.type() == PPath.PathfinderType.FLYING
|| (capabilities.type() == PPath.PathfinderType.AMPHIBIOUS && instance.getBlock(orgTarget).compare(Block.WATER)))
? Pos.fromPoint(orgTarget)
: PNode.gravitySnap(instance, orgTarget, boundingBox, 100);
if (start == null || target == null) return null;
PPath path = new PPath(maxDistance, pathVariance, capabilities, onComplete);
pool.submit(() -> computePath(instance, start, target, closeDistance, maxDistance, pathVariance, boundingBox, path));
return path;
}
private static void computePath(Instance instance, Pos start, Pos target, double closeDistance, double maxDistance, double pathVariance, BoundingBox boundingBox, PPath path) {
double closestDistance = Double.MAX_VALUE;
double straightDistance = heuristic(start, target);
int maxSize = (int) Math.floor(maxDistance * 10);
closeDistance = Math.max(0.8, closeDistance);
List<PNode> closestFoundNodes = List.of();
PNode pStart = new PNode(start, 0, heuristic(start, target), PNode.NodeType.WALK, null);
ObjectHeapPriorityQueue<PNode> open = new ObjectHeapPriorityQueue<>(pNodeComparator);
open.enqueue(pStart);
Set<PNode> closed = new ObjectOpenHashBigSet<>(maxSize);
while (!open.isEmpty() && closed.size() < maxSize) {
if (path.getState() == PPath.PathState.TERMINATING) {
path.setState(PPath.PathState.TERMINATED);
return;
}
PNode current = open.dequeue();
var chunk = instance.getChunkAt(current.point);
if (chunk == null) continue;
if (!chunk.isLoaded()) continue;
if (((current.g + current.h) - straightDistance) > pathVariance) continue;
if (!withinDistance(current.point, start, maxDistance)) continue;
if (withinDistance(current.point, target, closeDistance)) {
open.enqueue(current);
break;
}
if (current.h < closestDistance) {
closestDistance = current.h;
closestFoundNodes = List.of(current);
}
var found = current.getNearby(instance, closed, target, boundingBox, path.capabilities());
found.forEach(p -> {
if (p.point.distance(start) <= maxDistance) {
open.enqueue(p);
closed.add(p);
}
});
}
PNode current = open.isEmpty() ? null : open.dequeue();
if (current == null || open.isEmpty() || !withinDistance(current.point, target, closeDistance)) {
if (closestFoundNodes.isEmpty()) {
path.setState(PPath.PathState.INVALID);
return;
}
current = closestFoundNodes.get(0);
if (!open.isEmpty()) {
repathNode.parent = current;
current = repathNode;
}
}
while (current.parent != null) {
path.getNodes().add(current);
current = current.parent;
}
Collections.reverse(path.getNodes());
if (path.getCurrentType() == PNode.NodeType.REPATH) {
path.setState(PPath.PathState.INVALID);
path.getNodes().clear();
return;
}
var lastNode = path.getNodes().get(path.getNodes().size() - 1);
if (lastNode.point.distance(target) > closeDistance) {
path.setState(PPath.PathState.BEST_EFFORT);
return;
}
PNode pEnd = new PNode(target, 0, 0, PNode.NodeType.WALK, null);
path.getNodes().add(pEnd);
path.setState(PPath.PathState.COMPUTED);
}
private static boolean withinDistance(Pos point, Pos target, double closeDistance) {
return point.distanceSquared(target) < (closeDistance * closeDistance);
}
}

View File

@ -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.instance.block.BlockHandler;
import net.minestom.server.network.packet.server.SendablePacket;
@ -55,9 +54,6 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
protected volatile boolean loaded = true;
private final Viewable viewable;
// Path finding
protected PFColumnarSpace columnarSpace;
// Data
private final TagHandler tagHandler = TagHandler.newHandler();
@ -261,15 +257,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.
*

View File

@ -1,13 +1,11 @@
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.coordinate.Vec;
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.NetworkBuffer;
@ -75,12 +73,6 @@ public class DynamicChunk extends Chunk {
this.lastChange = System.currentTimeMillis();
this.chunkCache.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),

View File

@ -12,7 +12,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.EventFilter;
import net.minestom.server.event.EventHandler;
@ -100,9 +99,6 @@ public abstract class Instance implements Block.Getter, Block.Setter,
// the explosion supplier
private ExplosionSupplier explosionSupplier;
// Pathfinder
private final PFInstanceSpace instanceSpace = new PFInstanceSpace(this);
// Adventure
private final Pointers pointers;
@ -752,18 +748,6 @@ public abstract class Instance implements Block.Getter, Block.Setter,
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;

View File

@ -1,31 +0,0 @@
package net.minestom.server.utils.entity;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.block.Block;
import org.jetbrains.annotations.NotNull;
public final class EntityUtils {
private EntityUtils() {
}
public static boolean isOnGround(@NotNull Entity entity) {
final Chunk chunk = entity.getChunk();
if (chunk == null)
return false;
final Pos entityPosition = entity.getPosition();
// TODO: check entire bounding box
try {
final Block block;
synchronized (chunk) {
block = chunk.getBlock(entityPosition.sub(0, 1, 0));
}
return block.isSolid();
} catch (NullPointerException e) {
// Probably an entity at the border of an unloaded chunk
return false;
}
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.collision;
import net.minestom.server.utils.block.BlockIterator;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import net.minestom.server.coordinate.Point;
@ -11,6 +12,8 @@ import net.minestom.server.entity.metadata.other.SlimeMeta;
import net.minestom.server.instance.block.Block;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -632,6 +635,13 @@ public class EntityBlockPhysicsIntegrationTest {
assertEqualsPoint(new Pos(0.7, 42, 0), res.newPosition());
}
@Test
public void tmp(Env env) {
BoundingBox boundingBox = new BoundingBox(3,2.8,3);
Vec velocity = new Vec(1,3,5);
Pos entityPosition = new Pos(0,0,0);
}
// Checks C include all checks for crossing one intermediate block (3 block checks)
@Test
public void entityPhysicsSmallMoveC0(Env env) {

View File

@ -150,6 +150,7 @@ public class EntityVelocityIntegrationTest {
assertFalse(entity.hasVelocity());
entity.setInstance(instance, new Pos(0, 41, 0)).join();
entity.setVelocity(new Vec(0, -10, 0));
env.tick();

View File

@ -0,0 +1,189 @@
package net.minestom.server.entity.pathfinding;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class PathfinderIntegrationTest {
/**
* Validate that the path is valid
* Currently only checks to make sure path is not null, and that nodes are not inside blocks
* @param nodes the nodes to validate
* @return true if the path is valid
*/
private boolean validateNodes(List<PNode> nodes, Instance instance) {
if (nodes == null) fail("Path is null");
if (nodes.size() == 0) fail("Path is empty");
nodes.forEach((node) -> {
if (instance.getBlock(node.point).isSolid()) {
fail("Node is inside a block");
}
});
return true;
}
@Test
public void testTall(Env env) {
var i = env.createFlatInstance();
i.getWorldBorder().setCenter(0, 0);
i.getWorldBorder().setDiameter(10000);
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> {
i.loadChunk(x, z).join();
});
var zombie = new LivingEntity(EntityType.ZOMBIE);
zombie.setInstance(i, new Pos(0, 40, 0));
zombie.setBoundingBox(3f, 6.5f, 3f);
i.setBlock(1, 46, 7, Block.STONE);
Navigator nav = new Navigator(zombie);
nav.setPathTo(new Pos(0, 40, 10));
while (nav.getState() == PPath.PathState.CALCULATING) {}
assert(nav.getNodes() != null);
validateNodes(nav.getNodes(), i);
}
@Test
public void testStraightLine(Env env) {
var i = env.createFlatInstance();
i.getWorldBorder().setCenter(0, 0);
i.getWorldBorder().setDiameter(10000);
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> {
i.loadChunk(x, z).join();
});
var zombie = new LivingEntity(EntityType.ZOMBIE);
zombie.setInstance(i, new Pos(0, 40, 0));
Navigator nav = new Navigator(zombie);
nav.setPathTo(new Pos(0, 40, 10));
while (nav.getState() == PPath.PathState.CALCULATING) {}
assert(nav.getNodes() != null);
validateNodes(nav.getNodes(), i);
}
@Test
public void testShort(Env env) {
var i = env.createFlatInstance();
i.getWorldBorder().setCenter(0, 0);
i.getWorldBorder().setDiameter(10000);
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> {
i.loadChunk(x, z).join();
});
var zombie = new LivingEntity(EntityType.ZOMBIE);
zombie.setInstance(i, new Pos(0, 40, 0));
Navigator nav = new Navigator(zombie);
nav.setPathTo(new Pos(2, 40, 2));
while (nav.getState() == PPath.PathState.CALCULATING) {}
assert(nav.getNodes() != null);
validateNodes(nav.getNodes(), i);
}
@Test
public void testPFNodeEqual(Env env) {
PNode node1 = new PNode(new Pos(0.777, 0, 0), 2, 0, PNode.NodeType.WALK, null);
PNode node2 = new PNode(new Pos(0.777, 0, 0), 0, 3, PNode.NodeType.WALK, node1);
Set<PNode> nodes = new HashSet<>();
nodes.add(node1);
nodes.add(node2);
assert node1.equals(node2);
assert nodes.size() == 1;
}
@Test
public void testStraightLineBlocked(Env env) {
var i = env.createFlatInstance();
i.getWorldBorder().setCenter(0, 0);
i.getWorldBorder().setDiameter(10000);
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> {
i.loadChunk(x, z).join();
});
i.setBlock(-6, 40, 5, Block.STONE);
i.setBlock(-5, 40, 5, Block.STONE);
i.setBlock(-4, 40, 5, Block.STONE);
i.setBlock(-3, 40, 5, Block.STONE);
i.setBlock(-2, 40, 5, Block.STONE);
i.setBlock(-1, 40, 5, Block.STONE);
i.setBlock(0, 40, 5, Block.STONE);
i.setBlock(1, 40, 5, Block.STONE);
i.setBlock(2, 40, 5, Block.STONE);
i.setBlock(3, 40, 5, Block.STONE);
i.setBlock(4, 40, 5, Block.STONE);
i.setBlock(5, 40, 5, Block.STONE);
i.setBlock(6, 40, 5, Block.STONE);
i.setBlock(7, 40, 5, Block.STONE);
i.setBlock(-6, 41, 5, Block.STONE);
i.setBlock(-5, 41, 5, Block.STONE);
i.setBlock(-4, 41, 5, Block.STONE);
i.setBlock(-3, 41, 5, Block.STONE);
i.setBlock(-2, 41, 5, Block.STONE);
i.setBlock(-1, 41, 5, Block.STONE);
i.setBlock(0, 41, 5, Block.STONE);
i.setBlock(1, 41, 5, Block.STONE);
i.setBlock(2, 41, 5, Block.STONE);
i.setBlock(3, 41, 5, Block.STONE);
i.setBlock(4, 41, 5, Block.STONE);
i.setBlock(5, 41, 5, Block.STONE);
i.setBlock(6, 41, 5, Block.STONE);
i.setBlock(7, 41, 5, Block.STONE);
var zombie = new LivingEntity(EntityType.ZOMBIE);
zombie.setInstance(i, new Pos(0, 40, 0));
zombie.setBoundingBox(zombie.getBoundingBox().expand(4f, 4f, 4f));
Navigator nav = new Navigator(zombie);
nav.setPathTo(new Pos(0, 40, 10));
while (nav.getState() == PPath.PathState.CALCULATING) {}
System.out.println(nav.getNodes());
assert(nav.getNodes() != null);
validateNodes(nav.getNodes(), i);
}
@Test
public void testGravitySnap(Env env) {
var i = env.createFlatInstance();
i.getWorldBorder().setCenter(0, 0);
i.getWorldBorder().setDiameter(10000);
ChunkUtils.forChunksInRange(0, 0, 10, (x, z) -> {
i.loadChunk(x, z).join();
});
var zombie = new LivingEntity(EntityType.ZOMBIE);
var snapped = PNode.gravitySnap(i, new Pos(-140.74433362614695, 40.58268292446131, 18.87966960447388), zombie.getBoundingBox(), 100);
assertEquals(new Pos(-140.5, 40.0, 18.5), snapped);
}
}

View File

@ -264,24 +264,23 @@ public class BlockIteratorTest {
points.add(iterator.next());
}
// todo(mattw): I need to confirm that these are correct
Point[] validPoints = new Point[]{
new Vec(0.0, 0.0, 0.0),
new Vec(1.0, 1.0, 0.0),
new Vec(0.0, 1.0, 1.0),
new Vec(1.0, 0.0, 1.0),
new Vec(1.0, 0.0, 0.0),
new Vec(0.0, 1.0, 0.0),
new Vec(0.0, 0.0, 1.0),
new Vec(1.0, 1.0, 1.0),
new Vec(2.0, 2.0, 1.0),
new Vec(1.0, 2.0, 2.0),
new Vec(2.0, 1.0, 2.0),
new Vec(2.0, 1.0, 1.0),
new Vec(1.0, 2.0, 1.0),
new Vec(1.0, 1.0, 2.0),
new Vec(2.0, 2.0, 2.0),
// todo(mattw): I need to confirm that these are correct
new Vec(1.0, 1.0, 0.0),
new Vec(0.0, 1.0, 1.0),
new Vec(1.0, 0.0, 1.0),
new Vec(2.0, 2.0, 1.0),
new Vec(1.0, 2.0, 2.0),
new Vec(2.0, 1.0, 2.0)
new Vec(2.0, 2.0, 2.0)
};
for (Point p : validPoints) {