package net.citizensnpcs.npc; import java.util.ArrayList; import java.util.Collection; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.scheduler.BukkitRunnable; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.SetMultimap; import net.citizensnpcs.NPCNeedsRespawnEvent; import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.ai.Navigator; import net.citizensnpcs.api.astar.pathfinder.MinecraftBlockExaminer; import net.citizensnpcs.api.astar.pathfinder.SwimmingExaminer; import net.citizensnpcs.api.event.DespawnReason; import net.citizensnpcs.api.event.NPCDespawnEvent; import net.citizensnpcs.api.event.NPCSpawnEvent; import net.citizensnpcs.api.event.NPCTeleportEvent; import net.citizensnpcs.api.event.SpawnReason; import net.citizensnpcs.api.npc.AbstractNPC; import net.citizensnpcs.api.npc.BlockBreaker; import net.citizensnpcs.api.npc.BlockBreaker.BlockBreakerConfiguration; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPCRegistry; import net.citizensnpcs.api.trait.Trait; import net.citizensnpcs.api.trait.trait.MobType; import net.citizensnpcs.api.trait.trait.Spawned; import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.npc.ai.CitizensNavigator; import net.citizensnpcs.npc.skin.SkinnableEntity; import net.citizensnpcs.trait.CurrentLocation; import net.citizensnpcs.trait.Gravity; import net.citizensnpcs.trait.HologramTrait; import net.citizensnpcs.trait.PacketNPC; import net.citizensnpcs.trait.ScoreboardTrait; import net.citizensnpcs.trait.SitTrait; import net.citizensnpcs.trait.SneakTrait; import net.citizensnpcs.util.ChunkCoord; import net.citizensnpcs.util.Messages; import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.PlayerAnimation; import net.citizensnpcs.util.PlayerUpdateTask; import net.citizensnpcs.util.Util; public class CitizensNPC extends AbstractNPC { private ChunkCoord cachedCoord; private EntityController entityController; private final CitizensNavigator navigator = new CitizensNavigator(this); private int updateCounter = 0; public CitizensNPC(UUID uuid, int id, String name, EntityController controller, NPCRegistry registry) { super(uuid, id, name, registry); setEntityController(controller); } @Override public boolean despawn(DespawnReason reason) { if (getEntity() == null && reason != DespawnReason.DEATH) { Messaging.debug("Tried to despawn", this, "while already despawned, DespawnReason." + reason); if (reason == DespawnReason.RELOAD) { unloadEvents(); } return true; } NPCDespawnEvent event = new NPCDespawnEvent(this, reason); if (reason == DespawnReason.CHUNK_UNLOAD) { event.setCancelled(data().get(NPC.Metadata.KEEP_CHUNK_LOADED, Setting.KEEP_CHUNKS_LOADED.asBoolean())); } Bukkit.getPluginManager().callEvent(event); if (event.isCancelled() && reason != DespawnReason.DEATH) { Messaging.debug("Couldn't despawn", this, "due to despawn event cancellation. Will load chunk.", getEntity().isValid(), ", DespawnReason." + reason); return false; } boolean keepSelected = getOrAddTrait(Spawned.class).shouldSpawn(); if (!keepSelected) { data().remove("selectors"); } if (getEntity() instanceof Player) { PlayerUpdateTask.deregisterPlayer(getEntity()); } navigator.onDespawn(); if (reason == DespawnReason.RELOAD) { unloadEvents(); } for (Trait trait : new ArrayList(traits.values())) { trait.onDespawn(); } Messaging.debug("Despawned", this, "DespawnReason." + reason); if (getEntity() instanceof SkinnableEntity) { ((SkinnableEntity) getEntity()).getSkinTracker().onRemoveNPC(); } if (reason == DespawnReason.DEATH) { entityController.die(); } else { entityController.remove(); } return true; } @Override public void destroy() { super.destroy(); resetCachedCoord(); } @Override public void faceLocation(Location location) { if (!isSpawned()) return; Util.faceLocation(getEntity(), location); } @Override public BlockBreaker getBlockBreaker(Block targetBlock, BlockBreakerConfiguration config) { return NMS.getBlockBreaker(getEntity(), targetBlock, config); } @Override public Entity getEntity() { return entityController == null ? null : entityController.getBukkitEntity(); } public EntityController getEntityController() { return entityController; } @Override public Navigator getNavigator() { return navigator; } @Override public Location getStoredLocation() { return isSpawned() ? getEntity().getLocation() : getOrAddTrait(CurrentLocation.class).getLocation(); } @Override public boolean isFlyable() { updateFlyableState(); return super.isFlyable(); } @Override public boolean isSpawned() { return getEntity() != null && (hasTrait(PacketNPC.class) || NMS.isValid(getEntity())); } @Override public boolean isUpdating(NPCUpdate update) { return update == NPCUpdate.PACKET ? updateCounter > data().get(NPC.Metadata.PACKET_UPDATE_DELAY, Setting.PACKET_UPDATE_DELAY.asInt()) : false; } @Override public void load(final DataKey root) { super.load(root); // Spawn the NPC CurrentLocation spawnLocation = getOrAddTrait(CurrentLocation.class); if (getOrAddTrait(Spawned.class).shouldSpawn() && spawnLocation.getLocation() != null) { if (spawnLocation.getLocation() != null) { spawn(spawnLocation.getLocation(), SpawnReason.RESPAWN); } else { Messaging.debug("Tried to spawn", this, "on load but world was null"); } } navigator.load(root.getRelative("navigator")); } @Override public boolean requiresNameHologram() { return super.requiresNameHologram() || (getEntityType() != EntityType.ARMOR_STAND && Setting.ALWAYS_USE_NAME_HOLOGRAM.asBoolean()); } private void resetCachedCoord() { if (cachedCoord == null) return; Set npcs = CHUNK_LOADERS.get(cachedCoord); npcs.remove(this); if (npcs.size() == 0) { cachedCoord.setForceLoaded(false); } cachedCoord = null; } @Override public void save(DataKey root) { super.save(root); if (!data().get(NPC.Metadata.SHOULD_SAVE, true)) return; navigator.save(root.getRelative("navigator")); } @Override public void scheduleUpdate(NPCUpdate update) { if (update == NPCUpdate.PACKET) { updateCounter = data().get(NPC.Metadata.PACKET_UPDATE_DELAY, Setting.PACKET_UPDATE_DELAY.asTicks()) + 1; } } @Override public void setBukkitEntityType(EntityType type) { EntityController controller = EntityControllers.createForType(type); if (controller == null) throw new IllegalArgumentException("Unsupported entity type " + type); setEntityController(controller); } public void setEntityController(EntityController newController) { Preconditions.checkNotNull(newController); boolean wasSpawned = entityController == null ? false : isSpawned(); Location prev = null; if (wasSpawned) { prev = getEntity().getLocation(CACHE_LOCATION); despawn(DespawnReason.PENDING_RESPAWN); } PacketNPC packet = getTraitNullable(PacketNPC.class); if (packet != null) { newController = packet.wrap(newController); } entityController = newController; if (wasSpawned) { spawn(prev, SpawnReason.RESPAWN); } } @Override public void setFlyable(boolean flyable) { super.setFlyable(flyable); updateFlyableState(); } @Override public void setMoveDestination(Location destination) { if (!isSpawned()) return; if (destination == null) { NMS.cancelMoveDestination(getEntity()); } else { NMS.setDestination(getEntity(), destination.getX(), destination.getY(), destination.getZ(), 1); } } @Override public void setName(String name) { super.setName(name); if (requiresNameHologram() && !hasTrait(HologramTrait.class)) { addTrait(HologramTrait.class); } } @Override public boolean spawn(Location at) { return spawn(at, SpawnReason.PLUGIN); } @Override public boolean spawn(Location at, SpawnReason reason) { Preconditions.checkNotNull(at, "location cannot be null"); Preconditions.checkNotNull(reason, "reason cannot be null"); if (getEntity() != null) { Messaging.debug("Tried to spawn", this, "while already spawned. SpawnReason." + reason); return false; } if (at.getWorld() == null) { Messaging.debug("Tried to spawn", this, "but the world was null. SpawnReason." + reason); return false; } at = at.clone(); if (reason == SpawnReason.CHUNK_LOAD || reason == SpawnReason.COMMAND) { at.getChunk().load(); } getOrAddTrait(CurrentLocation.class).setLocation(at); entityController.create(at.clone(), this); getEntity().setMetadata("NPC", new FixedMetadataValue(CitizensAPI.getPlugin(), true)); Collection onPreSpawn = traits.values(); for (Trait trait : onPreSpawn.toArray(new Trait[onPreSpawn.size()])) { try { trait.onPreSpawn(); } catch (Throwable ex) { Messaging.severeTr(Messages.TRAIT_ONSPAWN_FAILED, trait.getName(), getId()); ex.printStackTrace(); } } boolean loaded = Messaging.isDebugging() ? false : Util.isLoaded(at); boolean couldSpawn = entityController.spawn(at); if (!couldSpawn) { if (Messaging.isDebugging()) { Messaging.debug("Retrying spawn of", this, "later, SpawnReason." + reason + ". Was loaded", loaded, "is loaded", Util.isLoaded(at)); } // we need to wait before trying to spawn entityController.remove(); Bukkit.getPluginManager().callEvent(new NPCNeedsRespawnEvent(this, at)); return false; } // send skin packets, if applicable, before other NMS packets are sent SkinnableEntity skinnable = getEntity() instanceof SkinnableEntity ? ((SkinnableEntity) getEntity()) : null; if (skinnable != null) { skinnable.getSkinTracker().onSpawnNPC(); } NMS.setLocationDirectly(getEntity(), at); NMS.setHeadYaw(getEntity(), at.getYaw()); NMS.setBodyYaw(getEntity(), at.getYaw()); final Location to = at; Consumer postSpawn = new Consumer() { private int timer; @Override public void accept(Runnable cancel) { if (getEntity() == null || (!hasTrait(PacketNPC.class) && !getEntity().isValid())) { if (timer++ > Setting.ENTITY_SPAWN_WAIT_DURATION.asTicks()) { Messaging.debug("Couldn't spawn ", CitizensNPC.this, "waited", timer, "ticks but entity not added to world"); entityController.remove(); cancel.run(); Bukkit.getPluginManager().callEvent(new NPCNeedsRespawnEvent(CitizensNPC.this, to)); } return; } // Set the spawned state getOrAddTrait(CurrentLocation.class).setLocation(to); getOrAddTrait(Spawned.class).setSpawned(true); NPCSpawnEvent spawnEvent = new NPCSpawnEvent(CitizensNPC.this, to, reason); Bukkit.getPluginManager().callEvent(spawnEvent); if (spawnEvent.isCancelled()) { Messaging.debug("Couldn't spawn", CitizensNPC.this, "SpawnReason." + reason, "due to event cancellation."); entityController.remove(); cancel.run(); return; } navigator.onSpawn(); for (Trait trait : Iterables.toArray(traits.values(), Trait.class)) { try { trait.onSpawn(); } catch (Throwable ex) { Messaging.severeTr(Messages.TRAIT_ONSPAWN_FAILED, trait.getName(), getId()); ex.printStackTrace(); } } EntityType type = getEntity().getType(); NMS.replaceTracker(getEntity()); if (type.isAlive()) { LivingEntity entity = (LivingEntity) getEntity(); entity.setRemoveWhenFarAway(false); if (NMS.getStepHeight(entity) < 1) { NMS.setStepHeight(entity, 1); } if (type == EntityType.PLAYER) { PlayerUpdateTask.registerPlayer(getEntity()); } else if (data().has(NPC.Metadata.AGGRESSIVE)) { NMS.setAggressive(entity, data(). get(NPC.Metadata.AGGRESSIVE)); } if (SUPPORT_NODAMAGE_TICKS && (Setting.DEFAULT_SPAWN_NODAMAGE_DURATION.asTicks() != 20 || data().has(NPC.Metadata.SPAWN_NODAMAGE_TICKS))) { try { entity.setNoDamageTicks(data().get(NPC.Metadata.SPAWN_NODAMAGE_TICKS, Setting.DEFAULT_SPAWN_NODAMAGE_DURATION.asTicks())); } catch (NoSuchMethodError err) { SUPPORT_NODAMAGE_TICKS = false; } } } if (requiresNameHologram() && !hasTrait(HologramTrait.class)) { addTrait(HologramTrait.class); } updateFlyableState(); updateCustomNameVisibility(); updateScoreboard(); Messaging.debug("Spawned", CitizensNPC.this, "SpawnReason." + reason); cancel.run(); } }; if (getEntity() != null && getEntity().isValid()) { postSpawn.accept(() -> { }); } else { new BukkitRunnable() { @Override public void run() { postSpawn.accept(() -> cancel()); } }.runTaskTimer(CitizensAPI.getPlugin(), 0, 1); } return true; } @Override public void teleport(Location location, TeleportCause reason) { if (!isSpawned()) return; if (hasTrait(SitTrait.class)) { getOrAddTrait(SitTrait.class).setSitting(location); } Location npcLoc = getEntity().getLocation(CACHE_LOCATION); if (isSpawned() && npcLoc.getWorld() == location.getWorld()) { if (npcLoc.distance(location) < 1) { NMS.setHeadYaw(getEntity(), location.getYaw()); } if (getEntity().getType() == EntityType.PLAYER && !getEntity().isInsideVehicle() && NMS.getPassengers(getEntity()).size() == 0) { NPCTeleportEvent event = new NPCTeleportEvent(this, location); Bukkit.getPluginManager().callEvent(event); if (event.isCancelled()) return; NMS.setLocationDirectly(getEntity(), location); return; } } super.teleport(location, reason); } @Override public String toString() { EntityType mobType = hasTrait(MobType.class) ? getTraitNullable(MobType.class).getType() : null; return getId() + "{" + getFullName() + ", " + mobType + "}"; } @Override public void update() { try { super.update(); if (!isSpawned()) { resetCachedCoord(); return; } if (data().has(NPC.Metadata.ACTIVATION_RANGE)) { int range = data().get(NPC.Metadata.ACTIVATION_RANGE); if (range == -1 || CitizensAPI.getLocationLookup().getNearbyPlayers(getStoredLocation(), range) .iterator().hasNext()) { NMS.activate(getEntity()); } } boolean shouldSwim = data().get(NPC.Metadata.SWIMMING, SwimmingExaminer.isWaterMob(getEntity())) && MinecraftBlockExaminer.isLiquid(getEntity().getLocation().getBlock().getType()); if (navigator.isNavigating()) { if (shouldSwim) { getEntity().setVelocity(getEntity().getVelocity().multiply( data().get(NPC.Metadata.WATER_SPEED_MODIFIER, Setting.NPC_WATER_SPEED_MODIFIER.asFloat()))); Location currentDest = navigator.getPathStrategy().getCurrentDestination(); if (currentDest == null || currentDest.getY() > getStoredLocation().getY()) { NMS.trySwim(getEntity()); } } } else if (shouldSwim) { Gravity trait = getTraitNullable(Gravity.class); if (trait == null || trait.hasGravity()) { NMS.trySwim(getEntity()); } } if (SUPPORT_GLOWING && data().has(NPC.Metadata.GLOWING)) { try { getEntity().setGlowing(data().get(NPC.Metadata.GLOWING, false)); } catch (NoSuchMethodError e) { SUPPORT_GLOWING = false; } } if (SUPPORT_SILENT && data().has(NPC.Metadata.SILENT)) { try { getEntity().setSilent(Boolean.parseBoolean(data().get(NPC.Metadata.SILENT).toString())); } catch (NoSuchMethodError e) { SUPPORT_SILENT = false; } } boolean isLiving = getEntity() instanceof LivingEntity; if (isUpdating(NPCUpdate.PACKET)) { if (data().get(NPC.Metadata.KEEP_CHUNK_LOADED, Setting.KEEP_CHUNKS_LOADED.asBoolean())) { ChunkCoord currentCoord = new ChunkCoord(getStoredLocation()); if (!currentCoord.equals(cachedCoord)) { resetCachedCoord(); currentCoord.setForceLoaded(true); CHUNK_LOADERS.put(currentCoord, this); cachedCoord = currentCoord; } } if (isLiving) { updateScoreboard(); } updateCounter = 0; } updateCustomNameVisibility(); if (isLiving) { NMS.setKnockbackResistance((LivingEntity) getEntity(), isProtected() ? 1D : 0D); if (SUPPORT_PICKUP_ITEMS) { try { ((LivingEntity) getEntity()).setCanPickupItems(data().get(NPC.Metadata.PICKUP_ITEMS, false)); } catch (Throwable t) { SUPPORT_PICKUP_ITEMS = false; } } if (getEntity() instanceof Player) { updateUsingItemState((Player) getEntity()); if (data().has(NPC.Metadata.SNEAKING) && !hasTrait(SneakTrait.class)) { addTrait(SneakTrait.class); } } } navigator.run(); updateCounter++; } catch (Exception ex) { Throwable error = Throwables.getRootCause(ex); Messaging.logTr(Messages.EXCEPTION_UPDATING_NPC, getId(), error.getMessage()); error.printStackTrace(); } } @Override public void updateCustomName() { if (coloredNameComponentCache != null) { NMS.setCustomName(getEntity(), coloredNameComponentCache, coloredNameStringCache); } else { super.updateCustomName(); } } private void updateCustomNameVisibility() { String nameplateVisible = data(). get(NPC.Metadata.NAMEPLATE_VISIBLE, true).toString(); if (requiresNameHologram()) { nameplateVisible = "false"; } if (nameplateVisible.equals("true") || nameplateVisible.equals("hover")) { updateCustomName(); } getEntity().setCustomNameVisible(Boolean.parseBoolean(nameplateVisible)); } private void updateFlyableState() { EntityType type = isSpawned() ? getEntity().getType() : getOrAddTrait(MobType.class).getType(); if (type == null) return; if (!Util.isAlwaysFlyable(type)) return; if (!data().has(NPC.Metadata.FLYABLE)) { data().setPersistent(NPC.Metadata.FLYABLE, true); } if (!hasTrait(Gravity.class)) { getOrAddTrait(Gravity.class).setEnabled(true); } } private void updateScoreboard() { if (data().has(NPC.Metadata.SCOREBOARD_FAKE_TEAM_NAME)) { getOrAddTrait(ScoreboardTrait.class).update(); } } private void updateUsingItemState(Player player) { boolean useItem = data().get(NPC.Metadata.USING_HELD_ITEM, false), offhand = data().get(NPC.Metadata.USING_OFFHAND_ITEM, false); if (!SUPPORT_USE_ITEM) return; try { if (useItem) { NMS.playAnimation(PlayerAnimation.STOP_USE_ITEM, player, 64); NMS.playAnimation(PlayerAnimation.START_USE_MAINHAND_ITEM, player, 64); } else if (offhand) { NMS.playAnimation(PlayerAnimation.STOP_USE_ITEM, player, 64); NMS.playAnimation(PlayerAnimation.START_USE_OFFHAND_ITEM, player, 64); } } catch (UnsupportedOperationException ex) { SUPPORT_USE_ITEM = false; } } private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0); private static final SetMultimap CHUNK_LOADERS = HashMultimap.create(); private static boolean SUPPORT_GLOWING = true; private static boolean SUPPORT_NODAMAGE_TICKS = true; private static boolean SUPPORT_PICKUP_ITEMS = true; private static boolean SUPPORT_SILENT = true; private static boolean SUPPORT_USE_ITEM = true; }