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() { @Override public PathStrategy apply(NavigatorParameters params) { return new MCTargetStrategy(npc, target, aggressive, params); } }); } @Override public void setTarget(Function strategy) { if (!npc.isSpawned()) throw new IllegalStateException("npc is not spawned"); switchParams(); switchStrategyTo(strategy.apply(localParams)); } @Override public void setTarget(Iterable path) { if (!npc.isSpawned()) throw new IllegalStateException("npc is not spawned"); if (path == null || Iterables.size(path) == 0) { cancelNavigation(); return; } setTarget(new Function() { @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() { @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 itr = localParams.callbacks().iterator(); List callbacks = new ArrayList(); 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; }