mirror of https://github.com/Minestom/Minestom.git
Pathfinding
This commit is contained in:
parent
d03466e5a3
commit
17ed34196f
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue