552 lines
21 KiB
Java
552 lines
21 KiB
Java
package net.citizensnpcs.npc.ai;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.function.Function;
|
|
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.entity.ArmorStand;
|
|
import org.bukkit.entity.Entity;
|
|
import org.bukkit.entity.LivingEntity;
|
|
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
|
|
import org.bukkit.util.Vector;
|
|
|
|
import com.google.common.collect.Iterables;
|
|
|
|
import net.citizensnpcs.Settings.Setting;
|
|
import net.citizensnpcs.api.CitizensAPI;
|
|
import net.citizensnpcs.api.ai.EntityTarget;
|
|
import net.citizensnpcs.api.ai.Navigator;
|
|
import net.citizensnpcs.api.ai.NavigatorParameters;
|
|
import net.citizensnpcs.api.ai.PathStrategy;
|
|
import net.citizensnpcs.api.ai.StuckAction;
|
|
import net.citizensnpcs.api.ai.TargetType;
|
|
import net.citizensnpcs.api.ai.TeleportStuckAction;
|
|
import net.citizensnpcs.api.ai.event.CancelReason;
|
|
import net.citizensnpcs.api.ai.event.NavigationBeginEvent;
|
|
import net.citizensnpcs.api.ai.event.NavigationCancelEvent;
|
|
import net.citizensnpcs.api.ai.event.NavigationCompleteEvent;
|
|
import net.citizensnpcs.api.ai.event.NavigationReplaceEvent;
|
|
import net.citizensnpcs.api.ai.event.NavigationStuckEvent;
|
|
import net.citizensnpcs.api.ai.event.NavigatorCallback;
|
|
import net.citizensnpcs.api.astar.pathfinder.DoorExaminer;
|
|
import net.citizensnpcs.api.astar.pathfinder.MinecraftBlockExaminer;
|
|
import net.citizensnpcs.api.astar.pathfinder.SwimmingExaminer;
|
|
import net.citizensnpcs.api.npc.NPC;
|
|
import net.citizensnpcs.api.util.DataKey;
|
|
import net.citizensnpcs.npc.ai.AStarNavigationStrategy.AStarPlanner;
|
|
import net.citizensnpcs.npc.ai.MCNavigationStrategy.MCNavigator;
|
|
import net.citizensnpcs.trait.RotationTrait;
|
|
import net.citizensnpcs.trait.RotationTrait.PacketRotationSession;
|
|
import net.citizensnpcs.trait.RotationTrait.RotationParams;
|
|
import net.citizensnpcs.util.ChunkCoord;
|
|
import net.citizensnpcs.util.NMS;
|
|
|
|
public class CitizensNavigator implements Navigator, Runnable {
|
|
private Location activeTicket;
|
|
private final NavigatorParameters defaultParams = new NavigatorParameters().baseSpeed(UNINITIALISED_SPEED)
|
|
.range(Setting.DEFAULT_PATHFINDING_RANGE.asFloat()).debug(Setting.DEBUG_PATHFINDING.asBoolean())
|
|
.defaultAttackStrategy(MCTargetStrategy.DEFAULT_ATTACK_STRATEGY)
|
|
.attackRange(Setting.NPC_ATTACK_DISTANCE.asDouble())
|
|
.updatePathRate(Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt())
|
|
.distanceMargin(Setting.DEFAULT_DISTANCE_MARGIN.asDouble())
|
|
.pathDistanceMargin(Setting.DEFAULT_PATH_DISTANCE_MARGIN.asDouble())
|
|
.stationaryTicks(Setting.DEFAULT_STATIONARY_TICKS.asInt()).stuckAction(TeleportStuckAction.INSTANCE)
|
|
.examiner(new MinecraftBlockExaminer()).useNewPathfinder(Setting.USE_NEW_PATHFINDER.asBoolean())
|
|
.straightLineTargetingDistance(Setting.DEFAULT_STRAIGHT_LINE_TARGETING_DISTANCE.asFloat())
|
|
.destinationTeleportMargin(Setting.DEFAULT_DESTINATION_TELEPORT_MARGIN.asDouble());
|
|
private PathStrategy executing;
|
|
private int lastX, lastY, lastZ;
|
|
private NavigatorParameters localParams = defaultParams;
|
|
private final NPC npc;
|
|
private boolean paused;
|
|
private PacketRotationSession session;
|
|
private int stationaryTicks;
|
|
|
|
public CitizensNavigator(NPC npc) {
|
|
this.npc = npc;
|
|
if (npc.data().get(NPC.Metadata.DISABLE_DEFAULT_STUCK_ACTION, false)) {
|
|
defaultParams.stuckAction(null);
|
|
}
|
|
defaultParams.examiner(new SwimmingExaminer(npc));
|
|
}
|
|
|
|
@Override
|
|
public void cancelNavigation() {
|
|
stopNavigating(CancelReason.PLUGIN);
|
|
}
|
|
|
|
@Override
|
|
public void cancelNavigation(CancelReason reason) {
|
|
stopNavigating(reason);
|
|
}
|
|
|
|
@Override
|
|
public boolean canNavigateTo(Location dest) {
|
|
return canNavigateTo(dest, defaultParams.clone());
|
|
}
|
|
|
|
@Override
|
|
public boolean canNavigateTo(Location dest, NavigatorParameters params) {
|
|
if (defaultParams.useNewPathfinder()) {
|
|
AStarPlanner planner = new AStarPlanner(params, npc.getStoredLocation(), dest);
|
|
planner.tick(Setting.MAXIMUM_ASTAR_ITERATIONS.asInt(), Setting.MAXIMUM_ASTAR_ITERATIONS.asInt());
|
|
return planner.plan != null;
|
|
} else {
|
|
MCNavigator nav = NMS.getTargetNavigator(npc.getEntity(), dest, params);
|
|
return nav.getCancelReason() == null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public NavigatorParameters getDefaultParameters() {
|
|
return defaultParams;
|
|
}
|
|
|
|
@Override
|
|
public EntityTarget getEntityTarget() {
|
|
return executing instanceof EntityTarget ? (EntityTarget) executing : null;
|
|
}
|
|
|
|
@Override
|
|
public NavigatorParameters getLocalParameters() {
|
|
if (!isNavigating()) {
|
|
return defaultParams;
|
|
}
|
|
return localParams;
|
|
}
|
|
|
|
@Override
|
|
public NPC getNPC() {
|
|
return npc;
|
|
}
|
|
|
|
@Override
|
|
public PathStrategy getPathStrategy() {
|
|
return executing;
|
|
}
|
|
|
|
@Override
|
|
public Location getTargetAsLocation() {
|
|
return isNavigating() ? executing.getTargetAsLocation() : null;
|
|
}
|
|
|
|
@Override
|
|
public TargetType getTargetType() {
|
|
return isNavigating() ? executing.getTargetType() : null;
|
|
}
|
|
|
|
@Override
|
|
public boolean isNavigating() {
|
|
return executing != null && !isPaused();
|
|
}
|
|
|
|
@Override
|
|
public boolean isPaused() {
|
|
return paused;
|
|
}
|
|
|
|
public void load(DataKey root) {
|
|
if (root.keyExists("pathfindingrange")) {
|
|
defaultParams.range((float) root.getDouble("pathfindingrange"));
|
|
}
|
|
if (root.keyExists("stationaryticks")) {
|
|
defaultParams.stationaryTicks(root.getInt("stationaryticks"));
|
|
}
|
|
if (root.keyExists("distancemargin")) {
|
|
defaultParams.distanceMargin(root.getDouble("distancemargin"));
|
|
}
|
|
if (root.keyExists("destinationteleportmargin")) {
|
|
defaultParams.destinationTeleportMargin(root.getDouble("destinationteleportmargin"));
|
|
}
|
|
if (root.keyExists("updatepathrate")) {
|
|
defaultParams.updatePathRate(root.getInt("updatepathrate"));
|
|
}
|
|
defaultParams.speedModifier((float) root.getDouble("speedmodifier", 1F));
|
|
defaultParams.avoidWater(root.getBoolean("avoidwater"));
|
|
if (!root.getBoolean("usedefaultstuckaction") && defaultParams.stuckAction() == TeleportStuckAction.INSTANCE) {
|
|
defaultParams.stuckAction(null);
|
|
}
|
|
}
|
|
|
|
public void onDespawn() {
|
|
stopNavigating(CancelReason.NPC_DESPAWNED);
|
|
}
|
|
|
|
public void onSpawn() {
|
|
if (defaultParams.baseSpeed() == UNINITIALISED_SPEED) {
|
|
defaultParams.baseSpeed(NMS.getSpeedFor(npc));
|
|
}
|
|
updatePathfindingRange();
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
updateMountedStatus();
|
|
if (!isNavigating() || !npc.isSpawned() || isPaused())
|
|
return;
|
|
Location npcLoc = npc.getStoredLocation();
|
|
Location targetLoc = getTargetAsLocation();
|
|
if (!npcLoc.getWorld().equals(targetLoc.getWorld()) || localParams.range() < npcLoc.distance(targetLoc)) {
|
|
stopNavigating(CancelReason.STUCK);
|
|
return;
|
|
}
|
|
if (updateStationaryStatus())
|
|
return;
|
|
updatePathfindingRange();
|
|
boolean finished = executing.update();
|
|
if (!finished) {
|
|
localParams.run();
|
|
}
|
|
|
|
if (localParams.lookAtFunction() != null) {
|
|
if (session == null) {
|
|
RotationTrait trait = npc.getOrAddTrait(RotationTrait.class);
|
|
session = trait.createPacketSession(new RotationParams().filter(p -> true).persist(true));
|
|
}
|
|
session.getSession().rotateToFace(localParams.lookAtFunction().apply(this));
|
|
}
|
|
|
|
if (localParams.destinationTeleportMargin() > 0
|
|
&& npcLoc.distance(targetLoc) <= localParams.destinationTeleportMargin()) {
|
|
// TODO: easing?
|
|
npc.teleport(targetLoc, TeleportCause.PLUGIN);
|
|
finished = true;
|
|
}
|
|
if (!finished) {
|
|
return;
|
|
}
|
|
if (executing.getCancelReason() != null) {
|
|
stopNavigating(executing.getCancelReason());
|
|
} else {
|
|
NavigationCompleteEvent event = new NavigationCompleteEvent(this);
|
|
PathStrategy old = executing;
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
if (old == executing) {
|
|
stopNavigating(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void save(DataKey root) {
|
|
if (defaultParams.range() != Setting.DEFAULT_PATHFINDING_RANGE.asFloat()) {
|
|
root.setDouble("pathfindingrange", defaultParams.range());
|
|
} else {
|
|
root.removeKey("pathfindingrange");
|
|
}
|
|
if (defaultParams.stationaryTicks() != Setting.DEFAULT_STATIONARY_TICKS.asTicks()) {
|
|
root.setInt("stationaryticks", defaultParams.stationaryTicks());
|
|
} else {
|
|
root.removeKey("stationaryticks");
|
|
}
|
|
if (defaultParams.destinationTeleportMargin() != Setting.DEFAULT_DESTINATION_TELEPORT_MARGIN.asDouble()) {
|
|
root.setDouble("destinationteleportmargin", defaultParams.destinationTeleportMargin());
|
|
} else {
|
|
root.removeKey("destinationteleportmargin");
|
|
}
|
|
if (defaultParams.distanceMargin() != Setting.DEFAULT_DISTANCE_MARGIN.asDouble()) {
|
|
root.setDouble("distancemargin", defaultParams.distanceMargin());
|
|
} else {
|
|
root.removeKey("distancemargin");
|
|
}
|
|
if (defaultParams.updatePathRate() != Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt()) {
|
|
root.setInt("updatepathrate", defaultParams.updatePathRate());
|
|
} else {
|
|
root.removeKey("updatepathrate");
|
|
}
|
|
if (defaultParams.useNewPathfinder() != Setting.USE_NEW_PATHFINDER.asBoolean()) {
|
|
root.setBoolean("usenewpathfinder", defaultParams.useNewPathfinder());
|
|
} else {
|
|
root.removeKey("usenewpathfinder");
|
|
}
|
|
root.setDouble("speedmodifier", defaultParams.speedModifier());
|
|
root.setBoolean("avoidwater", defaultParams.avoidWater());
|
|
root.setBoolean("usedefaultstuckaction", defaultParams.stuckAction() == TeleportStuckAction.INSTANCE);
|
|
}
|
|
|
|
@Override
|
|
public void setPaused(boolean paused) {
|
|
if (paused && isNavigating()) {
|
|
NMS.cancelMoveDestination(npc.getEntity());
|
|
}
|
|
this.paused = paused;
|
|
}
|
|
|
|
@Override
|
|
public void setStraightLineTarget(Entity target, boolean aggressive) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
if (target == null) {
|
|
cancelNavigation();
|
|
return;
|
|
}
|
|
setTarget((params) -> {
|
|
params.straightLineTargetingDistance(100000);
|
|
return new MCTargetStrategy(npc, target, aggressive, params);
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void setStraightLineTarget(Location target) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
if (target == null) {
|
|
cancelNavigation();
|
|
return;
|
|
}
|
|
setTarget((params) -> new StraightLineNavigationStrategy(npc, target.clone(), params));
|
|
}
|
|
|
|
@Override
|
|
public void setTarget(Entity target, boolean aggressive) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
if (target == null) {
|
|
cancelNavigation();
|
|
return;
|
|
}
|
|
setTarget(new Function<NavigatorParameters, PathStrategy>() {
|
|
@Override
|
|
public PathStrategy apply(NavigatorParameters params) {
|
|
return new MCTargetStrategy(npc, target, aggressive, params);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void setTarget(Function<NavigatorParameters, PathStrategy> strategy) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
switchParams();
|
|
switchStrategyTo(strategy.apply(localParams));
|
|
}
|
|
|
|
@Override
|
|
public void setTarget(Iterable<Vector> path) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
if (path == null || Iterables.size(path) == 0) {
|
|
cancelNavigation();
|
|
return;
|
|
}
|
|
setTarget(new Function<NavigatorParameters, PathStrategy>() {
|
|
@Override
|
|
public PathStrategy apply(NavigatorParameters params) {
|
|
if (npc.isFlyable()) {
|
|
return new FlyingAStarNavigationStrategy(npc, path, params);
|
|
} else if (params.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)
|
|
|| npc.getEntity() instanceof ArmorStand) {
|
|
return new AStarNavigationStrategy(npc, path, params);
|
|
} else {
|
|
return new MCNavigationStrategy(npc, path, params);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void setTarget(Location targetIn) {
|
|
if (!npc.isSpawned())
|
|
throw new IllegalStateException("npc is not spawned");
|
|
if (targetIn == null) {
|
|
cancelNavigation();
|
|
return;
|
|
}
|
|
final Location target = targetIn.clone();
|
|
setTarget(new Function<NavigatorParameters, PathStrategy>() {
|
|
@Override
|
|
public PathStrategy apply(NavigatorParameters params) {
|
|
if (npc.isFlyable()) {
|
|
return new FlyingAStarNavigationStrategy(npc, target, params);
|
|
} else if (params.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)
|
|
|| npc.getEntity() instanceof ArmorStand) {
|
|
return new AStarNavigationStrategy(npc, target, params);
|
|
} else {
|
|
return new MCNavigationStrategy(npc, target, params);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void stopNavigating() {
|
|
if (executing != null) {
|
|
executing.stop();
|
|
}
|
|
executing = null;
|
|
|
|
localParams = defaultParams;
|
|
stationaryTicks = 0;
|
|
if (npc.isSpawned()) {
|
|
Vector velocity = npc.getEntity().getVelocity();
|
|
velocity.setX(0).setY(0).setZ(0);
|
|
npc.getEntity().setVelocity(velocity);
|
|
NMS.cancelMoveDestination(npc.getEntity());
|
|
}
|
|
if (!SUPPORT_CHUNK_TICKETS || !CitizensAPI.hasImplementation() || !CitizensAPI.getPlugin().isEnabled())
|
|
return;
|
|
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
updateTicket(isNavigating() ? executing.getTargetAsLocation() : null);
|
|
}
|
|
}, 10);
|
|
// Location loc = npc.getEntity().getLocation(STATIONARY_LOCATION);
|
|
// NMS.look(npc.getEntity(), loc.getYaw(), 0);
|
|
}
|
|
|
|
private void stopNavigating(CancelReason reason) {
|
|
if (!isNavigating())
|
|
return;
|
|
if (session != null) {
|
|
session.end();
|
|
session = null;
|
|
}
|
|
Iterator<NavigatorCallback> itr = localParams.callbacks().iterator();
|
|
List<NavigatorCallback> callbacks = new ArrayList<NavigatorCallback>();
|
|
while (itr.hasNext()) {
|
|
callbacks.add(itr.next());
|
|
itr.remove();
|
|
}
|
|
for (NavigatorCallback callback : callbacks) {
|
|
callback.onCompletion(reason);
|
|
}
|
|
if (reason == null) {
|
|
stopNavigating();
|
|
return;
|
|
}
|
|
if (reason == CancelReason.STUCK) {
|
|
StuckAction action = localParams.stuckAction();
|
|
NavigationStuckEvent event = new NavigationStuckEvent(this, action);
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
action = event.getAction();
|
|
boolean shouldContinue = action != null ? action.run(npc, this) : false;
|
|
if (shouldContinue) {
|
|
stationaryTicks = 0;
|
|
executing.clearCancelReason();
|
|
return;
|
|
}
|
|
}
|
|
NavigationCancelEvent event = new NavigationCancelEvent(this, reason);
|
|
PathStrategy old = executing;
|
|
Bukkit.getPluginManager().callEvent(event);
|
|
if (old == executing) {
|
|
stopNavigating();
|
|
}
|
|
}
|
|
|
|
private void switchParams() {
|
|
localParams = defaultParams.clone();
|
|
int fallDistance = npc.data().get(NPC.Metadata.PATHFINDER_FALL_DISTANCE,
|
|
Setting.PATHFINDER_FALL_DISTANCE.asInt());
|
|
if (fallDistance != -1) {
|
|
localParams.examiner(new FallingExaminer(fallDistance));
|
|
}
|
|
if (npc.data().get(NPC.Metadata.PATHFINDER_OPEN_DOORS, Setting.NEW_PATHFINDER_OPENS_DOORS.asBoolean())) {
|
|
localParams.examiner(new DoorExaminer());
|
|
}
|
|
if (Setting.NEW_PATHFINDER_CHECK_BOUNDING_BOXES.asBoolean()) {
|
|
localParams.examiner(new BoundingBoxExaminer(npc.getEntity()));
|
|
}
|
|
}
|
|
|
|
private void switchStrategyTo(PathStrategy newStrategy) {
|
|
updatePathfindingRange();
|
|
if (executing != null) {
|
|
Bukkit.getPluginManager().callEvent(new NavigationReplaceEvent(this));
|
|
}
|
|
executing = newStrategy;
|
|
stationaryTicks = 0;
|
|
if (npc.isSpawned()) {
|
|
NMS.updateNavigationWorld(npc.getEntity(), npc.getEntity().getWorld());
|
|
updateTicket(executing.getTargetAsLocation());
|
|
}
|
|
Bukkit.getPluginManager().callEvent(new NavigationBeginEvent(this));
|
|
}
|
|
|
|
private void updateMountedStatus() {
|
|
// TODO: this method seems to break assumptions: better to let the NPC pathfind for itself rather than
|
|
// "commanding" the NPC below on the stack
|
|
if (!isNavigating() || true)
|
|
return;
|
|
Entity vehicle = NMS.getVehicle(npc.getEntity());
|
|
if (!(vehicle instanceof NPCHolder)) {
|
|
return;
|
|
}
|
|
NPC mount = ((NPCHolder) vehicle).getNPC();
|
|
if (mount.getNavigator().isNavigating())
|
|
return;
|
|
switch (getTargetType()) {
|
|
case ENTITY:
|
|
mount.getNavigator().setTarget(getEntityTarget().getTarget(), getEntityTarget().isAggressive());
|
|
break;
|
|
case LOCATION:
|
|
mount.getNavigator().setTarget(getTargetAsLocation());
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
cancelNavigation();
|
|
}
|
|
|
|
private void updatePathfindingRange() {
|
|
NMS.updatePathfindingRange(npc, localParams.range());
|
|
}
|
|
|
|
private boolean updateStationaryStatus() {
|
|
if (localParams.stationaryTicks() < 0)
|
|
return false;
|
|
Location current = npc.getEntity().getLocation(STATIONARY_LOCATION);
|
|
if (current.getY() < -6) {
|
|
stopNavigating(CancelReason.STUCK);
|
|
return true;
|
|
}
|
|
if (lastX == current.getBlockX() && lastY == current.getBlockY() && lastZ == current.getBlockZ()) {
|
|
if (++stationaryTicks >= localParams.stationaryTicks()) {
|
|
stopNavigating(CancelReason.STUCK);
|
|
return true;
|
|
}
|
|
} else {
|
|
stationaryTicks = 0;
|
|
}
|
|
lastX = current.getBlockX();
|
|
lastY = current.getBlockY();
|
|
lastZ = current.getBlockZ();
|
|
return false;
|
|
}
|
|
|
|
private void updateTicket(Location target) {
|
|
if (!SUPPORT_CHUNK_TICKETS || !CitizensAPI.hasImplementation() || !CitizensAPI.getPlugin().isEnabled())
|
|
return;
|
|
if (target != null && this.activeTicket != null
|
|
&& new ChunkCoord(target.getChunk()).equals(new ChunkCoord(this.activeTicket.getChunk()))) {
|
|
this.activeTicket = target.clone();
|
|
return;
|
|
}
|
|
if (this.activeTicket != null) {
|
|
try {
|
|
this.activeTicket.getChunk().removePluginChunkTicket(CitizensAPI.getPlugin());
|
|
} catch (NoSuchMethodError e) {
|
|
SUPPORT_CHUNK_TICKETS = false;
|
|
this.activeTicket = null;
|
|
}
|
|
}
|
|
if (target == null) {
|
|
this.activeTicket = null;
|
|
return;
|
|
}
|
|
this.activeTicket = target.clone();
|
|
try {
|
|
this.activeTicket.getChunk().addPluginChunkTicket(CitizensAPI.getPlugin());
|
|
} catch (NoSuchMethodError e) {
|
|
SUPPORT_CHUNK_TICKETS = false;
|
|
this.activeTicket = null;
|
|
}
|
|
}
|
|
|
|
private static final Location STATIONARY_LOCATION = new Location(null, 0, 0, 0);
|
|
private static boolean SUPPORT_CHUNK_TICKETS = true;
|
|
private static int UNINITIALISED_SPEED = Integer.MIN_VALUE;
|
|
}
|