All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.minestom.server.entity.pathfinding.Navigator Maven / Gradle / Ivy

There is a newer version: 7320437640
Show newest version
package net.minestom.server.entity.pathfinding;

import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.pathfinding.followers.GroundNodeFollower;
import net.minestom.server.entity.pathfinding.followers.NodeFollower;
import net.minestom.server.entity.pathfinding.generators.GroundNodeGenerator;
import net.minestom.server.entity.pathfinding.generators.NodeGenerator;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.WorldBorder;
import net.minestom.server.network.packet.server.play.ParticlePacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.function.Supplier;

/**
 * Necessary object for all {@link NavigableEntity}.
 */
public final class Navigator {
    private Point goalPosition;
    private final Entity entity;

    // Essentially a double buffer. Wait until a path is done computing before replacing the old one.
    private PPath computingPath;
    private PPath path;

    private double minimumDistance;

    NodeGenerator nodeGenerator = new GroundNodeGenerator();
    private NodeFollower nodeFollower;

    public Navigator(@NotNull Entity entity) {
        this.entity = entity;
        nodeFollower = new GroundNodeFollower(entity);
    }

    public @NotNull PPath.State getState() {
        if (path == null && computingPath == null) return PPath.State.INVALID;
        if (path == null) return computingPath.getState();
        return path.getState();
    }

    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, @Nullable Runnable onComplete) {
        return setPathTo(point, minimumDistance, 50, 20, onComplete);
    }

    /**
     * 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 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 is being generated
     */
    public synchronized boolean setPathTo(@Nullable Point point, double minimumDistance, double maxDistance, double pathVariance, @Nullable Runnable onComplete) {
        final Instance instance = entity.getInstance();
        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.inBounds(point)) {
            return false;
        }
        // Can't path in an unloaded chunk
        final Chunk chunk = instance.getChunkAt(point);
        if (!ChunkUtils.isLoaded(chunk)) {
            return false;
        }

        this.minimumDistance = minimumDistance;
        if (this.entity.getPosition().distance(point) < minimumDistance) {
            if (onComplete != null) onComplete.run();
            return false;
        }

        if (point.sameBlock(entity.getPosition())) {
            if (onComplete != null) onComplete.run();
            return false;
        }

        if (this.computingPath != null) this.computingPath.setState(PPath.State.TERMINATING);

        this.computingPath = PathGenerator.generate(instance,
                this.entity.getPosition(),
                point,
                minimumDistance, maxDistance,
                pathVariance,
                this.entity.getBoundingBox(),
                this.entity.isOnGround(),
                this.nodeGenerator,
                onComplete);

        this.goalPosition = point;
        return true;
    }

    @ApiStatus.Internal
    public synchronized void tick() {
        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.State.COMPUTED || computingPath.getState() == PPath.State.BEST_EFFORT)) {
            path = computingPath;
            computingPath = null;
        }

        if (path == null) return;

        // If the path is computed start following it
        if (path.getState() == PPath.State.COMPUTED || path.getState() == PPath.State.BEST_EFFORT) {
            path.setState(PPath.State.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 (isSameBlock(path.getNodes().get(i), entity.getPosition())) {
                    path.getNodes().subList(0, i).clear();
                    break;
                }
            }
        }

        // If the state is not following, wait until it is
        if (path.getState() != PPath.State.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();

        // Repath
        if (currentTarget == null || path.getCurrentType() == PNode.Type.REPATH || path.getCurrentType() == null) {
            if (computingPath != null && computingPath.getState() == PPath.State.CALCULATING) return;

            computingPath = PathGenerator.generate(entity.getInstance(),
                    entity.getPosition(),
                    Pos.fromPoint(goalPosition),
                    minimumDistance, path.maxDistance(),
                    path.pathVariance(), entity.getBoundingBox(), this.entity.isOnGround(), nodeGenerator, null);

            return;
        }

        if (nextTarget == null) {
            path.setState(PPath.State.INVALID);
            return;
        }

        boolean nextIsRepath = nextTarget.sameBlock(Pos.ZERO);
        nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRepath ? currentTarget : nextTarget);

        if (nodeFollower.isAtPoint(currentTarget)) path.next();
        else if (path.getCurrentType() == PNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget);
    }

    /**
     * Gets the target pathfinder position.
     *
     * @return the target pathfinder position, null if there is no one
     */
    public @Nullable Point getGoalPosition() {
        return goalPosition;
    }

    /**
     * Gets the entity which is navigating.
     *
     * @return the entity
     */
    public @NotNull Entity getEntity() {
        return entity;
    }

    public void reset() {
        if (this.path != null) this.path.setState(PPath.State.TERMINATING);
        this.goalPosition = null;
        this.path = null;

        if (this.computingPath != null) this.computingPath.setState(PPath.State.TERMINATING);
        this.computingPath = null;
    }

    public boolean isComplete() {
        if (this.path == null) return true;
        return goalPosition == null || entity.getPosition().sameBlock(goalPosition);
    }

    public List 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;
    }

    public void setNodeFollower(@NotNull Supplier nodeFollower) {
        this.nodeFollower = nodeFollower.get();
    }

    public void setNodeGenerator(@NotNull Supplier nodeGenerator) {
        this.nodeGenerator = nodeGenerator.get();
    }

    /**
     * Visualise path for debugging
     *
     * @param path the path to draw
     */
    private void drawPath(PPath path) {
        if (path == null) return;

        for (PNode point : path.getNodes()) {
            var packet = new ParticlePacket(Particle.COMPOSTER, point.x(), point.y() + 0.5, point.z(), 0, 0, 0, 0, 1);
            entity.sendPacketToViewers(packet);
        }
    }

    private static boolean isSameBlock(PNode pNode, Pos position) {
        return Math.floor(pNode.x()) == position.blockX() && Math.floor(pNode.y()) == position.blockY() && Math.floor(pNode.z()) == position.blockZ();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy