package net.citizensnpcs.trait.waypoint; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Objects; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.block.Action; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEvent; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import ch.ethz.globis.phtree.PhRangeQuery; import ch.ethz.globis.phtree.PhTree; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.ai.Goal; import net.citizensnpcs.api.ai.GoalSelector; import net.citizensnpcs.api.astar.AStarGoal; import net.citizensnpcs.api.astar.AStarMachine; import net.citizensnpcs.api.astar.AStarNode; import net.citizensnpcs.api.astar.Agent; import net.citizensnpcs.api.astar.Plan; import net.citizensnpcs.api.command.CommandContext; import net.citizensnpcs.api.command.CommandMessages; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.persistence.PersistenceLoader; import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.npc.ai.NPCHolder; import net.citizensnpcs.trait.waypoint.WaypointProvider.EnumerableWaypointProvider; import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.Util; /** * Stores guided waypoint info. Guided waypoints are a list of {@link Waypoint}s that will be navigated between * randomly. Helper waypoints can be used to guide navigation between the random waypoints i.e. navigating between guide * waypoints. For example, you might have a "realistic" NPC that walks between houses using helper waypoints placed * along the roads. */ public class GuidedWaypointProvider implements EnumerableWaypointProvider { private GuidedAIGoal currentGoal; private final List destinations = Lists.newArrayList(); private float distance = -1; private final List guides = Lists.newArrayList(); private NPC npc; private boolean paused; private final PhTree tree = PhTree.create(3); private final PhTree treePlusDestinations = PhTree.create(3); public void addDestination(Waypoint waypoint) { destinations.add(waypoint); rebuildTree(); } public void addDestinations(Collection waypoint) { destinations.addAll(waypoint); rebuildTree(); } public void addGuide(Waypoint helper) { guides.add(helper); rebuildTree(); } public void addGuides(Collection helper) { guides.addAll(helper); rebuildTree(); } @Override public WaypointEditor createEditor(CommandSender sender, CommandContext args) { if (!(sender instanceof Player)) { Messaging.sendErrorTr(sender, CommandMessages.MUST_BE_INGAME); return null; } Player player = (Player) sender; return new WaypointEditor() { private final EntityMarkers markers = new EntityMarkers<>(); private boolean showPath = true; @Override public void begin() { Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_BEGIN); if (showPath) { createWaypointMarkers(); } } private void createDestinationMarker(Waypoint element) { NPC npc = createMarker(element); if (npc != null) { npc.data().set(NPC.Metadata.GLOWING, true); } } private NPC createMarker(Waypoint element) { Entity entity = markers.createMarker(element, element.getLocation().clone().add(0, 1, 0)); if (entity == null) return null; ((NPCHolder) entity).getNPC().data().setPersistent("waypointhashcode", element.hashCode()); return ((NPCHolder) entity).getNPC(); } private void createWaypointMarkers() { for (Waypoint waypoint : guides) { createMarker(waypoint); } for (Waypoint waypoint : destinations) { createDestinationMarker(waypoint); } } @Override public void end() { Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_END); markers.destroyMarkers(); } @EventHandler(ignoreCancelled = true) public void onPlayerChat(AsyncPlayerChatEvent event) { if (!event.getPlayer().equals(sender)) return; if (event.getMessage().equalsIgnoreCase("toggle path")) { event.setCancelled(true); Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), this::togglePath); } else if (event.getMessage().equalsIgnoreCase("clear")) { event.setCancelled(true); Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> { destinations.clear(); guides.clear(); if (showPath) { markers.destroyMarkers(); } }); } else if (event.getMessage().startsWith("distance ")) { event.setCancelled(true); double d = Double.parseDouble(event.getMessage().replace("distance ", "").trim()); if (d <= 0) return; Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), () -> { distance = (float) d; Messaging.sendTr(sender, Messages.GUIDED_WAYPOINT_EDITOR_DISTANCE_SET, d); }); } } @EventHandler(ignoreCancelled = true) public void onPlayerInteract(PlayerInteractEvent event) { if (!event.getPlayer().equals(player) || event.getAction() == Action.PHYSICAL || event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null || Util.isOffHand(event)) return; if (event.getPlayer().getWorld() != npc.getEntity().getWorld()) return; event.setCancelled(true); Location at = event.getClickedBlock().getLocation(); for (Waypoint waypoint : waypoints()) { if (waypoint.getLocation().equals(at)) { Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_ALREADY_TAKEN); return; } } Waypoint element = new Waypoint(at); if (player.isSneaking()) { destinations.add(element); Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_ADDED_AVAILABLE); } else { guides.add(element); Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_ADDED_GUIDE); } if (showPath) { if (player.isSneaking()) { createDestinationMarker(element); } else { createMarker(element); } } rebuildTree(); } @EventHandler(ignoreCancelled = true) public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { NPC clicked = CitizensAPI.getNPCRegistry().getNPC(event.getRightClicked()); if (clicked == null || Util.isOffHand(event)) return; Integer hashcode = clicked.data().get("waypointhashcode"); if (hashcode == null) return; Iterator itr = waypoints().iterator(); while (itr.hasNext()) { Waypoint point = itr.next(); if (point.hashCode() == hashcode) { markers.removeMarker(point); itr.remove(); break; } } } private void togglePath() { showPath = !showPath; if (showPath) { createWaypointMarkers(); Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_SHOWING_MARKERS); } else { markers.destroyMarkers(); Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_NOT_SHOWING_MARKERS); } } }; } @Override public boolean isPaused() { return paused; } @Override public void load(DataKey key) { DataKey dd = key.keyExists("availablewaypoints") ? key.getRelative("availablewaypoints") : key.getRelative("destinations"); for (DataKey root : dd.getIntegerSubKeys()) { Waypoint waypoint = PersistenceLoader.load(Waypoint.class, root); if (waypoint == null) { continue; } destinations.add(waypoint); } DataKey gd = key.keyExists("helperwaypoints") ? key.getRelative("helperwaypoints") : key.getRelative("guides"); for (DataKey root : gd.getIntegerSubKeys()) { Waypoint waypoint = PersistenceLoader.load(Waypoint.class, root); if (waypoint == null) { continue; } guides.add(waypoint); } if (key.keyExists("distance")) { distance = (float) key.getDouble("distance"); } rebuildTree(); } @Override public void onRemove() { if (currentGoal == null) return; currentGoal.onProviderChanged(); npc.getDefaultGoalController().removeGoal(currentGoal); currentGoal = null; } @Override public void onSpawn(NPC npc) { this.npc = npc; if (currentGoal == null) { currentGoal = new GuidedAIGoal(); npc.getDefaultGoalController().addGoal(currentGoal, 1); } } private void rebuildTree() { tree.clear(); treePlusDestinations.clear(); for (Waypoint waypoint : guides) { tree.put(new long[] { waypoint.getLocation().getBlockX(), waypoint.getLocation().getBlockY(), waypoint.getLocation().getBlockZ() }, waypoint); treePlusDestinations.put(new long[] { waypoint.getLocation().getBlockX(), waypoint.getLocation().getBlockY(), waypoint.getLocation().getBlockZ() }, waypoint); } for (Waypoint waypoint : destinations) { treePlusDestinations.put(new long[] { waypoint.getLocation().getBlockX(), waypoint.getLocation().getBlockY(), waypoint.getLocation().getBlockZ() }, waypoint); } if (currentGoal != null) { currentGoal.onProviderChanged(); } } @Override public void save(DataKey key) { key.removeKey("availablewaypoints"); DataKey root = key.getRelative("destinations"); for (int i = 0; i < destinations.size(); ++i) { PersistenceLoader.save(destinations.get(i), root.getRelative(i)); } key.removeKey("helperwaypoints"); root = key.getRelative("guides"); for (int i = 0; i < guides.size(); ++i) { PersistenceLoader.save(guides.get(i), root.getRelative(i)); } if (distance != -1) { key.setDouble("distance", distance); } } @Override public void setPaused(boolean paused) { this.paused = paused; if (currentGoal != null) { currentGoal.onProviderChanged(); } } /** * Returns destination and guide waypoints. */ @Override public Iterable waypoints() { return Iterables.concat(destinations, guides); } private class GuidedAIGoal implements Goal { private GuidedPlan plan; private Waypoint target; public void onProviderChanged() { if (plan == null) return; reset(); if (npc.getNavigator().isNavigating()) { npc.getNavigator().cancelNavigation(); } } @Override public void reset() { plan = null; target = null; } @Override public void run(GoalSelector selector) { if (plan != null && plan.isComplete()) { target.onReach(npc); plan = null; } if (plan == null) { selector.finish(); return; } if (npc.getNavigator().isNavigating()) return; Waypoint current = plan.getCurrentWaypoint(); npc.getNavigator().setTarget(current.getLocation()); npc.getNavigator().getLocalParameters().addSingleUseCallback(cancelReason -> { if (plan != null) { plan.update(npc); } }); } @Override public boolean shouldExecute(GoalSelector selector) { if (paused || destinations.size() == 0 || !npc.isSpawned() || npc.getNavigator().isNavigating()) return false; target = destinations.get(Util.getFastRandom().nextInt(destinations.size())); if (!target.getLocation().getWorld().equals(npc.getEntity().getWorld())) { target = null; return false; } plan = ASTAR.runFully(new GuidedGoal(target), new GuidedNode(null, new Waypoint(npc.getStoredLocation()))); return plan != null; } } private static class GuidedGoal implements AStarGoal { private final Waypoint dest; public GuidedGoal(Waypoint dest) { this.dest = dest; } @Override public float g(GuidedNode from, GuidedNode to) { return (float) from.distance(to.waypoint); } @Override public float getInitialCost(GuidedNode node) { return h(node); } @Override public float h(GuidedNode from) { return (float) from.distance(dest); } @Override public boolean isFinished(GuidedNode node) { return node.waypoint.equals(dest); } } private class GuidedNode extends AStarNode { private final Waypoint waypoint; public GuidedNode(GuidedNode parent, Waypoint waypoint) { super(parent); this.waypoint = waypoint; } @Override public Plan buildPlan() { return new GuidedPlan(this. orderedPath()); } public double distance(Waypoint dest) { return waypoint.distance(dest); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; GuidedNode other = (GuidedNode) obj; if (!Objects.equals(waypoint, other.waypoint)) return false; return true; } @Override public Iterable getNeighbours() { PhTree source = getParent() == null ? tree : treePlusDestinations; PhRangeQuery query = source.rangeQuery( distance == -1 ? npc.getNavigator().getDefaultParameters().range() : distance, waypoint.getLocation().getBlockX(), waypoint.getLocation().getBlockY(), waypoint.getLocation().getBlockZ()); List neighbours = Lists.newArrayList(); query.forEachRemaining(wp -> neighbours.add(new GuidedNode(this, wp))); return neighbours; } @Override public int hashCode() { return 31 + (waypoint == null ? 0 : waypoint.hashCode()); } @Override public String toString() { return "GuidedNode [" + waypoint + "]"; } } private static class GuidedPlan implements Plan { private int index = 0; private final Waypoint[] path; public GuidedPlan(Iterable path) { this.path = Iterables.toArray(Iterables.transform(path, to -> to.waypoint), Waypoint.class); } public Waypoint getCurrentWaypoint() { return path[index]; } @Override public boolean isComplete() { return index >= path.length; } @Override public void update(Agent agent) { index++; } } private static AStarMachine ASTAR = AStarMachine.createWithDefaultStorage(); }