diff --git a/dist/pom.xml b/dist/pom.xml index a03dcc499..6e6832046 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -4,7 +4,7 @@ net.citizensnpcs citizens-parent - 2.0.20-SNAPSHOT + 2.0.21-SNAPSHOT citizens pom @@ -40,13 +40,20 @@ ${project.version} jar compile - - + + ${project.groupId} citizens-v1_10_R1 ${project.version} jar compile + + ${project.groupId} + citizens-v1_11_R1 + ${project.version} + jar + compile + \ No newline at end of file diff --git a/main/java/net/citizensnpcs/Citizens.java b/main/java/net/citizensnpcs/Citizens.java new file mode 100644 index 000000000..7154b46ad --- /dev/null +++ b/main/java/net/citizensnpcs/Citizens.java @@ -0,0 +1,470 @@ +package net.citizensnpcs; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.CitizensPlugin; +import net.citizensnpcs.api.ai.speech.SpeechFactory; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.CommandManager; +import net.citizensnpcs.api.command.CommandManager.CommandInfo; +import net.citizensnpcs.api.command.Injector; +import net.citizensnpcs.api.event.CitizensDisableEvent; +import net.citizensnpcs.api.event.CitizensEnableEvent; +import net.citizensnpcs.api.event.CitizensPreReloadEvent; +import net.citizensnpcs.api.event.CitizensReloadEvent; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.exception.NPCLoadException; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCDataStore; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.npc.SimpleNPCDataStore; +import net.citizensnpcs.api.scripting.EventRegistrar; +import net.citizensnpcs.api.scripting.ObjectProvider; +import net.citizensnpcs.api.scripting.ScriptCompiler; +import net.citizensnpcs.api.trait.TraitFactory; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.api.util.NBTStorage; +import net.citizensnpcs.api.util.Storage; +import net.citizensnpcs.api.util.Translator; +import net.citizensnpcs.api.util.YamlStorage; +import net.citizensnpcs.commands.AdminCommands; +import net.citizensnpcs.commands.EditorCommands; +import net.citizensnpcs.commands.NPCCommands; +import net.citizensnpcs.commands.TemplateCommands; +import net.citizensnpcs.commands.TraitCommands; +import net.citizensnpcs.commands.WaypointCommands; +import net.citizensnpcs.editor.Editor; +import net.citizensnpcs.npc.CitizensNPCRegistry; +import net.citizensnpcs.npc.CitizensTraitFactory; +import net.citizensnpcs.npc.NPCSelector; +import net.citizensnpcs.npc.ai.speech.Chat; +import net.citizensnpcs.npc.ai.speech.CitizensSpeechFactory; +import net.citizensnpcs.npc.profile.ProfileFetcher; +import net.citizensnpcs.npc.skin.Skin; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.NMS; +import net.citizensnpcs.util.PlayerUpdateTask; +import net.citizensnpcs.util.StringHelper; +import net.citizensnpcs.util.Util; +import net.milkbowl.vault.economy.Economy; + +public class Citizens extends JavaPlugin implements CitizensPlugin { + private final CommandManager commands = new CommandManager(); + private boolean compatible; + private Settings config; + private CitizensNPCRegistry npcRegistry; + private NPCDataStore saves; + private NPCSelector selector; + private CitizensSpeechFactory speechFactory; + private final Map storedRegistries = Maps.newHashMap(); + private CitizensTraitFactory traitFactory; + + @Override + public NPCRegistry createAnonymousNPCRegistry(NPCDataStore store) { + return new CitizensNPCRegistry(store); + } + + @Override + public NPCRegistry createNamedNPCRegistry(String name, NPCDataStore store) { + NPCRegistry created = new CitizensNPCRegistry(store); + storedRegistries.put(name, created); + return created; + } + + private NPCDataStore createStorage(File folder) { + Storage saves = null; + String type = Setting.STORAGE_TYPE.asString(); + if (type.equalsIgnoreCase("nbt")) { + saves = new NBTStorage(new File(folder + File.separator + Setting.STORAGE_FILE.asString()), + "Citizens NPC Storage"); + } + if (saves == null) { + saves = new YamlStorage(new File(folder, Setting.STORAGE_FILE.asString()), "Citizens NPC Storage"); + } + if (!saves.load()) + return null; + return SimpleNPCDataStore.create(saves); + } + + private void despawnNPCs() { + Iterator itr = npcRegistry.iterator(); + while (itr.hasNext()) { + NPC npc = itr.next(); + try { + npc.despawn(DespawnReason.RELOAD); + } catch (Throwable e) { + e.printStackTrace(); + // ensure that all entities are despawned + } + itr.remove(); + } + } + + private void enableSubPlugins() { + File root = new File(getDataFolder(), Setting.SUBPLUGIN_FOLDER.asString()); + if (!root.exists() || !root.isDirectory()) + return; + File[] files = root.listFiles(); + for (File file : files) { + Plugin plugin; + try { + plugin = Bukkit.getPluginManager().loadPlugin(file); + } catch (Exception e) { + continue; + } + if (plugin == null) + continue; + // code beneath modified from CraftServer + try { + Messaging.logTr(Messages.LOADING_SUB_PLUGIN, plugin.getDescription().getFullName()); + plugin.onLoad(); + } catch (Throwable ex) { + Messaging.severeTr(Messages.ERROR_INITALISING_SUB_PLUGIN, ex.getMessage(), + plugin.getDescription().getFullName()); + ex.printStackTrace(); + } + } + NMS.loadPlugins(); + } + + public CommandInfo getCommandInfo(String rootCommand, String modifier) { + return commands.getCommand(rootCommand, modifier); + } + + public Iterable getCommands(String base) { + return commands.getCommands(base); + } + + @Override + public net.citizensnpcs.api.npc.NPCSelector getDefaultNPCSelector() { + return selector; + } + + @Override + public NPCRegistry getNamedNPCRegistry(String name) { + return storedRegistries.get(name); + } + + @Override + public Iterable getNPCRegistries() { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + Iterator stored; + + @Override + public boolean hasNext() { + return stored == null ? true : stored.hasNext(); + } + + @Override + public NPCRegistry next() { + if (stored == null) { + stored = storedRegistries.values().iterator(); + return npcRegistry; + } + return stored.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + @Override + public NPCRegistry getNPCRegistry() { + return npcRegistry; + } + + public NPCSelector getNPCSelector() { + return selector; + } + + @Override + public ClassLoader getOwningClassLoader() { + return getClassLoader(); + } + + @Override + public File getScriptFolder() { + return new File(getDataFolder(), "scripts"); + } + + @Override + public SpeechFactory getSpeechFactory() { + return speechFactory; + } + + @Override + public TraitFactory getTraitFactory() { + return traitFactory; + } + + @Override + public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String cmdName, String[] args) { + String modifier = args.length > 0 ? args[0] : ""; + if (!commands.hasCommand(command, modifier) && !modifier.isEmpty()) { + return suggestClosestModifier(sender, command.getName(), modifier); + } + + NPC npc = selector == null ? null : selector.getSelected(sender); + // TODO: change the args supplied to a context style system for + // flexibility (ie. adding more context in the future without + // changing everything) + + Object[] methodArgs = { sender, npc }; + return commands.executeSafe(command, args, sender, methodArgs); + } + + @Override + public void onDisable() { + Bukkit.getPluginManager().callEvent(new CitizensDisableEvent()); + Editor.leaveAll(); + + if (compatible) { + saves.storeAll(npcRegistry); + saves.saveToDiskImmediate(); + despawnNPCs(); + npcRegistry = null; + } + + CitizensAPI.shutdown(); + } + + @Override + public void onEnable() { + setupTranslator(); + CitizensAPI.setImplementation(this); + config = new Settings(getDataFolder()); + // Disable if the server is not using the compatible Minecraft version + String mcVersion = Util.getMinecraftRevision(); + compatible = true; + try { + NMS.loadBridge(mcVersion); + } catch (Exception e) { + compatible = false; + if (Messaging.isDebugging()) { + e.printStackTrace(); + } + Messaging.severeTr(Messages.CITIZENS_INCOMPATIBLE, getDescription().getVersion(), mcVersion); + getServer().getPluginManager().disablePlugin(this); + return; + } + registerScriptHelpers(); + + saves = createStorage(getDataFolder()); + if (saves == null) { + Messaging.severeTr(Messages.FAILED_LOAD_SAVES); + getServer().getPluginManager().disablePlugin(this); + return; + } + + npcRegistry = new CitizensNPCRegistry(saves); + traitFactory = new CitizensTraitFactory(); + selector = new NPCSelector(this); + speechFactory = new CitizensSpeechFactory(); + speechFactory.register(Chat.class, "chat"); + + getServer().getPluginManager().registerEvents(new EventListen(storedRegistries), this); + + if (Setting.NPC_COST.asDouble() > 0) { + setupEconomy(); + } + + registerCommands(); + enableSubPlugins(); + + // Setup NPCs after all plugins have been enabled (allows for multiworld + // support and for NPCs to properly register external settings) + if (getServer().getScheduler().scheduleSyncDelayedTask(this, new Runnable() { + @Override + public void run() { + saves.loadInto(npcRegistry); + Messaging.logTr(Messages.NUM_LOADED_NOTIFICATION, Iterables.size(npcRegistry), "?"); + startMetrics(); + scheduleSaveTask(Setting.SAVE_TASK_DELAY.asInt()); + Bukkit.getPluginManager().callEvent(new CitizensEnableEvent()); + new PlayerUpdateTask().runTaskTimer(Citizens.this, 0, 1); + } + }, 1) == -1) { + Messaging.severeTr(Messages.LOAD_TASK_NOT_SCHEDULED); + getServer().getPluginManager().disablePlugin(this); + } + } + + @Override + public void onImplementationChanged() { + Messaging.severeTr(Messages.CITIZENS_IMPLEMENTATION_DISABLED); + Bukkit.getPluginManager().disablePlugin(this); + } + + public void registerCommandClass(Class clazz) { + try { + commands.register(clazz); + } catch (Throwable ex) { + Messaging.logTr(Messages.CITIZENS_INVALID_COMMAND_CLASS); + ex.printStackTrace(); + } + } + + private void registerCommands() { + commands.setInjector(new Injector(this)); + // Register command classes + commands.register(AdminCommands.class); + commands.register(EditorCommands.class); + commands.register(NPCCommands.class); + commands.register(TemplateCommands.class); + commands.register(TraitCommands.class); + commands.register(WaypointCommands.class); + } + + private void registerScriptHelpers() { + ScriptCompiler compiler = CitizensAPI.getScriptCompiler(); + compiler.registerGlobalContextProvider(new EventRegistrar(this)); + compiler.registerGlobalContextProvider(new ObjectProvider("plugin", this)); + } + + public void reload() throws NPCLoadException { + Editor.leaveAll(); + config.reload(); + despawnNPCs(); + ProfileFetcher.reset(); + Skin.clearCache(); + getServer().getPluginManager().callEvent(new CitizensPreReloadEvent()); + + saves = createStorage(getDataFolder()); + saves.loadInto(npcRegistry); + + getServer().getPluginManager().callEvent(new CitizensReloadEvent()); + } + + @Override + public void removeNamedNPCRegistry(String name) { + storedRegistries.remove(name); + } + + private void scheduleSaveTask(int delay) { + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, new Runnable() { + @Override + public void run() { + storeNPCs(); + saves.saveToDisk(); + } + }, delay, delay); + } + + private void setupEconomy() { + try { + RegisteredServiceProvider provider = Bukkit.getServicesManager().getRegistration(Economy.class); + if (provider != null && provider.getProvider() != null) { + Economy economy = provider.getProvider(); + Bukkit.getPluginManager().registerEvents(new PaymentListener(economy), this); + } + } catch (NoClassDefFoundError e) { + Messaging.logTr(Messages.ERROR_LOADING_ECONOMY); + } + } + + private void setupTranslator() { + Locale locale = Locale.getDefault(); + String setting = Setting.LOCALE.asString(); + if (!setting.isEmpty()) { + String[] parts = setting.split("[\\._]"); + switch (parts.length) { + case 1: + locale = new Locale(parts[0]); + break; + case 2: + locale = new Locale(parts[0], parts[1]); + break; + case 3: + locale = new Locale(parts[0], parts[1], parts[2]); + break; + default: + break; + } + } + Translator.setInstance(new File(getDataFolder(), "lang"), locale); + } + + private void startMetrics() { + try { + Metrics metrics = new Metrics(Citizens.this); + if (metrics.isOptOut()) + return; + metrics.addCustomData(new Metrics.Plotter("Total NPCs") { + @Override + public int getValue() { + if (npcRegistry == null) + return 0; + return Iterables.size(npcRegistry); + } + }); + metrics.addCustomData(new Metrics.Plotter("Total goals") { + @Override + public int getValue() { + if (npcRegistry == null) + return 0; + int goalCount = 0; + for (NPC npc : npcRegistry) { + goalCount += Iterables.size(npc.getDefaultGoalController()); + } + return goalCount; + } + }); + traitFactory.addPlotters(metrics.createGraph("traits")); + metrics.start(); + } catch (IOException e) { + Messaging.logTr(Messages.METRICS_ERROR_NOTIFICATION, e.getMessage()); + } + } + + public void storeNPCs() { + if (saves == null) + return; + for (NPC npc : npcRegistry) { + saves.store(npc); + } + } + + public void storeNPCs(CommandContext args) { + storeNPCs(); + boolean async = args.hasFlag('a'); + if (async) { + saves.saveToDisk(); + } else { + saves.saveToDiskImmediate(); + } + } + + private boolean suggestClosestModifier(CommandSender sender, String command, String modifier) { + String closest = commands.getClosestCommandModifier(command, modifier); + if (!closest.isEmpty()) { + sender.sendMessage(ChatColor.GRAY + Messaging.tr(Messages.UNKNOWN_COMMAND)); + sender.sendMessage(StringHelper.wrap(" /") + command + " " + StringHelper.wrap(closest)); + return true; + } + return false; + } +} diff --git a/main/java/net/citizensnpcs/EventListen.java b/main/java/net/citizensnpcs/EventListen.java new file mode 100644 index 000000000..4f3128dcc --- /dev/null +++ b/main/java/net/citizensnpcs/EventListen.java @@ -0,0 +1,614 @@ +package net.citizensnpcs; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.FishHook; +import org.bukkit.entity.Minecart; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityCombustByBlockEvent; +import org.bukkit.event.entity.EntityCombustByEntityEvent; +import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityDamageByBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.entity.ProjectileHitEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerFishEvent; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; +import org.bukkit.event.vehicle.VehicleDestroyEvent; +import org.bukkit.event.vehicle.VehicleEnterEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scoreboard.Team; + +import com.google.common.base.Predicates; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; + +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.ai.event.NavigationBeginEvent; +import net.citizensnpcs.api.ai.event.NavigationCompleteEvent; +import net.citizensnpcs.api.event.CitizensDeserialiseMetaEvent; +import net.citizensnpcs.api.event.CitizensPreReloadEvent; +import net.citizensnpcs.api.event.CitizensSerialiseMetaEvent; +import net.citizensnpcs.api.event.CommandSenderCreateNPCEvent; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.event.EntityTargetNPCEvent; +import net.citizensnpcs.api.event.NPCCombustByBlockEvent; +import net.citizensnpcs.api.event.NPCCombustByEntityEvent; +import net.citizensnpcs.api.event.NPCCombustEvent; +import net.citizensnpcs.api.event.NPCDamageByBlockEvent; +import net.citizensnpcs.api.event.NPCDamageByEntityEvent; +import net.citizensnpcs.api.event.NPCDamageEntityEvent; +import net.citizensnpcs.api.event.NPCDamageEvent; +import net.citizensnpcs.api.event.NPCDeathEvent; +import net.citizensnpcs.api.event.NPCDespawnEvent; +import net.citizensnpcs.api.event.NPCLeftClickEvent; +import net.citizensnpcs.api.event.NPCRightClickEvent; +import net.citizensnpcs.api.event.NPCSpawnEvent; +import net.citizensnpcs.api.event.PlayerCreateNPCEvent; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.trait.trait.Owner; +import net.citizensnpcs.api.util.DataKey; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.editor.Editor; +import net.citizensnpcs.npc.skin.SkinUpdateTracker; +import net.citizensnpcs.trait.Controllable; +import net.citizensnpcs.trait.CurrentLocation; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.NMS; + +public class EventListen implements Listener { + private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry(); + private final Map registries; + private final SkinUpdateTracker skinUpdateTracker; + private final ListMultimap toRespawn = ArrayListMultimap.create(); + + EventListen(Map registries) { + this.registries = registries; + this.skinUpdateTracker = new SkinUpdateTracker(npcRegistry, registries); + } + + private void checkCreationEvent(CommandSenderCreateNPCEvent event) { + if (event.getCreator().hasPermission("citizens.admin.avoid-limits")) + return; + int limit = Setting.DEFAULT_NPC_LIMIT.asInt(); + int maxChecks = Setting.MAX_NPC_LIMIT_CHECKS.asInt(); + for (int i = maxChecks; i >= 0; i--) { + if (!event.getCreator().hasPermission("citizens.npc.limit." + i)) + continue; + limit = i; + break; + } + if (limit < 0) + return; + int owned = 0; + for (NPC npc : npcRegistry) { + if (!event.getNPC().equals(npc) && npc.hasTrait(Owner.class) + && npc.getTrait(Owner.class).isOwnedBy(event.getCreator())) + owned++; + } + int wouldOwn = owned + 1; + if (wouldOwn > limit) { + event.setCancelled(true); + event.setCancelReason(Messaging.tr(Messages.OVER_NPC_LIMIT, limit)); + } + } + + private Iterable getAllNPCs() { + return Iterables.filter(Iterables. concat(npcRegistry, Iterables.concat(registries.values())), + Predicates.notNull()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onChunkLoad(ChunkLoadEvent event) { + respawnAllFromCoord(toCoord(event.getChunk())); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onChunkUnload(ChunkUnloadEvent event) { + ChunkCoord coord = toCoord(event.getChunk()); + Location loc = new Location(null, 0, 0, 0); + for (NPC npc : getAllNPCs()) { + if (npc == null || !npc.isSpawned()) + continue; + loc = npc.getEntity().getLocation(loc); + boolean sameChunkCoordinates = coord.z == loc.getBlockZ() >> 4 && coord.x == loc.getBlockX() >> 4; + if (!sameChunkCoordinates || !event.getWorld().equals(loc.getWorld())) + continue; + if (!npc.despawn(DespawnReason.CHUNK_UNLOAD)) { + event.setCancelled(true); + if (Messaging.isDebugging()) { + Messaging.debug("Cancelled chunk unload at [" + coord.x + "," + coord.z + "]"); + } + respawnAllFromCoord(coord); + return; + } + toRespawn.put(coord, npc); + if (Messaging.isDebugging()) { + Messaging.debug("Despawned id", npc.getId(), + "due to chunk unload at [" + coord.x + "," + coord.z + "]"); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onCitizensReload(CitizensPreReloadEvent event) { + skinUpdateTracker.reset(); + toRespawn.clear(); + } + + @EventHandler(ignoreCancelled = true) + public void onCommandSenderCreateNPC(CommandSenderCreateNPCEvent event) { + checkCreationEvent(event); + } + + /* + * Entity events + */ + @EventHandler + public void onEntityCombust(EntityCombustEvent event) { + NPC npc = npcRegistry.getNPC(event.getEntity()); + if (npc == null) + return; + event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true)); + if (event instanceof EntityCombustByEntityEvent) { + Bukkit.getPluginManager().callEvent(new NPCCombustByEntityEvent((EntityCombustByEntityEvent) event, npc)); + } else if (event instanceof EntityCombustByBlockEvent) { + Bukkit.getPluginManager().callEvent(new NPCCombustByBlockEvent((EntityCombustByBlockEvent) event, npc)); + } else { + Bukkit.getPluginManager().callEvent(new NPCCombustEvent(event, npc)); + } + } + + @EventHandler + public void onEntityDamage(EntityDamageEvent event) { + NPC npc = npcRegistry.getNPC(event.getEntity()); + + if (npc == null) { + if (event instanceof EntityDamageByEntityEvent) { + npc = npcRegistry.getNPC(((EntityDamageByEntityEvent) event).getDamager()); + if (npc == null) + return; + event.setCancelled(!npc.data().get(NPC.DAMAGE_OTHERS_METADATA, true)); + NPCDamageEntityEvent damageEvent = new NPCDamageEntityEvent(npc, (EntityDamageByEntityEvent) event); + Bukkit.getPluginManager().callEvent(damageEvent); + } + return; + } + event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true)); + if (event instanceof EntityDamageByEntityEvent) { + NPCDamageByEntityEvent damageEvent = new NPCDamageByEntityEvent(npc, (EntityDamageByEntityEvent) event); + Bukkit.getPluginManager().callEvent(damageEvent); + + if (!damageEvent.isCancelled() || !(damageEvent.getDamager() instanceof Player)) + return; + Player damager = (Player) damageEvent.getDamager(); + + NPCLeftClickEvent leftClickEvent = new NPCLeftClickEvent(npc, damager); + Bukkit.getPluginManager().callEvent(leftClickEvent); + } else if (event instanceof EntityDamageByBlockEvent) { + Bukkit.getPluginManager().callEvent(new NPCDamageByBlockEvent(npc, (EntityDamageByBlockEvent) event)); + } else { + Bukkit.getPluginManager().callEvent(new NPCDamageEvent(npc, event)); + } + } + + @EventHandler(ignoreCancelled = true) + public void onEntityDeath(EntityDeathEvent event) { + final NPC npc = npcRegistry.getNPC(event.getEntity()); + if (npc == null) { + return; + } + + if (!npc.data().get(NPC.DROPS_ITEMS_METADATA, false)) { + event.getDrops().clear(); + } + + final Location location = npc.getEntity().getLocation(); + Bukkit.getPluginManager().callEvent(new NPCDeathEvent(npc, event)); + npc.despawn(DespawnReason.DEATH); + + if (npc.data().has(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA)) { + String teamName = npc.data().get(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA); + Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName); + if (team != null) { + team.unregister(); + } + + npc.data().remove(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA); + } + + if (npc.data().get(NPC.RESPAWN_DELAY_METADATA, -1) >= 0) { + int delay = npc.data().get(NPC.RESPAWN_DELAY_METADATA, -1); + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override + public void run() { + if (!npc.isSpawned() && npc.getOwningRegistry().getByUniqueId(npc.getUniqueId()) == npc) { + npc.spawn(location); + } + } + }, delay + 2); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onEntitySpawn(CreatureSpawnEvent event) { + if (event.isCancelled() && npcRegistry.isNPC(event.getEntity())) { + event.setCancelled(false); + } + } + + @EventHandler + public void onEntityTarget(EntityTargetEvent event) { + NPC npc = npcRegistry.getNPC(event.getTarget()); + if (npc == null) + return; + event.setCancelled( + !npc.data().get(NPC.TARGETABLE_METADATA, !npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true))); + Bukkit.getPluginManager().callEvent(new EntityTargetNPCEvent(event, npc)); + } + + @EventHandler + public void onMetaDeserialise(CitizensDeserialiseMetaEvent event) { + if (event.getKey().keyExists("skull")) { + String owner = event.getKey().getString("skull.owner", ""); + UUID uuid = event.getKey().keyExists("skull.uuid") ? UUID.fromString(event.getKey().getString("skull.uuid")) + : null; + if (owner.isEmpty() && uuid == null) { + return; + } + GameProfile profile = new GameProfile(uuid, owner); + for (DataKey sub : event.getKey().getRelative("skull.properties").getSubKeys()) { + String propertyName = sub.name(); + for (DataKey property : sub.getIntegerSubKeys()) { + profile.getProperties().put(propertyName, + new Property(property.getString("name"), property.getString("value"), + property.keyExists("signature") ? property.getString("signature") : null)); + } + } + SkullMeta meta = (SkullMeta) Bukkit.getItemFactory().getItemMeta(Material.SKULL_ITEM); + NMS.setProfile(meta, profile); + event.getItemStack().setItemMeta(meta); + } + } + + @EventHandler + public void onMetaSerialise(CitizensSerialiseMetaEvent event) { + if (!(event.getMeta() instanceof SkullMeta)) + return; + SkullMeta meta = (SkullMeta) event.getMeta(); + GameProfile profile = NMS.getProfile(meta); + if (profile == null) + return; + if (profile.getName() != null) { + event.getKey().setString("skull.owner", profile.getName()); + } + if (profile.getId() != null) { + event.getKey().setString("skull.uuid", profile.getId().toString()); + } + if (profile.getProperties() != null) { + for (Entry> entry : profile.getProperties().asMap().entrySet()) { + DataKey relative = event.getKey().getRelative("skull.properties." + entry.getKey()); + int i = 0; + for (Property value : entry.getValue()) { + relative.getRelative(i).setString("name", value.getName()); + if (value.getSignature() != null) { + relative.getRelative(i).setString("signature", value.getSignature()); + } + relative.getRelative(i).setString("value", value.getValue()); + i++; + } + } + } + } + + @EventHandler + public void onNavigationBegin(NavigationBeginEvent event) { + skinUpdateTracker.onNPCNavigationBegin(event.getNPC()); + } + + @EventHandler + public void onNavigationComplete(NavigationCompleteEvent event) { + skinUpdateTracker.onNPCNavigationComplete(event.getNPC()); + } + + @EventHandler + public void onNeedsRespawn(NPCNeedsRespawnEvent event) { + ChunkCoord coord = toCoord(event.getSpawnLocation()); + if (toRespawn.containsEntry(coord, event.getNPC())) + return; + Messaging.debug("Stored", event.getNPC().getId(), "for respawn from NPCNeedsRespawnEvent"); + toRespawn.put(coord, event.getNPC()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onNPCDespawn(NPCDespawnEvent event) { + if (event.getReason() == DespawnReason.PLUGIN || event.getReason() == DespawnReason.REMOVAL + || event.getReason() == DespawnReason.RELOAD) { + Messaging.debug("Preventing further respawns of " + event.getNPC().getId() + " due to DespawnReason." + + event.getReason().name()); + if (event.getNPC().getStoredLocation() != null) { + toRespawn.remove(toCoord(event.getNPC().getStoredLocation()), event.getNPC()); + } + } else { + Messaging.debug("Removing " + event.getNPC().getId() + " from skin tracker due to DespawnReason." + + event.getReason().name()); + } + skinUpdateTracker.onNPCDespawn(event.getNPC()); + } + + @EventHandler(ignoreCancelled = true) + public void onNPCSpawn(NPCSpawnEvent event) { + skinUpdateTracker.onNPCSpawn(event.getNPC()); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerChangedWorld(PlayerChangedWorldEvent event) { + if (npcRegistry.getNPC(event.getPlayer()) == null) + return; + NMS.removeFromServerPlayerList(event.getPlayer()); + // on teleport, player NPCs are added to the server player list. this is + // undesirable as player NPCs are not real players and confuse plugins. + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerChangeWorld(PlayerChangedWorldEvent event) { + skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerCreateNPC(PlayerCreateNPCEvent event) { + checkCreationEvent(event); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerFish(PlayerFishEvent event) { + if (npcRegistry.isNPC(event.getCaught()) && npcRegistry.getNPC(event.getCaught()).isProtected()) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + NPC npc = npcRegistry.getNPC(event.getRightClicked()); + if (npc == null || event.getHand() == EquipmentSlot.OFF_HAND) { + return; + } + Player player = event.getPlayer(); + NPCRightClickEvent rightClickEvent = new NPCRightClickEvent(npc, player); + Bukkit.getPluginManager().callEvent(rightClickEvent); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true); + } + + // recalculate player NPCs the first time a player moves and every time + // a player moves a certain distance from their last position. + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerMove(final PlayerMoveEvent event) { + skinUpdateTracker.onPlayerMove(event.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerQuit(PlayerQuitEvent event) { + Editor.leave(event.getPlayer()); + if (event.getPlayer().isInsideVehicle()) { + NPC npc = npcRegistry.getNPC(event.getPlayer().getVehicle()); + if (npc != null) { + event.getPlayer().leaveVehicle(); + } + } + skinUpdateTracker.removePlayer(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerRespawn(PlayerRespawnEvent event) { + skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerTeleport(final PlayerTeleportEvent event) { + if (event.getCause() == TeleportCause.PLUGIN && !event.getPlayer().hasMetadata("citizens-force-teleporting") + && npcRegistry.getNPC(event.getPlayer()) != null && Setting.TELEPORT_DELAY.asInt() > 0) { + event.setCancelled(true); + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override + public void run() { + event.getPlayer().setMetadata("citizens-force-teleporting", + new FixedMetadataValue(CitizensAPI.getPlugin(), true)); + event.getPlayer().teleport(event.getTo()); + event.getPlayer().removeMetadata("citizens-force-teleporting", CitizensAPI.getPlugin()); + } + }, Setting.TELEPORT_DELAY.asInt()); + } + skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true); + } + + @EventHandler(ignoreCancelled = true) + public void onProjectileHit(final ProjectileHitEvent event) { + if (!(event.getEntity() instanceof FishHook)) + return; + NMS.removeHookIfNecessary(npcRegistry, (FishHook) event.getEntity()); + new BukkitRunnable() { + int n = 0; + + @Override + public void run() { + if (n++ > 5) { + cancel(); + } + NMS.removeHookIfNecessary(npcRegistry, (FishHook) event.getEntity()); + } + }.runTaskTimer(CitizensAPI.getPlugin(), 0, 1); + } + + @EventHandler + public void onVehicleDestroy(VehicleDestroyEvent event) { + NPC npc = npcRegistry.getNPC(event.getVehicle()); + if (npc == null) { + return; + } + event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true)); + } + + @EventHandler(ignoreCancelled = true) + public void onVehicleEnter(VehicleEnterEvent event) { + if (!npcRegistry.isNPC(event.getEntered())) + return; + NPC npc = npcRegistry.getNPC(event.getEntered()); + if ((npc.getEntity().getType() == EntityType.HORSE || npc.getEntity().getType() == EntityType.BOAT + || npc.getEntity() instanceof Minecart) && !npc.getTrait(Controllable.class).isEnabled()) { + event.setCancelled(true); + } + } + + @EventHandler(ignoreCancelled = true) + public void onWorldLoad(WorldLoadEvent event) { + for (ChunkCoord chunk : toRespawn.keySet()) { + if (!chunk.worldName.equals(event.getWorld().getName()) + || !event.getWorld().isChunkLoaded(chunk.x, chunk.z)) + continue; + respawnAllFromCoord(chunk); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldUnload(WorldUnloadEvent event) { + for (NPC npc : getAllNPCs()) { + if (npc == null || !npc.isSpawned() || !npc.getEntity().getWorld().equals(event.getWorld())) + continue; + boolean despawned = npc.despawn(DespawnReason.WORLD_UNLOAD); + if (event.isCancelled() || !despawned) { + for (ChunkCoord coord : toRespawn.keySet()) { + if (event.getWorld().getName().equals(coord.worldName)) { + respawnAllFromCoord(coord); + } + } + event.setCancelled(true); + return; + } + if (npc.isSpawned()) { + storeForRespawn(npc); + Messaging.debug("Despawned", npc.getId() + "due to world unload at", event.getWorld().getName()); + } + } + } + + private void respawnAllFromCoord(ChunkCoord coord) { + List ids = toRespawn.get(coord); + for (int i = 0; i < ids.size(); i++) { + NPC npc = ids.get(i); + boolean success = spawn(npc); + if (!success) { + if (Messaging.isDebugging()) { + Messaging.debug("Couldn't respawn id", npc.getId(), + "during chunk event at [" + coord.x + "," + coord.z + "]"); + } + continue; + } + ids.remove(i--); + if (Messaging.isDebugging()) { + Messaging.debug("Spawned id", npc.getId(), "due to chunk event at [" + coord.x + "," + coord.z + "]"); + } + } + } + + private boolean spawn(NPC npc) { + Location spawn = npc.getTrait(CurrentLocation.class).getLocation(); + if (spawn == null) { + if (Messaging.isDebugging()) { + Messaging.debug("Couldn't find a spawn location for despawned NPC id", npc.getId()); + } + return false; + } + return npc.spawn(spawn); + } + + private void storeForRespawn(NPC npc) { + toRespawn.put(toCoord(npc.getEntity().getLocation()), npc); + } + + private ChunkCoord toCoord(Chunk chunk) { + return new ChunkCoord(chunk); + } + + private ChunkCoord toCoord(Location loc) { + return new ChunkCoord(loc.getWorld().getName(), loc.getBlockX() >> 4, loc.getBlockZ() >> 4); + } + + private static class ChunkCoord { + private final String worldName; + private final int x; + private final int z; + + private ChunkCoord(Chunk chunk) { + this(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()); + } + + private ChunkCoord(String worldName, int x, int z) { + this.x = x; + this.z = z; + this.worldName = worldName; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ChunkCoord other = (ChunkCoord) obj; + if (worldName == null) { + if (other.worldName != null) { + return false; + } + } else if (!worldName.equals(other.worldName)) { + return false; + } + return x == other.x && z == other.z; + } + + @Override + public int hashCode() { + final int prime = 31; + return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z; + } + } +} diff --git a/main/java/net/citizensnpcs/Metrics.java b/main/java/net/citizensnpcs/Metrics.java new file mode 100644 index 000000000..4ba2114a7 --- /dev/null +++ b/main/java/net/citizensnpcs/Metrics.java @@ -0,0 +1,787 @@ +/* + * Copyright 2011-2013 Tyler Blair. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and contributors and should not be interpreted as representing official policies, + * either expressed or implied, of anybody else. + */ +package net.citizensnpcs; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitTask; + +public class Metrics { + /** + * The plugin configuration file + */ + private final YamlConfiguration configuration; + + /** + * The plugin configuration file + */ + private final File configurationFile; + + /** + * Debug mode + */ + private final boolean debug; + + /** + * All of the custom graphs to submit to metrics + */ + private final Set graphs = Collections.synchronizedSet(new HashSet()); + + /** + * Unique server id + */ + private final String guid; + + /** + * Lock for synchronization + */ + private final Object optOutLock = new Object(); + + /** + * The plugin this metrics submits for + */ + private final Plugin plugin; + + /** + * The scheduled task + */ + private volatile BukkitTask task = null; + + public Metrics(final Plugin plugin) throws IOException { + if (plugin == null) { + throw new IllegalArgumentException("Plugin cannot be null"); + } + + this.plugin = plugin; + + // load the config + configurationFile = getConfigFile(); + configuration = YamlConfiguration.loadConfiguration(configurationFile); + + // add some defaults + configuration.addDefault("opt-out", false); + configuration.addDefault("guid", UUID.randomUUID().toString()); + configuration.addDefault("debug", false); + + // Do we need to create the file? + if (configuration.get("guid", null) == null) { + configuration.options().header("http://mcstats.org").copyDefaults(true); + configuration.save(configurationFile); + } + + // Load the guid then + guid = configuration.getString("guid"); + debug = configuration.getBoolean("debug", false); + } + + public void addCustomData(Plotter plotter) { + Graph graph = new Graph(plotter.name); + graph.addPlotter(plotter); + addGraph(graph); + } + + /** + * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend + * + * @param graph + * The name of the graph + */ + public void addGraph(final Graph graph) { + if (graph == null) { + throw new IllegalArgumentException("Graph cannot be null"); + } + + graphs.add(graph); + } + + /** + * Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics + * website. Plotters can be added to the graph object returned. + * + * @param name + * The name of the graph + * @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given + */ + public Graph createGraph(final String name) { + if (name == null) { + throw new IllegalArgumentException("Graph name cannot be null"); + } + + // Construct the graph object + final Graph graph = new Graph(name); + + // Now we can add our graph + graphs.add(graph); + + // and return back + return graph; + } + + /** + * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. + * + * @throws java.io.IOException + */ + public void disable() throws IOException { + // This has to be synchronized or it can collide with the check in the + // task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set + // it. + if (!isOptOut()) { + configuration.set("opt-out", true); + configuration.save(configurationFile); + } + + // Disable Task, if it is running + if (task != null) { + task.cancel(); + task = null; + } + } + } + + /** + * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. + * + * @throws java.io.IOException + */ + public void enable() throws IOException { + // This has to be synchronized or it can collide with the check in the + // task. + synchronized (optOutLock) { + // Check if the server owner has already set opt-out, if not, set + // it. + if (isOptOut()) { + configuration.set("opt-out", false); + configuration.save(configurationFile); + } + + // Enable Task, if it is not running + if (task == null) { + start(); + } + } + } + + /** + * Gets the File object of the config file that should be used to store data such as the GUID and opt-out status + * + * @return the File object for the config file + */ + public File getConfigFile() { + // I believe the easiest way to get the base folder (e.g craftbukkit set + // via -P) for plugins to use + // is to abuse the plugin object we already have + // plugin.getDataFolder() => base/plugins/PluginA/ + // pluginsFolder => base/plugins/ + // The base is not necessarily relative to the startup directory. + File pluginsFolder = plugin.getDataFolder().getParentFile(); + + // return => base/plugins/PluginMetrics/config.yml + return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml"); + } + + /** + * Check if mineshafter is present. If it is, we need to bypass it to send POST requests + * + * @return true if mineshafter is installed on the server + */ + private boolean isMineshafterPresent() { + try { + Class.forName("mineshafter.MineServer"); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Has the server owner denied plugin metrics? + * + * @return true if metrics should be opted out of it + */ + public boolean isOptOut() { + synchronized (optOutLock) { + try { + // Reload the metrics file + configuration.load(getConfigFile()); + } catch (IOException ex) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } + return true; + } catch (InvalidConfigurationException ex) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage()); + } + return true; + } + return configuration.getBoolean("opt-out", false); + } + } + + /** + * Generic method that posts a plugin to the metrics website + */ + private void postPlugin(final boolean isPing) throws IOException { + // Server software specific section + PluginDescriptionFile description = plugin.getDescription(); + String pluginName = description.getName(); + boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if + // online mode + // is enabled + String pluginVersion = description.getVersion(); + String serverVersion = Bukkit.getVersion(); + int playersOnline = Bukkit.getServer().getOnlinePlayers().size(); + + // END server software specific section -- all code below does not use + // any code outside of this class / Java + + // Construct the post data + StringBuilder json = new StringBuilder(1024); + json.append('{'); + + // The plugin's description file containg all of the plugin data such as + // name, version, author, etc + appendJSONPair(json, "guid", guid); + appendJSONPair(json, "plugin_version", pluginVersion); + appendJSONPair(json, "server_version", serverVersion); + appendJSONPair(json, "players_online", Integer.toString(playersOnline)); + + // New data as of R6 + String osname = System.getProperty("os.name"); + String osarch = System.getProperty("os.arch"); + String osversion = System.getProperty("os.version"); + String java_version = System.getProperty("java.version"); + int coreCount = Runtime.getRuntime().availableProcessors(); + + // normalize os arch .. amd64 -> x86_64 + if (osarch.equals("amd64")) { + osarch = "x86_64"; + } + + appendJSONPair(json, "osname", osname); + appendJSONPair(json, "osarch", osarch); + appendJSONPair(json, "osversion", osversion); + appendJSONPair(json, "cores", Integer.toString(coreCount)); + appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0"); + appendJSONPair(json, "java_version", java_version); + + // If we're pinging, append it + if (isPing) { + appendJSONPair(json, "ping", "1"); + } + + if (graphs.size() > 0) { + synchronized (graphs) { + json.append(','); + json.append('"'); + json.append("graphs"); + json.append('"'); + json.append(':'); + json.append('{'); + + boolean firstGraph = true; + + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + Graph graph = iter.next(); + + StringBuilder graphJson = new StringBuilder(); + graphJson.append('{'); + + for (Plotter plotter : graph.getPlotters()) { + appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); + } + + graphJson.append('}'); + + if (!firstGraph) { + json.append(','); + } + + json.append(escapeJSON(graph.getName())); + json.append(':'); + json.append(graphJson); + + firstGraph = false; + } + + json.append('}'); + } + } + + // close json + json.append('}'); + + // Create the url + URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName))); + + // Connect to the website + URLConnection connection; + + // Mineshafter creates a socks proxy, so we can safely bypass it + // It does not reroute POST requests so we need to go around it + if (isMineshafterPresent()) { + connection = url.openConnection(Proxy.NO_PROXY); + } else { + connection = url.openConnection(); + } + + byte[] uncompressed = json.toString().getBytes(); + byte[] compressed = gzip(json.toString()); + + // Headers + connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + + connection.setDoOutput(true); + + if (debug) { + System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + + " compressed=" + compressed.length); + } + + // Write the data + OutputStream os = connection.getOutputStream(); + os.write(compressed); + os.flush(); + + // Now read the response + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String response = reader.readLine(); + + // close resources + os.close(); + reader.close(); + + if (response == null || response.startsWith("ERR") || response.startsWith("7")) { + if (response == null) { + response = "null"; + } else if (response.startsWith("7")) { + response = response.substring(response.startsWith("7,") ? 2 : 1); + } + + throw new IOException(response); + } else { + // Is this the first update this hour? + if (response.equals("1") || response.contains("This is your first update this hour")) { + synchronized (graphs) { + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { + final Graph graph = iter.next(); + + for (Plotter plotter : graph.getPlotters()) { + plotter.reset(); + } + } + } + } + } + } + + /** + * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the + * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200 + * ticks. + * + * @return True if statistics measuring is running, otherwise false. + */ + public boolean start() { + synchronized (optOutLock) { + // Did we opt out? + if (isOptOut()) { + return false; + } + + // Is metrics already running? + if (task != null) { + return true; + } + + // Begin hitting the server with glorious data + task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { + + private boolean firstPost = true; + + @Override + public void run() { + try { + // This has to be synchronized or it can collide with + // the disable method. + synchronized (optOutLock) { + // Disable Task, if it is running and the server + // owner decided to opt-out + if (isOptOut() && task != null) { + task.cancel(); + task = null; + // Tell all plotters to stop gathering + // information. + for (Graph graph : graphs) { + graph.onOptOut(); + } + } + } + + // We use the inverse of firstPost because if it is the + // first time we are posting, + // it is not a interval ping, so it evaluates to FALSE + // Each time thereafter it will evaluate to TRUE, i.e + // PING! + postPlugin(!firstPost); + + // After the first post we set firstPost to false + // Each post thereafter will be a ping + firstPost = false; + } catch (IOException e) { + if (debug) { + Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage()); + } + } + } + }, 0, PING_INTERVAL * 1200); + + return true; + } + } + + /** + * Represents a custom graph on the website + */ + public static class Graph { + + /** + * The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is + * rejected + */ + private final String name; + + /** + * The set of plotters that are contained within this graph + */ + private final Set plotters = new LinkedHashSet(); + + private Graph(final String name) { + this.name = name; + } + + /** + * Add a plotter to the graph, which will be used to plot entries + * + * @param plotter + * the plotter to add to the graph + */ + public void addPlotter(final Plotter plotter) { + plotters.add(plotter); + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Graph)) { + return false; + } + + final Graph graph = (Graph) object; + return graph.name.equals(name); + } + + /** + * Gets the graph's name + * + * @return the Graph's name + */ + public String getName() { + return name; + } + + /** + * Gets an unmodifiable set of the plotter objects in the graph + * + * @return an unmodifiable {@link java.util.Set} of the plotter objects + */ + public Set getPlotters() { + return Collections.unmodifiableSet(plotters); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. + */ + protected void onOptOut() { + } + + /** + * Remove a plotter from the graph + * + * @param plotter + * the plotter to remove from the graph + */ + public void removePlotter(final Plotter plotter) { + plotters.remove(plotter); + } + } + + /** + * Interface used to collect custom data for a plugin + */ + public static abstract class Plotter { + + /** + * The plot's name + */ + private final String name; + + /** + * Construct a plotter with the default plot name + */ + public Plotter() { + this("Default"); + } + + /** + * Construct a plotter with a specific plot name + * + * @param name + * the name of the plotter to use, which will show up on the website + */ + public Plotter(final String name) { + this.name = name; + } + + @Override + public boolean equals(final Object object) { + if (!(object instanceof Plotter)) { + return false; + } + + final Plotter plotter = (Plotter) object; + return plotter.name.equals(name) && plotter.getValue() == getValue(); + } + + /** + * Get the column name for the plotted point + * + * @return the plotted point's column name + */ + public String getColumnName() { + return name; + } + + /** + * Get the current value for the plotted point. Since this function defers to an external function it may or may + * not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called + * from any thread so care should be taken when accessing resources that need to be synchronized. + * + * @return the current value for the point to be plotted. + */ + public abstract int getValue(); + + @Override + public int hashCode() { + return getColumnName().hashCode(); + } + + /** + * Called after the website graphs have been updated + */ + public void reset() { + } + } + + /** + * Appends a json encoded key/value pair to the given string builder. + * + * @param json + * @param key + * @param value + * @throws UnsupportedEncodingException + */ + private static void appendJSONPair(StringBuilder json, String key, String value) + throws UnsupportedEncodingException { + boolean isValueNumeric = false; + + try { + if (value.equals("0") || !value.endsWith("0")) { + Double.parseDouble(value); + isValueNumeric = true; + } + } catch (NumberFormatException e) { + isValueNumeric = false; + } + + if (json.charAt(json.length() - 1) != '{') { + json.append(','); + } + + json.append(escapeJSON(key)); + json.append(':'); + + if (isValueNumeric) { + json.append(value); + } else { + json.append(escapeJSON(value)); + } + } + + /** + * Escape a string to create a valid JSON string + * + * @param text + * @return + */ + private static String escapeJSON(String text) { + StringBuilder builder = new StringBuilder(); + + builder.append('"'); + for (int index = 0; index < text.length(); index++) { + char chr = text.charAt(index); + + switch (chr) { + case '"': + case '\\': + builder.append('\\'); + builder.append(chr); + break; + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + default: + if (chr < ' ') { + String t = "000" + Integer.toHexString(chr); + builder.append("\\u" + t.substring(t.length() - 4)); + } else { + builder.append(chr); + } + break; + } + } + builder.append('"'); + + return builder.toString(); + } + + /** + * GZip compress a string of bytes + * + * @param input + * @return + */ + public static byte[] gzip(String input) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = null; + + try { + gzos = new GZIPOutputStream(baos); + gzos.write(input.getBytes("UTF-8")); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (gzos != null) + try { + gzos.close(); + } catch (IOException ignore) { + } + } + + return baos.toByteArray(); + } + + /** + * Encode text as UTF-8 + * + * @param text + * the text to encode + * @return the encoded text, as UTF-8 + */ + private static String urlEncode(final String text) throws UnsupportedEncodingException { + return URLEncoder.encode(text, "UTF-8"); + } + + /** + * The base url of the metrics domain + */ + private static final String BASE_URL = "http://report.mcstats.org"; + + /** + * Interval of time to ping (in minutes) + */ + private static final int PING_INTERVAL = 15; + + /** + * The url used to report a server's status + */ + private static final String REPORT_URL = "/plugin/%s"; + + /** + * The current revision number + */ + private final static int REVISION = 7; +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/NPCNeedsRespawnEvent.java b/main/java/net/citizensnpcs/NPCNeedsRespawnEvent.java new file mode 100644 index 000000000..b9e1264cc --- /dev/null +++ b/main/java/net/citizensnpcs/NPCNeedsRespawnEvent.java @@ -0,0 +1,31 @@ +package net.citizensnpcs; + +import net.citizensnpcs.api.event.NPCEvent; +import net.citizensnpcs.api.npc.NPC; + +import org.bukkit.Location; +import org.bukkit.event.HandlerList; + +public class NPCNeedsRespawnEvent extends NPCEvent { + private final Location spawn; + + public NPCNeedsRespawnEvent(NPC npc, Location at) { + super(npc); + this.spawn = at; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public Location getSpawnLocation() { + return spawn; + } + + private static final HandlerList handlers = new HandlerList(); + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/main/java/net/citizensnpcs/PaymentListener.java b/main/java/net/citizensnpcs/PaymentListener.java new file mode 100644 index 000000000..d237dabb9 --- /dev/null +++ b/main/java/net/citizensnpcs/PaymentListener.java @@ -0,0 +1,38 @@ +package net.citizensnpcs; + +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.event.PlayerCreateNPCEvent; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; + +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import com.google.common.base.Preconditions; + +public class PaymentListener implements Listener { + private final Economy provider; + + public PaymentListener(Economy provider) { + Preconditions.checkNotNull(provider, "provider cannot be null"); + this.provider = provider; + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerCreateNPC(PlayerCreateNPCEvent event) { + boolean hasAccount = provider.hasAccount(event.getCreator()); + if (!hasAccount || event.getCreator().hasPermission("citizens.npc.ignore-cost")) + return; + double cost = Setting.NPC_COST.asDouble(); + EconomyResponse response = provider.withdrawPlayer(event.getCreator(), cost); + if (!response.transactionSuccess()) { + event.setCancelled(true); + event.setCancelReason(response.errorMessage); + return; + } + String formattedCost = provider.format(cost); + Messaging.sendTr(event.getCreator(), Messages.MONEY_WITHDRAWN, formattedCost); + } +} diff --git a/main/java/net/citizensnpcs/Settings.java b/main/java/net/citizensnpcs/Settings.java new file mode 100644 index 000000000..9da3935d4 --- /dev/null +++ b/main/java/net/citizensnpcs/Settings.java @@ -0,0 +1,181 @@ +package net.citizensnpcs; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.Lists; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.util.DataKey; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.api.util.Storage; +import net.citizensnpcs.api.util.YamlStorage; + +public class Settings { + private final Storage config; + private final DataKey root; + + public Settings(File folder) { + config = new YamlStorage(new File(folder, "config.yml"), "Citizens Configuration"); + root = config.getKey(""); + + config.load(); + for (Setting setting : Setting.values()) { + if (!root.keyExists(setting.path)) { + setting.setAtKey(root); + } else + setting.loadFromKey(root); + } + updateMessagingSettings(); + + save(); + } + + public void reload() { + config.load(); + for (Setting setting : Setting.values()) { + if (root.keyExists(setting.path)) { + setting.loadFromKey(root); + } + } + updateMessagingSettings(); + save(); + } + + public void save() { + config.save(); + } + + private void updateMessagingSettings() { + File file = null; + if (!Setting.DEBUG_FILE.asString().isEmpty()) { + file = new File(CitizensAPI.getPlugin().getDataFolder(), Setting.DEBUG_FILE.asString()); + } + Messaging.configure(file, Setting.DEBUG_MODE.asBoolean(), Setting.MESSAGE_COLOUR.asString(), + Setting.HIGHLIGHT_COLOUR.asString()); + } + + public enum Setting { + AUTH_SERVER_URL("general.authlib.profile-url", "https://sessionserver.mojang.com/session/minecraft/profile/"), + CHAT_BYSTANDERS_HEAR_TARGETED_CHAT("npc.chat.options.bystanders-hear-targeted-chat", true), + CHAT_FORMAT("npc.chat.format.no-targets", "[]: "), + CHAT_FORMAT_TO_BYSTANDERS("npc.chat.format.with-target-to-bystanders", "[] -> []: "), + CHAT_FORMAT_TO_TARGET("npc.chat.format.to-target", "[] -> You: "), + CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS("npc.chat.format.with-targets-to-bystanders", + "[] -> []: "), + CHAT_MAX_NUMBER_OF_TARGETS("npc.chat.options.max-number-of-targets-to-show", 2), + CHAT_MULTIPLE_TARGETS_FORMAT("npc.chat.options.multiple-targets-format", + "|, | & | & others"), + CHAT_RANGE("npc.chat.options.range", 5), + CHECK_MINECRAFT_VERSION("advanced.check-minecraft-version", true), + DEBUG_FILE("general.debug-file", ""), + DEBUG_MODE("general.debug-mode", false), + DEBUG_PATHFINDING("general.debug-pathfinding", false), + DEFAULT_DISTANCE_MARGIN("npc.pathfinding.default-distance-margin", 2), + DEFAULT_LOOK_CLOSE("npc.default.look-close.enabled", false), + DEFAULT_LOOK_CLOSE_RANGE("npc.default.look-close.range", 5), + DEFAULT_NPC_LIMIT("npc.limits.default-limit", 10), + DEFAULT_PATHFINDER_UPDATE_PATH_RATE("npc.pathfinding.update-path-rate", 20), + DEFAULT_PATHFINDING_RANGE("npc.default.pathfinding.range", 25F), + DEFAULT_RANDOM_TALKER("npc.default.random-talker", true), + DEFAULT_REALISTIC_LOOKING("npc.default.realistic-looking", false), + DEFAULT_STATIONARY_TICKS("npc.default.stationary-ticks", -1), + DEFAULT_TALK_CLOSE("npc.default.talk-close.enabled", false), + DEFAULT_TALK_CLOSE_RANGE("npc.default.talk-close.range", 5), + DEFAULT_TEXT("npc.default.text.0", "Hi, I'm !") { + @Override + public void loadFromKey(DataKey root) { + List list = new ArrayList(); + for (DataKey key : root.getRelative("npc.default.text").getSubKeys()) + list.add(key.getString("")); + value = list; + } + }, + DISABLE_TABLIST("npc.tablist.disable", true), + HIGHLIGHT_COLOUR("general.color-scheme.message-highlight", ""), + KEEP_CHUNKS_LOADED("npc.chunks.always-keep-loaded", false), + LOCALE("general.translation.locale", ""), + MAX_NPC_LIMIT_CHECKS("npc.limits.max-permission-checks", 100), + MAX_NPC_SKIN_RETRIES("npc.skins.max-retries", -1), + MAX_PACKET_ENTRIES("npc.limits.max-packet-entries", 15), + MAX_SPEED("npc.limits.max-speed", 100), + MAX_TEXT_RANGE("npc.chat.options.max-text-range", 500), + MESSAGE_COLOUR("general.color-scheme.message", ""), + NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false), + NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75), + NPC_COST("economy.npc.cost", 100D), + NPC_SKIN_RETRY_DELAY("npc.skins.retry-delay", 120), + NPC_SKIN_ROTATION_UPDATE_DEGREES("npc.skins.rotation-update-degrees", 90f), + NPC_SKIN_USE_LATEST("npc.skins.use-latest", true), + NPC_SKIN_VIEW_DISTANCE("npc.skins.view-distance", 100D), + PACKET_UPDATE_DELAY("npc.packets.update-delay", 30), + QUICK_SELECT("npc.selection.quick-select", false), + REMOVE_PLAYERS_FROM_PLAYER_LIST("npc.player.remove-from-list", true), + SAVE_TASK_DELAY("storage.save-task.delay", 20 * 60 * 60), + SELECTION_ITEM("npc.selection.item", "280"), + SELECTION_MESSAGE("npc.selection.message", "You selected !"), + SERVER_OWNS_NPCS("npc.server-ownership", false), + STORAGE_FILE("storage.file", "saves.yml"), + STORAGE_TYPE("storage.type", "yaml"), + SUBPLUGIN_FOLDER("subplugins.folder", "plugins"), + TALK_CLOSE_MAXIMUM_COOLDOWN("npc.text.max-talk-cooldown", 5), + TALK_CLOSE_MINIMUM_COOLDOWN("npc.text.min-talk-cooldown", 10), + TALK_ITEM("npc.text.talk-item", "340"), + TELEPORT_DELAY("npc.teleport-delay", -1), + USE_BOAT_CONTROLS("npc.controllable.use-boat-controls", true), + USE_NEW_PATHFINDER("npc.pathfinding.use-new-finder", false), + USE_SCOREBOARD_TEAMS("npc.player-scoreboard-teams.enable", true); + + protected String path; + protected Object value; + + Setting(String path, Object value) { + this.path = path; + this.value = value; + } + + public boolean asBoolean() { + return (Boolean) value; + } + + public double asDouble() { + return ((Number) value).doubleValue(); + } + + public float asFloat() { + return ((Number) value).floatValue(); + } + + public int asInt() { + if (value instanceof String) { + return Integer.parseInt(value.toString()); + } + return ((Number) value).intValue(); + } + + @SuppressWarnings("unchecked") + public List asList() { + if (!(value instanceof List)) { + value = Lists.newArrayList(value); + } + return (List) value; + } + + public long asLong() { + return ((Number) value).longValue(); + } + + public String asString() { + return value.toString(); + } + + protected void loadFromKey(DataKey root) { + value = root.getRaw(path); + } + + protected void setAtKey(DataKey root) { + root.setRaw(path, value); + } + } +} diff --git a/main/java/net/citizensnpcs/commands/AdminCommands.java b/main/java/net/citizensnpcs/commands/AdminCommands.java new file mode 100644 index 000000000..0c77d068b --- /dev/null +++ b/main/java/net/citizensnpcs/commands/AdminCommands.java @@ -0,0 +1,67 @@ +package net.citizensnpcs.commands; + +import org.bukkit.command.CommandSender; + +import net.citizensnpcs.Citizens; +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.exception.NPCLoadException; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.StringHelper; + +@Requirements +public class AdminCommands { + private final Citizens plugin; + + public AdminCommands(Citizens plugin) { + this.plugin = plugin; + } + + @Command(aliases = { "citizens" }, desc = "Show basic plugin information", max = 0, permission = "citizens.admin") + public void citizens(CommandContext args, CommandSender sender, NPC npc) { + Messaging.send(sender, + " " + StringHelper.wrapHeader("Citizens v" + plugin.getDescription().getVersion())); + Messaging.send(sender, " <7>-- Written by fullwall and aPunch"); + Messaging.send(sender, " <7>-- Source Code: http://github.com/CitizensDev"); + Messaging.send(sender, " <7>-- Website: " + plugin.getDescription().getWebsite()); + } + + @Command( + aliases = { "citizens" }, + usage = "reload", + desc = "Reload Citizens", + modifiers = { "reload" }, + min = 1, + max = 1, + permission = "citizens.admin") + public void reload(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Messaging.sendTr(sender, Messages.CITIZENS_RELOADING); + try { + plugin.reload(); + Messaging.sendTr(sender, Messages.CITIZENS_RELOADED); + } catch (NPCLoadException ex) { + ex.printStackTrace(); + throw new CommandException(Messages.CITIZENS_RELOAD_ERROR); + } + } + + @Command( + aliases = { "citizens" }, + usage = "save (-a)", + desc = "Save NPCs", + help = Messages.COMMAND_SAVE_HELP, + modifiers = { "save" }, + min = 1, + max = 1, + flags = "a", + permission = "citizens.admin") + public void save(CommandContext args, CommandSender sender, NPC npc) { + Messaging.sendTr(sender, Messages.CITIZENS_SAVING); + plugin.storeNPCs(args); + Messaging.sendTr(sender, Messages.CITIZENS_SAVED); + } +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/commands/EditorCommands.java b/main/java/net/citizensnpcs/commands/EditorCommands.java new file mode 100644 index 000000000..35249afd1 --- /dev/null +++ b/main/java/net/citizensnpcs/commands/EditorCommands.java @@ -0,0 +1,70 @@ +package net.citizensnpcs.commands; + +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.editor.CopierEditor; +import net.citizensnpcs.editor.Editor; +import net.citizensnpcs.editor.EquipmentEditor; +import net.citizensnpcs.trait.text.Text; +import net.citizensnpcs.trait.waypoint.Waypoints; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@Requirements(selected = true, ownership = true) +public class EditorCommands { + @Command( + aliases = { "npc" }, + usage = "copier", + desc = "Toggle the NPC copier", + modifiers = { "copier" }, + min = 1, + max = 1, + permission = "citizens.npc.edit.copier") + public void copier(CommandContext args, Player player, NPC npc) { + Editor.enterOrLeave(player, new CopierEditor(player, npc)); + } + + @Command( + aliases = { "npc" }, + usage = "equip", + desc = "Toggle the equipment editor", + modifiers = { "equip" }, + min = 1, + max = 1, + permission = "citizens.npc.edit.equip") + public void equip(CommandContext args, Player player, NPC npc) { + Editor.enterOrLeave(player, new EquipmentEditor(player, npc)); + } + + @Command( + aliases = { "npc" }, + usage = "path", + desc = "Toggle the waypoint editor", + modifiers = { "path" }, + min = 1, + max = 1, + flags = "*", + permission = "citizens.npc.edit.path") + @Requirements(selected = true, ownership = true) + public void path(CommandContext args, CommandSender player, NPC npc) { + Editor editor = npc.getTrait(Waypoints.class).getEditor(player, args); + if (editor == null) + return; + Editor.enterOrLeave((Player) player, editor); + } + + @Command( + aliases = { "npc" }, + usage = "text", + desc = "Toggle the text editor", + modifiers = { "text" }, + min = 1, + max = 1, + permission = "citizens.npc.edit.text") + public void text(CommandContext args, Player player, NPC npc) { + Editor.enterOrLeave(player, npc.getTrait(Text.class).getEditor(player)); + } +} diff --git a/main/java/net/citizensnpcs/commands/NPCCommandSelector.java b/main/java/net/citizensnpcs/commands/NPCCommandSelector.java new file mode 100644 index 000000000..8cb2c11eb --- /dev/null +++ b/main/java/net/citizensnpcs/commands/NPCCommandSelector.java @@ -0,0 +1,121 @@ +package net.citizensnpcs.commands; + +import java.util.List; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.CommandMessages; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.command.exception.CommandUsageException; +import net.citizensnpcs.api.command.exception.ServerCommandException; +import net.citizensnpcs.api.command.exception.UnhandledCommandException; +import net.citizensnpcs.api.command.exception.WrappedCommandException; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.Util; + +import org.bukkit.command.CommandSender; +import org.bukkit.conversations.Conversable; +import org.bukkit.conversations.Conversation; +import org.bukkit.conversations.ConversationContext; +import org.bukkit.conversations.ConversationFactory; +import org.bukkit.conversations.NumericPrompt; +import org.bukkit.conversations.Prompt; + +import com.google.common.collect.Lists; + +public class NPCCommandSelector extends NumericPrompt { + private final Callback callback; + private final List choices; + + public NPCCommandSelector(Callback callback, List possible) { + this.callback = callback; + this.choices = possible; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, Number input) { + boolean found = false; + for (NPC npc : choices) { + if (input.intValue() == npc.getId()) { + found = true; + break; + } + } + CommandSender sender = (CommandSender) context.getForWhom(); + if (!found) { + Messaging.sendErrorTr(sender, Messages.SELECTION_PROMPT_INVALID_CHOICE, input); + return this; + } + NPC toSelect = CitizensAPI.getNPCRegistry().getById(input.intValue()); + try { + callback.run(toSelect); + } catch (ServerCommandException ex) { + Messaging.sendTr(sender, CommandMessages.MUST_BE_INGAME); + } catch (CommandUsageException ex) { + Messaging.sendError(sender, ex.getMessage()); + Messaging.sendError(sender, ex.getUsage()); + } catch (UnhandledCommandException ex) { + ex.printStackTrace(); + } catch (WrappedCommandException ex) { + ex.getCause().printStackTrace(); + } catch (CommandException ex) { + Messaging.sendError(sender, ex.getMessage()); + } catch (NumberFormatException ex) { + Messaging.sendErrorTr(sender, CommandMessages.INVALID_NUMBER); + } + return null; + } + + @Override + public String getPromptText(ConversationContext context) { + String text = Messaging.tr(Messages.SELECTION_PROMPT); + for (NPC npc : choices) { + text += "\n - " + npc.getId(); + } + return text; + } + + public static interface Callback { + public void run(NPC npc) throws CommandException; + } + + public static void start(Callback callback, Conversable player, List possible) { + final Conversation conversation = new ConversationFactory(CitizensAPI.getPlugin()).withLocalEcho(false) + .withEscapeSequence("exit").withModality(false) + .withFirstPrompt(new NPCCommandSelector(callback, possible)).buildConversation(player); + conversation.begin(); + } + + public static void startWithCallback(Callback callback, NPCRegistry npcRegistry, CommandSender sender, + CommandContext args, String raw) throws CommandException { + try { + int id = Integer.parseInt(raw); + callback.run(npcRegistry.getById(id)); + return; + } catch (NumberFormatException ex) { + String name = args.getString(1); + List possible = Lists.newArrayList(); + double range = -1; + if (args.hasValueFlag("r")) { + range = Math.abs(args.getFlagDouble("r")); + } + for (NPC test : npcRegistry) { + if (test.getName().equalsIgnoreCase(name)) { + if (range > 0 && test.isSpawned() && !Util.locationWithinRange(args.getSenderLocation(), + test.getEntity().getLocation(), range)) + continue; + possible.add(test); + } + } + if (possible.size() == 1) { + callback.run(possible.get(0)); + } else if (possible.size() > 1) { + NPCCommandSelector.start(callback, (Conversable) sender, possible); + return; + } + } + } +} diff --git a/main/java/net/citizensnpcs/commands/NPCCommands.java b/main/java/net/citizensnpcs/commands/NPCCommands.java new file mode 100644 index 000000000..a9b98da40 --- /dev/null +++ b/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -0,0 +1,1997 @@ +package net.citizensnpcs.commands; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.DyeColor; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarFlag; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Ageable; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Guardian; +import org.bukkit.entity.Horse.Color; +import org.bukkit.entity.Horse.Style; +import org.bukkit.entity.Horse.Variant; +import org.bukkit.entity.ItemFrame; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Ocelot; +import org.bukkit.entity.Player; +import org.bukkit.entity.Rabbit; +import org.bukkit.entity.Skeleton.SkeletonType; +import org.bukkit.entity.Villager.Profession; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import net.citizensnpcs.Citizens; +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.ai.speech.SpeechContext; +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.CommandMessages; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.command.exception.NoPermissionsException; +import net.citizensnpcs.api.command.exception.ServerCommandException; +import net.citizensnpcs.api.event.CommandSenderCreateNPCEvent; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.event.PlayerCreateNPCEvent; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.trait.Trait; +import net.citizensnpcs.api.trait.trait.Inventory; +import net.citizensnpcs.api.trait.trait.MobType; +import net.citizensnpcs.api.trait.trait.Owner; +import net.citizensnpcs.api.trait.trait.Spawned; +import net.citizensnpcs.api.trait.trait.Speech; +import net.citizensnpcs.api.util.Colorizer; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.api.util.Paginator; +import net.citizensnpcs.npc.EntityControllers; +import net.citizensnpcs.npc.NPCSelector; +import net.citizensnpcs.npc.Template; +import net.citizensnpcs.npc.skin.SkinnableEntity; +import net.citizensnpcs.trait.Age; +import net.citizensnpcs.trait.Anchors; +import net.citizensnpcs.trait.ArmorStandTrait; +import net.citizensnpcs.trait.BossBarTrait; +import net.citizensnpcs.trait.Controllable; +import net.citizensnpcs.trait.CurrentLocation; +import net.citizensnpcs.trait.Gravity; +import net.citizensnpcs.trait.HorseModifiers; +import net.citizensnpcs.trait.LookClose; +import net.citizensnpcs.trait.NPCSkeletonType; +import net.citizensnpcs.trait.OcelotModifiers; +import net.citizensnpcs.trait.Poses; +import net.citizensnpcs.trait.Powered; +import net.citizensnpcs.trait.RabbitType; +import net.citizensnpcs.trait.ScriptTrait; +import net.citizensnpcs.trait.SheepTrait; +import net.citizensnpcs.trait.SkinLayers; +import net.citizensnpcs.trait.SkinLayers.Layer; +import net.citizensnpcs.trait.SlimeSize; +import net.citizensnpcs.trait.VillagerProfession; +import net.citizensnpcs.trait.WitherTrait; +import net.citizensnpcs.trait.WolfModifiers; +import net.citizensnpcs.trait.ZombieModifier; +import net.citizensnpcs.util.Anchor; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.NMS; +import net.citizensnpcs.util.StringHelper; +import net.citizensnpcs.util.Util; + +@Requirements(selected = true, ownership = true) +public class NPCCommands { + private final NPCRegistry npcRegistry; + private final NPCSelector selector; + + public NPCCommands(Citizens plugin) { + npcRegistry = CitizensAPI.getNPCRegistry(); + selector = plugin.getNPCSelector(); + } + + @Command( + aliases = { "npc" }, + usage = "age [age] (-l)", + desc = "Set the age of a NPC", + help = Messages.COMMAND_AGE_HELP, + flags = "l", + modifiers = { "age" }, + min = 1, + max = 2, + permission = "citizens.npc.age") + public void age(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + if (!npc.isSpawned() || !(npc.getEntity() instanceof Ageable)) + throw new CommandException(Messages.MOBTYPE_CANNOT_BE_AGED); + Age trait = npc.getTrait(Age.class); + + boolean toggleLock = args.hasFlag('l'); + if (toggleLock) { + Messaging.sendTr(sender, trait.toggle() ? Messages.AGE_LOCKED : Messages.AGE_UNLOCKED); + } + if (args.argsLength() <= 1) { + if (!toggleLock) + trait.describe(sender); + return; + } + int age = 0; + try { + age = args.getInteger(1); + if (age > 0) { + throw new CommandException(Messages.INVALID_AGE); + } + Messaging.sendTr(sender, Messages.AGE_SET_NORMAL, npc.getName(), age); + } catch (NumberFormatException ex) { + if (args.getString(1).equalsIgnoreCase("baby")) { + age = -24000; + Messaging.sendTr(sender, Messages.AGE_SET_BABY, npc.getName()); + } else if (args.getString(1).equalsIgnoreCase("adult")) { + age = 0; + Messaging.sendTr(sender, Messages.AGE_SET_ADULT, npc.getName()); + } else + throw new CommandException(Messages.INVALID_AGE); + } + + trait.setAge(age); + } + + @Command( + aliases = { "npc" }, + usage = "anchor (--save [name]|--assume [name]|--remove [name]) (-a)(-c)", + desc = "Changes/Saves/Lists NPC's location anchor(s)", + flags = "ac", + modifiers = { "anchor" }, + min = 1, + max = 3, + permission = "citizens.npc.anchor") + public void anchor(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Anchors trait = npc.getTrait(Anchors.class); + if (args.hasValueFlag("save")) { + if (args.getFlag("save").isEmpty()) + throw new CommandException(Messages.INVALID_ANCHOR_NAME); + + if (args.getSenderLocation() == null) + throw new ServerCommandException(); + + if (args.hasFlag('c')) { + if (trait.addAnchor(args.getFlag("save"), args.getSenderTargetBlockLocation())) { + Messaging.sendTr(sender, Messages.ANCHOR_ADDED); + } else + throw new CommandException(Messages.ANCHOR_ALREADY_EXISTS, args.getFlag("save")); + } else { + if (trait.addAnchor(args.getFlag("save"), args.getSenderLocation())) { + Messaging.sendTr(sender, Messages.ANCHOR_ADDED); + } else + throw new CommandException(Messages.ANCHOR_ALREADY_EXISTS, args.getFlag("save")); + } + } else if (args.hasValueFlag("assume")) { + if (args.getFlag("assume").isEmpty()) + throw new CommandException(Messages.INVALID_ANCHOR_NAME); + + Anchor anchor = trait.getAnchor(args.getFlag("assume")); + if (anchor == null) + throw new CommandException(Messages.ANCHOR_MISSING, args.getFlag("assume")); + npc.teleport(anchor.getLocation(), TeleportCause.COMMAND); + } else if (args.hasValueFlag("remove")) { + if (args.getFlag("remove").isEmpty()) + throw new CommandException(Messages.INVALID_ANCHOR_NAME); + if (trait.removeAnchor(trait.getAnchor(args.getFlag("remove")))) + Messaging.sendTr(sender, Messages.ANCHOR_REMOVED); + else + throw new CommandException(Messages.ANCHOR_MISSING, args.getFlag("remove")); + } else if (!args.hasFlag('a')) { + Paginator paginator = new Paginator().header("Anchors"); + paginator.addLine("Key: ID Name World Location (X,Y,Z)"); + for (int i = 0; i < trait.getAnchors().size(); i++) { + if (trait.getAnchors().get(i).isLoaded()) { + String line = "" + i + " " + trait.getAnchors().get(i).getName() + " " + + trait.getAnchors().get(i).getLocation().getWorld().getName() + " " + + trait.getAnchors().get(i).getLocation().getBlockX() + ", " + + trait.getAnchors().get(i).getLocation().getBlockY() + ", " + + trait.getAnchors().get(i).getLocation().getBlockZ(); + paginator.addLine(line); + } else { + String[] parts = trait.getAnchors().get(i).getUnloadedValue(); + String line = "" + i + " " + trait.getAnchors().get(i).getName() + " " + parts[0] + + " " + parts[1] + ", " + parts[2] + ", " + parts[3] + " (unloaded)"; + paginator.addLine(line); + } + } + + int page = args.getInteger(1, 1); + if (!paginator.sendPage(sender, page)) + throw new CommandException(Messages.COMMAND_PAGE_MISSING); + } + + // Assume Player's position + if (!args.hasFlag('a')) + return; + if (sender instanceof ConsoleCommandSender) + throw new ServerCommandException(); + npc.teleport(args.getSenderLocation(), TeleportCause.COMMAND); + } + + @Command( + aliases = { "npc" }, + usage = "armorstand --visible [visible] --small [small] --gravity [gravity] --arms [arms] --baseplate [baseplate]", + desc = "Edit armorstand properties", + modifiers = { "armorstand" }, + min = 1, + max = 1) + @Requirements(selected = true, ownership = true, types = EntityType.ARMOR_STAND) + public void armorstand(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + ArmorStandTrait trait = npc.getTrait(ArmorStandTrait.class); + if (args.hasValueFlag("visible")) { + trait.setVisible(Boolean.valueOf(args.getFlag("visible"))); + } + if (args.hasValueFlag("small")) { + trait.setSmall(Boolean.valueOf(args.getFlag("small"))); + } + if (args.hasValueFlag("gravity")) { + trait.setGravity(Boolean.valueOf(args.getFlag("gravity"))); + } + if (args.hasValueFlag("arms")) { + trait.setHasArms(Boolean.valueOf(args.getFlag("arms"))); + } + if (args.hasValueFlag("baseplate")) { + trait.setHasBaseplate(Boolean.valueOf(args.getFlag("baseplate"))); + } + } + + @Command( + aliases = { "npc" }, + usage = "bossbar --color [color] --title [title] --visible [visible] --flags [flags]", + desc = "Edit bossbar properties", + modifiers = { "bossbar" }, + min = 1, + max = 1) + @Requirements(selected = true, ownership = true, types = { EntityType.WITHER, EntityType.ENDER_DRAGON }) + public void bossbar(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + BossBarTrait trait = npc.getTrait(BossBarTrait.class); + if (args.hasValueFlag("color")) { + BarColor color = Util.matchEnum(BarColor.values(), args.getFlag("color")); + trait.setColor(color); + } + if (args.hasValueFlag("title")) { + trait.setTitle(args.getFlag("title")); + } + if (args.hasValueFlag("visible")) { + trait.setVisible(Boolean.parseBoolean(args.getFlag("visible"))); + } + if (args.hasValueFlag("flags")) { + List flags = Lists.newArrayList(); + for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split(args.getFlag("flags"))) { + BarFlag flag = Util.matchEnum(BarFlag.values(), s); + if (flag != null) { + flags.add(flag); + } + } + trait.setFlags(flags); + } + } + + @Command( + aliases = { "npc" }, + usage = "collidable", + desc = "Toggles an NPC's collidability", + modifiers = { "collidable" }, + min = 1, + max = 1, + permission = "citizens.npc.collidable") + @Requirements(ownership = true, selected = true, types = { EntityType.PLAYER }) + public void collidable(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + npc.data().setPersistent(NPC.COLLIDABLE_METADATA, !npc.data().get(NPC.COLLIDABLE_METADATA, true)); + Messaging.sendTr(sender, + npc.data().get(NPC.COLLIDABLE_METADATA) ? Messages.COLLIDABLE_SET : Messages.COLLIDABLE_UNSET, + npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "controllable|control (-m(ount),-y,-n,-o)", + desc = "Toggles whether the NPC can be ridden and controlled", + modifiers = { "controllable", "control" }, + min = 1, + max = 1, + flags = "myno") + public void controllable(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + if ((npc.isSpawned() && !sender.hasPermission( + "citizens.npc.controllable." + npc.getEntity().getType().name().toLowerCase().replace("_", ""))) + || !sender.hasPermission("citizens.npc.controllable")) + throw new NoPermissionsException(); + if (!npc.hasTrait(Controllable.class)) { + npc.addTrait(new Controllable(false)); + } + Controllable trait = npc.getTrait(Controllable.class); + boolean enabled = trait.toggle(); + if (args.hasFlag('y')) { + enabled = trait.setEnabled(true); + } else if (args.hasFlag('n')) { + enabled = trait.setEnabled(false); + } + trait.setOwnerRequired(args.hasFlag('o')); + String key = enabled ? Messages.CONTROLLABLE_SET : Messages.CONTROLLABLE_REMOVED; + Messaging.sendTr(sender, key, npc.getName()); + if (enabled && args.hasFlag('m') && sender instanceof Player) { + trait.mount((Player) sender); + } + } + + @Command( + aliases = { "npc" }, + usage = "copy (--name newname)", + desc = "Copies an NPC", + modifiers = { "copy" }, + min = 1, + max = 1, + permission = "citizens.npc.copy") + public void copy(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String name = args.getFlag("name", npc.getFullName()); + NPC copy = npc.clone(); + if (!copy.getFullName().equals(name)) { + copy.setName(name); + } + + if (copy.isSpawned() && args.getSenderLocation() != null) { + Location location = args.getSenderLocation(); + location.getChunk().load(); + copy.teleport(location, TeleportCause.COMMAND); + copy.getTrait(CurrentLocation.class).setLocation(location); + } + + CommandSenderCreateNPCEvent event = sender instanceof Player ? new PlayerCreateNPCEvent((Player) sender, copy) + : new CommandSenderCreateNPCEvent(sender, copy); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + event.getNPC().destroy(); + String reason = "Couldn't create NPC."; + if (!event.getCancelReason().isEmpty()) + reason += " Reason: " + event.getCancelReason(); + throw new CommandException(reason); + } + + Messaging.sendTr(sender, Messages.NPC_COPIED, npc.getName()); + selector.select(sender, copy); + } + + @Command( + aliases = { "npc" }, + usage = "create [name] ((-b,u) --at (x:y:z:world) --type (type) --trait ('trait1, trait2...') --b (behaviours))", + desc = "Create a new NPC", + flags = "bu", + modifiers = { "create" }, + min = 2, + permission = "citizens.npc.create") + @Requirements + public void create(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String name = Colorizer.parseColors(args.getJoinedStrings(1).trim()); + + EntityType type = EntityType.PLAYER; + if (args.hasValueFlag("type")) { + String inputType = args.getFlag("type"); + type = Util.matchEntityType(inputType); + if (type == null) { + throw new CommandException(Messaging.tr(Messages.NPC_CREATE_INVALID_MOBTYPE, inputType)); + } else if (!EntityControllers.controllerExistsForType(type)) { + throw new CommandException(Messaging.tr(Messages.NPC_CREATE_MISSING_MOBTYPE, inputType)); + } + } + + int nameLength = type == EntityType.PLAYER ? 46 : 64; + if (name.length() > nameLength) { + Messaging.sendErrorTr(sender, Messages.NPC_NAME_TOO_LONG); + name = name.substring(0, nameLength); + } + if (name.length() == 0) + throw new CommandException(); + + if (!sender.hasPermission("citizens.npc.create.*") && !sender.hasPermission("citizens.npc.createall") + && !sender.hasPermission("citizens.npc.create." + type.name().toLowerCase().replace("_", ""))) + throw new NoPermissionsException(); + + npc = npcRegistry.createNPC(type, name); + String msg = "You created [[" + npc.getName() + "]]"; + + int age = 0; + if (args.hasFlag('b')) { + if (!Ageable.class.isAssignableFrom(type.getEntityClass())) + Messaging.sendErrorTr(sender, Messages.MOBTYPE_CANNOT_BE_AGED, + type.name().toLowerCase().replace("_", "-")); + else { + age = -24000; + msg += " as a baby"; + } + } + + // Initialize necessary traits + if (!Setting.SERVER_OWNS_NPCS.asBoolean()) { + npc.getTrait(Owner.class).setOwner(sender); + } + npc.getTrait(MobType.class).setType(type); + + Location spawnLoc = null; + if (sender instanceof Player) { + spawnLoc = args.getSenderLocation(); + } else if (sender instanceof BlockCommandSender) { + spawnLoc = args.getSenderLocation(); + } + CommandSenderCreateNPCEvent event = sender instanceof Player ? new PlayerCreateNPCEvent((Player) sender, npc) + : new CommandSenderCreateNPCEvent(sender, npc); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + npc.destroy(); + String reason = "Couldn't create NPC."; + if (!event.getCancelReason().isEmpty()) + reason += " Reason: " + event.getCancelReason(); + throw new CommandException(reason); + } + + if (args.hasValueFlag("at")) { + spawnLoc = CommandContext.parseLocation(args.getSenderLocation(), args.getFlag("at")); + } + + if (spawnLoc == null) { + npc.destroy(); + throw new CommandException(Messages.INVALID_SPAWN_LOCATION); + } + + if (!args.hasFlag('u')) { + npc.spawn(spawnLoc); + } + + if (args.hasValueFlag("trait")) { + Iterable parts = Splitter.on(',').trimResults().split(args.getFlag("trait")); + StringBuilder builder = new StringBuilder(); + for (String tr : parts) { + Trait trait = CitizensAPI.getTraitFactory().getTrait(tr); + if (trait == null) + continue; + npc.addTrait(trait); + builder.append(StringHelper.wrap(tr) + ", "); + } + if (builder.length() > 0) + builder.delete(builder.length() - 2, builder.length()); + msg += " with traits " + builder.toString(); + } + + if (args.hasValueFlag("template")) { + Iterable parts = Splitter.on(',').trimResults().split(args.getFlag("template")); + StringBuilder builder = new StringBuilder(); + for (String part : parts) { + Template template = Template.byName(part); + if (template == null) + continue; + template.apply(npc); + builder.append(StringHelper.wrap(part) + ", "); + } + if (builder.length() > 0) + builder.delete(builder.length() - 2, builder.length()); + msg += " with templates " + builder.toString(); + } + + // Set age after entity spawns + if (npc.getEntity() instanceof Ageable) { + npc.getTrait(Age.class).setAge(age); + } + selector.select(sender, npc); + Messaging.send(sender, msg + '.'); + } + + @Command( + aliases = { "npc" }, + usage = "despawn (id)", + desc = "Despawn a NPC", + modifiers = { "despawn" }, + min = 1, + max = 2, + permission = "citizens.npc.despawn") + @Requirements + public void despawn(final CommandContext args, final CommandSender sender, NPC npc) throws CommandException { + NPCCommandSelector.Callback callback = new NPCCommandSelector.Callback() { + @Override + public void run(NPC npc) throws CommandException { + if (npc == null) { + throw new CommandException(Messages.NO_NPC_WITH_ID_FOUND, args.getString(1)); + } + npc.getTrait(Spawned.class).setSpawned(false); + npc.despawn(DespawnReason.REMOVAL); + Messaging.sendTr(sender, Messages.NPC_DESPAWNED, npc.getName()); + } + }; + if (npc == null || args.argsLength() == 2) { + if (args.argsLength() < 2) { + throw new CommandException(Messages.COMMAND_MUST_HAVE_SELECTED); + } + NPCCommandSelector.startWithCallback(callback, npcRegistry, sender, args, args.getString(1)); + } else { + callback.run(npc); + } + } + + @Command( + aliases = { "npc" }, + usage = "flyable (true|false)", + desc = "Toggles or sets an NPC's flyable status", + modifiers = { "flyable" }, + min = 1, + max = 2, + permission = "citizens.npc.flyable") + @Requirements( + selected = true, + ownership = true, + excludedTypes = { EntityType.BAT, EntityType.BLAZE, EntityType.ENDER_DRAGON, EntityType.GHAST, + EntityType.WITHER }) + public void flyable(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + boolean flyable = args.argsLength() == 2 ? args.getString(1).equals("true") : !npc.isFlyable(); + npc.setFlyable(flyable); + flyable = npc.isFlyable(); // may not have applied, eg bats always + // flyable + Messaging.sendTr(sender, flyable ? Messages.FLYABLE_SET : Messages.FLYABLE_UNSET); + } + + @Command( + aliases = { "npc" }, + usage = "gamemode [gamemode]", + desc = "Changes the gamemode", + modifiers = { "gamemode" }, + min = 1, + max = 2, + permission = "citizens.npc.gravity") + @Requirements(selected = true, ownership = true, types = { EntityType.PLAYER }) + public void gamemode(CommandContext args, CommandSender sender, NPC npc) { + Player player = (Player) npc.getEntity(); + if (args.argsLength() == 1) { + Messaging.sendTr(sender, Messages.GAMEMODE_DESCRIBE, npc.getName(), + player.getGameMode().name().toLowerCase()); + return; + } + GameMode mode = null; + try { + int value = args.getInteger(1); + mode = GameMode.getByValue(value); + } catch (NumberFormatException ex) { + try { + mode = GameMode.valueOf(args.getString(1)); + } catch (IllegalArgumentException e) { + } + } + if (mode == null) { + Messaging.sendErrorTr(sender, Messages.GAMEMODE_INVALID, args.getString(1)); + return; + } + player.setGameMode(mode); + Messaging.sendTr(sender, Messages.GAMEMODE_SET, mode.name().toLowerCase()); + } + + @Command( + aliases = { "npc" }, + usage = "glowing --color [minecraft chat color]", + desc = "Toggles an NPC's glowing status", + modifiers = { "glowing" }, + min = 1, + max = 1, + permission = "citizens.npc.glowing") + @Requirements(selected = true, ownership = true) + public void glowing(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + if (args.hasValueFlag("color")) { + ChatColor chatColor = Util.matchEnum(ChatColor.values(), args.getFlag("color")); + if (!(npc.getEntity() instanceof Player)) + throw new CommandException(); + if (chatColor == null) { + npc.data().remove(NPC.GLOWING_COLOR_METADATA); + } else { + npc.data().setPersistent(NPC.GLOWING_COLOR_METADATA, chatColor.name()); + } + Messaging.sendTr(sender, Messages.GLOWING_COLOR_SET, npc.getName(), + chatColor == null ? ChatColor.WHITE + "white" : chatColor + Util.prettyEnum(chatColor)); + return; + } + npc.data().setPersistent(NPC.GLOWING_METADATA, !npc.data().get(NPC.GLOWING_METADATA, false)); + boolean glowing = npc.data().get(NPC.GLOWING_METADATA); + Messaging.sendTr(sender, glowing ? Messages.GLOWING_SET : Messages.GLOWING_UNSET, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "gravity", + desc = "Toggles gravity", + modifiers = { "gravity" }, + min = 1, + max = 1, + permission = "citizens.npc.gravity") + public void gravity(CommandContext args, CommandSender sender, NPC npc) { + boolean enabled = npc.getTrait(Gravity.class).toggle(); + String key = !enabled ? Messages.GRAVITY_ENABLED : Messages.GRAVITY_DISABLED; + Messaging.sendTr(sender, key, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "guardian --elder [true|false]", + desc = "Changes guardian modifiers", + modifiers = { "guardian" }, + min = 1, + max = 2, + permission = "citizens.npc.guardian") + @Requirements(selected = true, ownership = true, types = { EntityType.GUARDIAN }) + public void guardian(CommandContext args, CommandSender sender, NPC npc) { + Guardian guardian = (Guardian) npc.getEntity(); + if (args.hasValueFlag("elder")) { + guardian.setElder(args.getFlag("elder", "false").equals("true") ? true : false); + Messaging.sendTr(sender, guardian.isElder() ? Messages.ELDER_SET : Messages.ELDER_UNSET, npc.getName()); + } + } + + @Command( + aliases = { "npc" }, + usage = "horse (--color color) (--type type) (--style style) (-cb)", + desc = "Sets horse modifiers", + help = "Use the -c flag to make the horse have a chest, or the -b flag to stop them from having a chest.", + modifiers = { "horse" }, + min = 1, + max = 1, + flags = "cb", + permission = "citizens.npc.horse") + @Requirements(selected = true, ownership = true, types = { EntityType.HORSE }) + public void horse(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + HorseModifiers horse = npc.getTrait(HorseModifiers.class); + String output = ""; + if (args.hasFlag('c')) { + horse.setCarryingChest(true); + output += Messaging.tr(Messages.HORSE_CHEST_SET) + " "; + } else if (args.hasFlag('b')) { + horse.setCarryingChest(false); + output += Messaging.tr(Messages.HORSE_CHEST_UNSET) + " "; + } + if (args.hasValueFlag("color") || args.hasValueFlag("colour")) { + String colorRaw = args.getFlag("color", args.getFlag("colour")); + Color color = Util.matchEnum(Color.values(), colorRaw); + if (color == null) { + String valid = Util.listValuesPretty(Color.values()); + throw new CommandException(Messages.INVALID_HORSE_COLOR, valid); + } + horse.setColor(color); + output += Messaging.tr(Messages.HORSE_COLOR_SET, Util.prettyEnum(color)); + } + if (args.hasValueFlag("type")) { + Variant variant = Util.matchEnum(Variant.values(), args.getFlag("type")); + if (variant == null) { + String valid = Util.listValuesPretty(Variant.values()); + throw new CommandException(Messages.INVALID_HORSE_VARIANT, valid); + } + horse.setType(variant); + output += Messaging.tr(Messages.HORSE_TYPE_SET, Util.prettyEnum(variant)); + } + if (args.hasValueFlag("style")) { + Style style = Util.matchEnum(Style.values(), args.getFlag("style")); + if (style == null) { + String valid = Util.listValuesPretty(Style.values()); + throw new CommandException(Messages.INVALID_HORSE_STYLE, valid); + } + horse.setStyle(style); + output += Messaging.tr(Messages.HORSE_STYLE_SET, Util.prettyEnum(style)); + } + if (output.isEmpty()) { + Messaging.sendTr(sender, Messages.HORSE_DESCRIBE, Util.prettyEnum(horse.getColor()), + Util.prettyEnum(horse.getType()), Util.prettyEnum(horse.getStyle())); + } else { + sender.sendMessage(output); + } + } + + @Command( + aliases = { "npc" }, + usage = "id", + desc = "Sends the selected NPC's ID to the sender", + modifiers = { "id" }, + min = 1, + max = 1, + permission = "citizens.npc.id") + public void id(CommandContext args, CommandSender sender, NPC npc) { + Messaging.send(sender, npc.getId()); + } + + @Command( + aliases = { "npc" }, + usage = "inventory", + desc = "Show's an NPC's inventory", + modifiers = { "inventory" }, + min = 1, + max = 1, + permission = "citizens.npc.inventory") + public void inventory(CommandContext args, CommandSender sender, NPC npc) { + npc.getTrait(Inventory.class).openInventory((Player) sender); + } + + @Command( + aliases = { "npc" }, + usage = "item [item] (data)", + desc = "Sets the NPC's item", + modifiers = { "item", }, + min = 2, + max = 3, + flags = "", + permission = "citizens.npc.item") + @Requirements( + selected = true, + ownership = true, + types = { EntityType.DROPPED_ITEM, EntityType.ITEM_FRAME, EntityType.FALLING_BLOCK }) + public void item(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Material mat = Material.matchMaterial(args.getString(1)); + if (mat == null) + throw new CommandException(Messages.UNKNOWN_MATERIAL); + int data = args.getInteger(2, 0); + npc.data().setPersistent(NPC.ITEM_ID_METADATA, mat.name()); + npc.data().setPersistent(NPC.ITEM_DATA_METADATA, data); + switch (npc.getEntity().getType()) { + case DROPPED_ITEM: + ((org.bukkit.entity.Item) npc.getEntity()).getItemStack().setType(mat); + break; + case ITEM_FRAME: + ((ItemFrame) npc.getEntity()).getItem().setType(mat); + break; + default: + break; + } + if (npc.isSpawned()) { + npc.despawn(); + npc.spawn(npc.getStoredLocation()); + } + Messaging.sendTr(sender, Messages.ITEM_SET, Util.prettyEnum(mat)); + } + + @Command( + aliases = { "npc" }, + usage = "leashable", + desc = "Toggles leashability", + modifiers = { "leashable" }, + min = 1, + max = 1, + flags = "t", + permission = "citizens.npc.leashable") + public void leashable(CommandContext args, CommandSender sender, NPC npc) { + boolean vulnerable = !npc.data().get(NPC.LEASH_PROTECTED_METADATA, true); + if (args.hasFlag('t')) { + npc.data().set(NPC.LEASH_PROTECTED_METADATA, vulnerable); + } else { + npc.data().setPersistent(NPC.LEASH_PROTECTED_METADATA, vulnerable); + } + String key = vulnerable ? Messages.LEASHABLE_STOPPED : Messages.LEASHABLE_SET; + Messaging.sendTr(sender, key, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "list (page) ((-a) --owner (owner) --type (type) --char (char) --registry (name))", + desc = "List NPCs", + flags = "a", + modifiers = { "list" }, + min = 1, + max = 2, + permission = "citizens.npc.list") + @Requirements + public void list(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + NPCRegistry source = args.hasValueFlag("registry") ? CitizensAPI.getNamedNPCRegistry(args.getFlag("registry")) + : npcRegistry; + if (source == null) + throw new CommandException(); + List npcs = new ArrayList(); + + if (args.hasFlag('a')) { + for (NPC add : source.sorted()) { + npcs.add(add); + } + } else if (args.getValueFlags().size() == 0 && sender instanceof Player) { + for (NPC add : source.sorted()) { + if (!npcs.contains(add) && add.getTrait(Owner.class).isOwnedBy(sender)) { + npcs.add(add); + } + } + } else { + if (args.hasValueFlag("owner")) { + String name = args.getFlag("owner"); + for (NPC add : source.sorted()) { + if (!npcs.contains(add) && add.getTrait(Owner.class).isOwnedBy(name)) { + npcs.add(add); + } + } + } + + if (args.hasValueFlag("type")) { + EntityType type = Util.matchEntityType(args.getFlag("type")); + + if (type == null) + throw new CommandException(Messages.COMMAND_INVALID_MOBTYPE, type); + + for (NPC add : source) { + if (!npcs.contains(add) && add.getTrait(MobType.class).getType() == type) + npcs.add(add); + } + } + } + + Paginator paginator = new Paginator().header("NPCs"); + paginator.addLine("Key: ID Name"); + for (int i = 0; i < npcs.size(); i += 2) { + String line = "" + npcs.get(i).getId() + " " + npcs.get(i).getName(); + if (npcs.size() >= i + 2) + line += " " + "" + npcs.get(i + 1).getId() + " " + npcs.get(i + 1).getName(); + paginator.addLine(line); + } + + int page = args.getInteger(1, 1); + if (!paginator.sendPage(sender, page)) + throw new CommandException(Messages.COMMAND_PAGE_MISSING); + } + + @Command( + aliases = { "npc" }, + usage = "lookclose", + desc = "Toggle whether a NPC will look when a player is near", + modifiers = { "lookclose", "look", "rotate" }, + min = 1, + max = 1, + permission = "citizens.npc.lookclose") + public void lookClose(CommandContext args, CommandSender sender, NPC npc) { + Messaging.sendTr(sender, + npc.getTrait(LookClose.class).toggle() ? Messages.LOOKCLOSE_SET : Messages.LOOKCLOSE_STOPPED, + npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "minecart (--item item_name(:data)) (--offset offset)", + desc = "Sets minecart item", + modifiers = { "minecart" }, + min = 1, + max = 1, + flags = "", + permission = "citizens.npc.minecart") + @Requirements( + selected = true, + ownership = true, + types = { EntityType.MINECART, EntityType.MINECART_CHEST, EntityType.MINECART_COMMAND, + EntityType.MINECART_FURNACE, EntityType.MINECART_HOPPER, EntityType.MINECART_MOB_SPAWNER, + EntityType.MINECART_TNT }) + public void minecart(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + if (args.hasValueFlag("item")) { + String raw = args.getFlag("item"); + int data = 0; + if (raw.contains(":")) { + int dataIndex = raw.indexOf(':'); + data = Integer.parseInt(raw.substring(dataIndex + 1)); + raw = raw.substring(0, dataIndex); + } + Material material = Material.matchMaterial(raw); + if (material == null) + throw new CommandException(); + npc.data().setPersistent(NPC.MINECART_ITEM_METADATA, material.name()); + npc.data().setPersistent(NPC.MINECART_ITEM_DATA_METADATA, data); + } + if (args.hasValueFlag("offset")) { + npc.data().setPersistent(NPC.MINECART_OFFSET_METADATA, args.getFlagInteger("offset")); + } + + Messaging.sendTr(sender, Messages.MINECART_SET, npc.data().get(NPC.MINECART_ITEM_METADATA, ""), + npc.data().get(NPC.MINECART_ITEM_DATA_METADATA, 0), npc.data().get(NPC.MINECART_OFFSET_METADATA, 0)); + } + + @Command( + aliases = { "npc" }, + usage = "mount (--onnpc )", + desc = "Mounts a controllable NPC", + modifiers = { "mount" }, + min = 1, + max = 1, + permission = "citizens.npc.controllable") + public void mount(CommandContext args, Player player, NPC npc) throws CommandException { + if (args.hasValueFlag("onnpc")) { + NPC mount; + try { + UUID uuid = UUID.fromString(args.getFlag("onnpc")); + mount = CitizensAPI.getNPCRegistry().getByUniqueId(uuid); + } catch (IllegalArgumentException ex) { + mount = CitizensAPI.getNPCRegistry().getById(args.getFlagInteger("onnpc")); + } + if (mount == null || !mount.isSpawned()) { + throw new CommandException(Messaging.tr(Messages.MOUNT_NPC_MUST_BE_SPAWNED, args.getFlag("onnpc"))); + } + if (mount.equals(npc)) { + throw new CommandException(); + } + NMS.mount(mount.getEntity(), npc.getEntity()); + return; + } + boolean enabled = npc.hasTrait(Controllable.class) && npc.getTrait(Controllable.class).isEnabled(); + if (!enabled) { + Messaging.sendTr(player, Messages.NPC_NOT_CONTROLLABLE, npc.getName()); + return; + } + boolean success = npc.getTrait(Controllable.class).mount(player); + if (!success) { + Messaging.sendTr(player, Messages.FAILED_TO_MOUNT_NPC, npc.getName()); + } + } + + @Command( + aliases = { "npc" }, + usage = "moveto x:y:z:world | x y z world", + desc = "Teleports a NPC to a given location", + modifiers = "moveto", + min = 1, + permission = "citizens.npc.moveto") + public void moveto(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + // Spawn the NPC if it isn't spawned to prevent NPEs + if (!npc.isSpawned()) { + npc.spawn(npc.getTrait(CurrentLocation.class).getLocation()); + } + if (!npc.isSpawned()) { + throw new CommandException("NPC could not be spawned."); + } + Location current = npc.getEntity().getLocation(); + Location to; + if (args.argsLength() > 1) { + String[] parts = Iterables.toArray(Splitter.on(':').split(args.getJoinedStrings(1, ':')), String.class); + if (parts.length != 4 && parts.length != 3) + throw new CommandException(Messages.MOVETO_FORMAT); + double x = Double.parseDouble(parts[0]); + double y = Double.parseDouble(parts[1]); + double z = Double.parseDouble(parts[2]); + World world = parts.length == 4 ? Bukkit.getWorld(parts[3]) : current.getWorld(); + if (world == null) + throw new CommandException(Messages.WORLD_NOT_FOUND); + to = new Location(world, x, y, z, current.getYaw(), current.getPitch()); + } else { + to = current.clone(); + if (args.hasValueFlag("x")) + to.setX(args.getFlagDouble("x")); + if (args.hasValueFlag("y")) + to.setY(args.getFlagDouble("y")); + if (args.hasValueFlag("z")) + to.setZ(args.getFlagDouble("z")); + if (args.hasValueFlag("yaw")) + to.setYaw((float) args.getFlagDouble("yaw")); + if (args.hasValueFlag("pitch")) + to.setPitch((float) args.getFlagDouble("pitch")); + if (args.hasValueFlag("world")) { + World world = Bukkit.getWorld(args.getFlag("world")); + if (world == null) + throw new CommandException(Messages.WORLD_NOT_FOUND); + to.setWorld(world); + } + } + + npc.teleport(to, TeleportCause.COMMAND); + Messaging.sendTr(sender, Messages.MOVETO_TELEPORTED, npc.getName(), to); + } + + @Command( + aliases = { "npc" }, + modifiers = { "name" }, + usage = "name", + desc = "Toggle nameplate visibility", + min = 1, + max = 1, + permission = "citizens.npc.name") + @Requirements(selected = true, ownership = true, livingEntity = true) + public void name(CommandContext args, CommandSender sender, NPC npc) { + LivingEntity entity = (LivingEntity) npc.getEntity(); + entity.setCustomNameVisible(!entity.isCustomNameVisible()); + npc.data().setPersistent(NPC.NAMEPLATE_VISIBLE_METADATA, entity.isCustomNameVisible()); + Messaging.sendTr(sender, Messages.NAMEPLATE_VISIBILITY_TOGGLED); + } + + @Command(aliases = { "npc" }, desc = "Show basic NPC information", max = 0, permission = "citizens.npc.info") + public void npc(CommandContext args, CommandSender sender, final NPC npc) { + Messaging.send(sender, StringHelper.wrapHeader(npc.getName())); + Messaging.send(sender, " ID: " + npc.getId()); + Messaging.send(sender, " Type: " + npc.getTrait(MobType.class).getType()); + if (npc.isSpawned()) { + Location loc = npc.getEntity().getLocation(); + String format = " Spawned at %d, %d, %d in world %s"; + Messaging.send(sender, + String.format(format, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ(), loc.getWorld().getName())); + } + Messaging.send(sender, " Traits"); + for (Trait trait : npc.getTraits()) { + if (CitizensAPI.getTraitFactory().isInternalTrait(trait)) + continue; + String message = " - " + trait.getName(); + Messaging.send(sender, message); + } + } + + @Command( + aliases = { "npc" }, + usage = "ocelot (--type type) (-s(itting), -n(ot sitting))", + desc = "Set the ocelot type of an NPC and whether it is sitting", + modifiers = { "ocelot" }, + min = 1, + max = 1, + flags = "sn", + permission = "citizens.npc.ocelot") + @Requirements(selected = true, ownership = true, types = { EntityType.OCELOT }) + public void ocelot(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + OcelotModifiers trait = npc.getTrait(OcelotModifiers.class); + if (args.hasFlag('s')) { + trait.setSitting(true); + } else if (args.hasFlag('n')) { + trait.setSitting(false); + } + if (args.hasValueFlag("type")) { + Ocelot.Type type = Util.matchEnum(Ocelot.Type.values(), args.getFlag("type")); + if (type == null) { + String valid = Util.listValuesPretty(Ocelot.Type.values()); + throw new CommandException(Messages.INVALID_OCELOT_TYPE, valid); + } + trait.setType(type); + } + } + + @Command( + aliases = { "npc" }, + usage = "owner [name]", + desc = "Set the owner of an NPC", + modifiers = { "owner" }, + min = 1, + max = 2, + permission = "citizens.npc.owner") + public void owner(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Owner ownerTrait = npc.getTrait(Owner.class); + if (args.argsLength() == 1) { + Messaging.sendTr(sender, Messages.NPC_OWNER, npc.getName(), ownerTrait.getOwner()); + return; + } + String name = args.getString(1); + if (ownerTrait.isOwnedBy(name)) + throw new CommandException(Messages.ALREADY_OWNER, name, npc.getName()); + ownerTrait.setOwner(name); + boolean serverOwner = name.equalsIgnoreCase(Owner.SERVER); + Messaging.sendTr(sender, serverOwner ? Messages.OWNER_SET_SERVER : Messages.OWNER_SET, npc.getName(), name); + } + + @Command( + aliases = { "npc" }, + usage = "passive (--set [true|false])", + desc = "Sets whether an NPC damages other entities or not", + modifiers = { "passive" }, + min = 1, + max = 1, + permission = "citizens.npc.passive") + public void passive(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + boolean passive = args.hasValueFlag("set") ? Boolean.parseBoolean(args.getFlag("set")) + : npc.data().get(NPC.DAMAGE_OTHERS_METADATA, true); + npc.data().setPersistent(NPC.DAMAGE_OTHERS_METADATA, !passive); + Messaging.sendTr(sender, passive ? Messages.PASSIVE_SET : Messages.PASSIVE_UNSET, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "pathopt --avoid-water|aw [true|false] --stationary-ticks [ticks] --attack-range [range] --distance-margin [margin]", + desc = "Sets an NPC's pathfinding options", + modifiers = { "pathopt", "po", "patho" }, + min = 1, + max = 1, + permission = "citizens.npc.pathfindingoptions") + public void pathfindingOptions(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + boolean found = false; + if (args.hasValueFlag("avoid-water") || args.hasValueFlag("aw")) { + String raw = args.getFlag("avoid-water", args.getFlag("aw")); + boolean avoid = Boolean.parseBoolean(raw); + npc.getNavigator().getDefaultParameters().avoidWater(avoid); + Messaging.sendTr(sender, avoid ? Messages.PATHFINDING_OPTIONS_AVOID_WATER_SET + : Messages.PATHFINDING_OPTIONS_AVOID_WATER_UNSET, npc.getName()); + found = true; + } + if (args.hasValueFlag("stationary-ticks")) { + int ticks = Integer.parseInt(args.getFlag("stationary-ticks")); + if (ticks < 0) + throw new CommandException(); + npc.getNavigator().getDefaultParameters().stationaryTicks(ticks); + Messaging.sendTr(sender, Messages.PATHFINDING_OPTIONS_STATIONARY_TICKS_SET, npc.getName(), ticks); + found = true; + } + if (args.hasValueFlag("distance-margin")) { + double distance = Double.parseDouble(args.getFlag("distance-margin")); + if (distance < 0) + throw new CommandException(); + npc.getNavigator().getDefaultParameters().distanceMargin(Math.pow(distance, 2)); + Messaging.sendTr(sender, Messages.PATHFINDING_OPTIONS_DISTANCE_MARGIN_SET, npc.getName(), distance); + found = true; + } + if (args.hasValueFlag("attack-range")) { + double range = Double.parseDouble(args.getFlag("attack-range")); + if (range < 0) + throw new CommandException(); + npc.getNavigator().getDefaultParameters().attackRange(range); + Messaging.sendTr(sender, Messages.PATHFINDING_OPTIONS_ATTACK_RANGE_SET, npc.getName(), range); + found = true; + } + if (!found) { + throw new CommandException(); + } + } + + @Command( + aliases = { "npc" }, + usage = "pathrange [range]", + desc = "Sets an NPC's pathfinding range", + modifiers = { "pathrange", "pathfindingrange", "prange" }, + min = 2, + max = 2, + permission = "citizens.npc.pathfindingrange") + public void pathfindingRange(CommandContext args, CommandSender sender, NPC npc) { + double range = Math.max(1, args.getDouble(1)); + npc.getNavigator().getDefaultParameters().range((float) range); + Messaging.sendTr(sender, Messages.PATHFINDING_RANGE_SET, range); + } + + @Command( + aliases = { "npc" }, + usage = "playerlist (-a,r)", + desc = "Sets whether the NPC is put in the playerlist", + modifiers = { "playerlist" }, + min = 1, + max = 1, + flags = "ar", + permission = "citizens.npc.playerlist") + @Requirements(selected = true, ownership = true, types = EntityType.PLAYER) + public void playerlist(CommandContext args, CommandSender sender, NPC npc) { + boolean remove = !npc.data().get("removefromplayerlist", Setting.REMOVE_PLAYERS_FROM_PLAYER_LIST.asBoolean()); + if (args.hasFlag('a')) { + remove = false; + } else if (args.hasFlag('r')) { + remove = true; + } + npc.data().setPersistent("removefromplayerlist", remove); + if (npc.isSpawned()) { + npc.despawn(DespawnReason.PENDING_RESPAWN); + npc.spawn(npc.getTrait(CurrentLocation.class).getLocation()); + NMS.addOrRemoveFromPlayerList(npc.getEntity(), remove); + } + Messaging.sendTr(sender, remove ? Messages.REMOVED_FROM_PLAYERLIST : Messages.ADDED_TO_PLAYERLIST, + npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "pose (--save [name]|--assume [name]|--remove [name]) (-a)", + desc = "Changes/Saves/Lists NPC's head pose(s)", + flags = "a", + modifiers = { "pose" }, + min = 1, + max = 2, + permission = "citizens.npc.pose") + public void pose(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Poses trait = npc.getTrait(Poses.class); + if (args.hasValueFlag("save")) { + if (args.getFlag("save").isEmpty()) + throw new CommandException(Messages.INVALID_POSE_NAME); + + if (args.getSenderLocation() == null) + throw new ServerCommandException(); + + if (trait.addPose(args.getFlag("save"), args.getSenderLocation())) { + Messaging.sendTr(sender, Messages.POSE_ADDED); + } else + throw new CommandException(Messages.POSE_ALREADY_EXISTS, args.getFlag("save")); + } else if (args.hasValueFlag("assume")) { + String pose = args.getFlag("assume"); + if (pose.isEmpty()) + throw new CommandException(Messages.INVALID_POSE_NAME); + + if (!trait.hasPose(pose)) + throw new CommandException(Messages.POSE_MISSING, pose); + trait.assumePose(pose); + } else if (args.hasValueFlag("remove")) { + if (args.getFlag("remove").isEmpty()) + throw new CommandException(Messages.INVALID_POSE_NAME); + if (trait.removePose(args.getFlag("remove"))) { + Messaging.sendTr(sender, Messages.POSE_REMOVED); + } else + throw new CommandException(Messages.POSE_MISSING, args.getFlag("remove")); + } else if (!args.hasFlag('a')) { + trait.describe(sender, args.getInteger(1, 1)); + } + + // Assume Player's pose + if (!args.hasFlag('a')) + return; + if (args.getSenderLocation() == null) + throw new ServerCommandException(); + Location location = args.getSenderLocation(); + trait.assumePose(location); + } + + @Command( + aliases = { "npc" }, + usage = "power", + desc = "Toggle a creeper NPC as powered", + modifiers = { "power" }, + min = 1, + max = 1, + permission = "citizens.npc.power") + @Requirements(selected = true, ownership = true, types = { EntityType.CREEPER }) + public void power(CommandContext args, CommandSender sender, NPC npc) { + Messaging.sendTr(sender, + npc.getTrait(Powered.class).toggle() ? Messages.POWERED_SET : Messages.POWERED_STOPPED); + } + + @Command( + aliases = { "npc" }, + usage = "profession|prof [profession]", + desc = "Set a NPC's profession", + modifiers = { "profession", "prof" }, + min = 2, + max = 2, + permission = "citizens.npc.profession") + @Requirements(selected = true, ownership = true, types = { EntityType.VILLAGER }) + public void profession(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String profession = args.getString(1); + Profession parsed = Util.matchEnum(Profession.values(), profession.toUpperCase()); + if (parsed == null) { + throw new CommandException(Messages.INVALID_PROFESSION, args.getString(1), + StringUtils.join(Profession.values(), ",")); + } + npc.getTrait(VillagerProfession.class).setProfession(parsed); + Messaging.sendTr(sender, Messages.PROFESSION_SET, npc.getName(), profession); + } + + @Command( + aliases = { "npc" }, + usage = "rabbittype [type]", + desc = "Set the Type of a Rabbit NPC", + modifiers = { "rabbittype", "rbtype" }, + min = 2, + permission = "citizens.npc.rabbittype") + @Requirements(selected = true, ownership = true, types = { EntityType.RABBIT }) + public void rabbitType(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Rabbit.Type type; + try { + type = Rabbit.Type.valueOf(args.getString(1).toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new CommandException(Messages.INVALID_RABBIT_TYPE, StringUtils.join(Rabbit.Type.values(), ",")); + } + npc.getTrait(RabbitType.class).setType(type); + Messaging.sendTr(sender, Messages.RABBIT_TYPE_SET, npc.getName(), type.name()); + } + + @Command( + aliases = { "npc" }, + usage = "remove|rem (all|id|name)", + desc = "Remove a NPC", + modifiers = { "remove", "rem" }, + min = 1, + max = 2) + @Requirements + public void remove(final CommandContext args, final CommandSender sender, NPC npc) throws CommandException { + if (args.argsLength() == 2) { + if (args.getString(1).equalsIgnoreCase("all")) { + if (!sender.hasPermission("citizens.admin.remove.all") && !sender.hasPermission("citizens.admin")) + throw new NoPermissionsException(); + npcRegistry.deregisterAll(); + Messaging.sendTr(sender, Messages.REMOVED_ALL_NPCS); + return; + } else { + NPCCommandSelector.Callback callback = new NPCCommandSelector.Callback() { + @Override + public void run(NPC npc) throws CommandException { + if (npc == null) + throw new CommandException(Messages.COMMAND_MUST_HAVE_SELECTED); + if (!(sender instanceof ConsoleCommandSender) && !npc.getTrait(Owner.class).isOwnedBy(sender)) + throw new CommandException(Messages.COMMAND_MUST_BE_OWNER); + if (!sender.hasPermission("citizens.npc.remove") && !sender.hasPermission("citizens.admin")) + throw new NoPermissionsException(); + npc.destroy(); + Messaging.sendTr(sender, Messages.NPC_REMOVED, npc.getName()); + } + }; + NPCCommandSelector.startWithCallback(callback, npcRegistry, sender, args, args.getString(1)); + return; + } + } + if (npc == null) + throw new CommandException(Messages.COMMAND_MUST_HAVE_SELECTED); + if (!(sender instanceof ConsoleCommandSender) && !npc.getTrait(Owner.class).isOwnedBy(sender)) + throw new CommandException(Messages.COMMAND_MUST_BE_OWNER); + if (!sender.hasPermission("citizens.npc.remove") && !sender.hasPermission("citizens.admin")) + throw new NoPermissionsException(); + npc.destroy(); + Messaging.sendTr(sender, Messages.NPC_REMOVED, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "rename [name]", + desc = "Rename a NPC", + modifiers = { "rename" }, + min = 2, + permission = "citizens.npc.rename") + public void rename(CommandContext args, CommandSender sender, NPC npc) { + String oldName = npc.getName(); + String newName = Colorizer.parseColors(args.getJoinedStrings(1)); + int nameLength = npc.getTrait(MobType.class).getType() == EntityType.PLAYER ? 46 : 64; + if (newName.length() > nameLength) { + Messaging.sendErrorTr(sender, Messages.NPC_NAME_TOO_LONG); + newName = newName.substring(0, nameLength); + } + Location prev = npc.isSpawned() ? npc.getEntity().getLocation() : null; + npc.despawn(DespawnReason.PENDING_RESPAWN); + npc.setName(newName); + if (prev != null) { + npc.spawn(prev); + } + + Messaging.sendTr(sender, Messages.NPC_RENAMED, oldName, newName); + } + + @Command( + aliases = { "npc" }, + usage = "respawn [delay in ticks]", + desc = "Sets an NPC's respawn delay in ticks", + modifiers = { "respawn" }, + min = 1, + max = 2, + permission = "citizens.npc.respawn") + public void respawn(CommandContext args, CommandSender sender, NPC npc) { + if (args.argsLength() > 1) { + int delay = args.getInteger(1); + npc.data().setPersistent(NPC.RESPAWN_DELAY_METADATA, delay); + Messaging.sendTr(sender, Messages.RESPAWN_DELAY_SET, delay); + } else { + Messaging.sendTr(sender, Messages.RESPAWN_DELAY_DESCRIBE, npc.data().get(NPC.RESPAWN_DELAY_METADATA, -1)); + } + } + + @Command( + aliases = { "npc" }, + usage = "script --add [files] --remove [files]", + desc = "Controls an NPC's scripts", + modifiers = { "script" }, + min = 1, + max = 1, + permission = "citizens.npc.script") + public void script(CommandContext args, CommandSender sender, NPC npc) { + ScriptTrait trait = npc.getTrait(ScriptTrait.class); + if (args.hasValueFlag("add")) { + List files = new ArrayList(); + for (String file : args.getFlag("add").split(",")) { + if (!trait.validateFile(file)) { + Messaging.sendErrorTr(sender, Messages.INVALID_SCRIPT_FILE, file); + return; + } + files.add(file); + } + trait.addScripts(files); + } + if (args.hasValueFlag("remove")) { + trait.removeScripts(Arrays.asList(args.getFlag("remove").split(","))); + } + Messaging.sendTr(sender, Messages.CURRENT_SCRIPTS, npc.getName(), Joiner.on("]],[[ ").join(trait.getScripts())); + } + + @Command( + aliases = { "npc" }, + usage = "select|sel [id|name] (--r range)", + desc = "Select a NPC with the given ID or name", + modifiers = { "select", "sel" }, + min = 1, + max = 2, + permission = "citizens.npc.select") + @Requirements + public void select(CommandContext args, final CommandSender sender, final NPC npc) throws CommandException { + NPCCommandSelector.Callback callback = new NPCCommandSelector.Callback() { + @Override + public void run(NPC toSelect) throws CommandException { + if (toSelect == null) + throw new CommandException(Messages.NPC_NOT_FOUND); + if (npc != null && toSelect.getId() == npc.getId()) + throw new CommandException(Messages.NPC_ALREADY_SELECTED); + selector.select(sender, toSelect); + Messaging.sendWithNPC(sender, Setting.SELECTION_MESSAGE.asString(), toSelect); + } + }; + if (args.argsLength() <= 1) { + if (!(sender instanceof Player)) + throw new ServerCommandException(); + double range = Math.abs(args.getFlagDouble("r", 10)); + Entity player = (Player) sender; + final Location location = args.getSenderLocation(); + List search = player.getNearbyEntities(range, range, range); + Collections.sort(search, new Comparator() { + @Override + public int compare(Entity o1, Entity o2) { + double d = o1.getLocation().distanceSquared(location) - o2.getLocation().distanceSquared(location); + return d > 0 ? 1 : d < 0 ? -1 : 0; + } + }); + for (Entity possibleNPC : search) { + NPC test = npcRegistry.getNPC(possibleNPC); + if (test == null) + continue; + callback.run(test); + break; + } + } else { + NPCCommandSelector.startWithCallback(callback, npcRegistry, sender, args, args.getString(1)); + } + } + + @Command( + aliases = { "npc" }, + usage = "sheep (--color [color]) (--sheared [sheared])", + desc = "Sets sheep modifiers", + modifiers = { "sheep" }, + min = 1, + max = 1, + permission = "citizens.npc.sheep") + @Requirements(selected = true, ownership = true, types = { EntityType.SHEEP }) + public void sheep(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + SheepTrait trait = npc.getTrait(SheepTrait.class); + boolean hasArg = false; + if (args.hasValueFlag("sheared")) { + trait.setSheared(Boolean.valueOf(args.getFlag("sheared"))); + hasArg = true; + } + if (args.hasValueFlag("color")) { + DyeColor color = Util.matchEnum(DyeColor.values(), args.getFlag("color")); + if (color != null) { + trait.setColor(color); + Messaging.sendTr(sender, Messages.SHEEP_COLOR_SET, color.toString().toLowerCase()); + } else { + Messaging.sendErrorTr(sender, Messages.INVALID_SHEEP_COLOR, Util.listValuesPretty(DyeColor.values())); + } + hasArg = true; + } + if (!hasArg) { + throw new CommandException(); + } + } + + @Command( + aliases = { "npc" }, + usage = "skeletontype [type]", + desc = "Sets the NPC's skeleton type", + modifiers = { "skeletontype", "sktype" }, + min = 2, + max = 2, + permission = "citizens.npc.skeletontype") + @Requirements(selected = true, ownership = true, types = EntityType.SKELETON) + public void skeletonType(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + SkeletonType type; + try { + type = SkeletonType.valueOf(args.getString(1).toUpperCase()); + } catch (IllegalArgumentException ex) { + throw new CommandException(Messages.INVALID_SKELETON_TYPE, StringUtils.join(SkeletonType.values(), ",")); + } + npc.getTrait(NPCSkeletonType.class).setType(type); + Messaging.sendTr(sender, Messages.SKELETON_TYPE_SET, npc.getName(), type); + } + + @Command( + aliases = { "npc" }, + usage = "skin (-c -p) [name]", + desc = "Sets an NPC's skin name, Use -p to save a skin snapshot that won't change", + modifiers = { "skin" }, + min = 1, + max = 2, + flags = "cp", + permission = "citizens.npc.skin") + @Requirements(types = EntityType.PLAYER, selected = true, ownership = true) + public void skin(final CommandContext args, final CommandSender sender, final NPC npc) throws CommandException { + String skinName = npc.getName(); + if (args.hasFlag('c')) { + npc.data().remove(NPC.PLAYER_SKIN_UUID_METADATA); + } else { + if (args.argsLength() != 2) + throw new CommandException(); + npc.data().setPersistent(NPC.PLAYER_SKIN_UUID_METADATA, args.getString(1)); + if (args.hasFlag('p')) { + npc.data().setPersistent(NPC.PLAYER_SKIN_USE_LATEST, false); + } + skinName = args.getString(1); + } + Messaging.sendTr(sender, Messages.SKIN_SET, npc.getName(), skinName); + if (npc.isSpawned()) { + SkinnableEntity skinnable = npc.getEntity() instanceof SkinnableEntity ? (SkinnableEntity) npc.getEntity() + : null; + if (skinnable != null) { + skinnable.setSkinName(skinName, args.hasFlag('p')); + } + } + } + + @Command( + aliases = { "npc" }, + usage = "skinlayers (--cape [true|false]) (--hat [true|false]) (--jacket [true|false]) (--sleeves [true|false]) (--pants [true|false])", + desc = "Sets an NPC's skin layers visibility.", + modifiers = { "skinlayers" }, + min = 1, + max = 5, + permission = "citizens.npc.skinlayers") + @Requirements(types = EntityType.PLAYER, selected = true, ownership = true) + public void skinLayers(final CommandContext args, final CommandSender sender, final NPC npc) + throws CommandException { + SkinLayers trait = npc.getTrait(SkinLayers.class); + if (args.hasValueFlag("cape")) { + trait.setVisible(Layer.CAPE, Boolean.valueOf(args.getFlag("cape"))); + } + if (args.hasValueFlag("hat")) { + trait.setVisible(Layer.HAT, Boolean.valueOf(args.getFlag("hat"))); + } + if (args.hasValueFlag("jacket")) { + trait.setVisible(Layer.JACKET, Boolean.valueOf(args.getFlag("jacket"))); + } + if (args.hasValueFlag("sleeves")) { + boolean hasSleeves = Boolean.valueOf(args.getFlag("sleeves")); + trait.setVisible(Layer.LEFT_SLEEVE, hasSleeves); + trait.setVisible(Layer.RIGHT_SLEEVE, hasSleeves); + } + if (args.hasValueFlag("pants")) { + boolean hasPants = Boolean.valueOf(args.getFlag("pants")); + trait.setVisible(Layer.LEFT_PANTS, hasPants); + trait.setVisible(Layer.RIGHT_PANTS, hasPants); + } + Messaging.sendTr(sender, Messages.SKIN_LAYERS_SET, npc.getName(), trait.isVisible(Layer.CAPE), + trait.isVisible(Layer.HAT), trait.isVisible(Layer.JACKET), + trait.isVisible(Layer.LEFT_SLEEVE) || trait.isVisible(Layer.RIGHT_SLEEVE), + trait.isVisible(Layer.LEFT_PANTS) || trait.isVisible(Layer.RIGHT_PANTS)); + } + + @Command( + aliases = { "npc" }, + usage = "size [size]", + desc = "Sets the NPC's size", + modifiers = { "size" }, + min = 1, + max = 2, + permission = "citizens.npc.size") + @Requirements(selected = true, ownership = true, types = { EntityType.MAGMA_CUBE, EntityType.SLIME }) + public void slimeSize(CommandContext args, CommandSender sender, NPC npc) { + SlimeSize trait = npc.getTrait(SlimeSize.class); + if (args.argsLength() <= 1) { + trait.describe(sender); + return; + } + int size = Math.max(-2, args.getInteger(1)); + trait.setSize(size); + Messaging.sendTr(sender, Messages.SIZE_SET, npc.getName(), size); + } + + @Command( + aliases = { "npc" }, + usage = "sound (--death [death sound|d]) (--ambient [ambient sound|d]) (--hurt [hurt sound|d]) (-n(one)) (-d(efault))", + desc = "Sets an NPC's played sounds", + modifiers = { "sound" }, + flags = "dns", + min = 1, + max = 1, + permission = "citizens.npc.sound") + @Requirements(selected = true, ownership = true, livingEntity = true, excludedTypes = { EntityType.PLAYER }) + public void sound(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String ambientSound = npc.data().get(NPC.AMBIENT_SOUND_METADATA); + String deathSound = npc.data().get(NPC.DEATH_SOUND_METADATA); + String hurtSound = npc.data().get(NPC.HURT_SOUND_METADATA); + if (args.getValueFlags().size() == 0 && args.getFlags().size() == 0) { + Messaging.sendTr(sender, Messages.SOUND_INFO, npc.getName(), ambientSound, hurtSound, deathSound); + return; + } + + if (args.hasFlag('n') || args.hasFlag('s')) { + ambientSound = deathSound = hurtSound = ""; + npc.data().setPersistent(NPC.SILENT_METADATA, true); + } + if (args.hasFlag('d')) { + ambientSound = deathSound = hurtSound = null; + } else { + if (args.hasValueFlag("death")) { + deathSound = args.getFlag("death").equals("d") ? null : NMS.getSound(args.getFlag("death")); + } + if (args.hasValueFlag("ambient")) { + ambientSound = args.getFlag("ambient").equals("d") ? null : NMS.getSound(args.getFlag("ambient")); + } + if (args.hasValueFlag("hurt")) { + hurtSound = args.getFlag("hurt").equals("d") ? null : NMS.getSound(args.getFlag("hurt")); + } + } + if (deathSound == null) { + npc.data().remove(NPC.DEATH_SOUND_METADATA); + } else { + npc.data().setPersistent(NPC.DEATH_SOUND_METADATA, deathSound); + } + if (hurtSound == null) { + npc.data().remove(NPC.HURT_SOUND_METADATA); + } else { + npc.data().setPersistent(NPC.HURT_SOUND_METADATA, hurtSound); + } + if (ambientSound == null) { + npc.data().remove(ambientSound); + } else { + npc.data().setPersistent(NPC.AMBIENT_SOUND_METADATA, ambientSound); + } + + if (ambientSound != null && ambientSound.isEmpty()) { + ambientSound = "none"; + } + if (hurtSound != null && hurtSound.isEmpty()) { + hurtSound = "none"; + } + if (deathSound != null && deathSound.isEmpty()) { + deathSound = "none"; + } + if (ambientSound != null || deathSound != null || hurtSound != null) { + npc.data().setPersistent(NPC.SILENT_METADATA, false); + } + Messaging.sendTr(sender, Messages.SOUND_SET, npc.getName(), ambientSound, hurtSound, deathSound); + } + + @Command( + aliases = { "npc" }, + usage = "spawn (id|name)", + desc = "Spawn an existing NPC", + modifiers = { "spawn" }, + min = 1, + max = 2, + permission = "citizens.npc.spawn") + @Requirements(ownership = true) + public void spawn(final CommandContext args, final CommandSender sender, NPC npc) throws CommandException { + NPCCommandSelector.Callback callback = new NPCCommandSelector.Callback() { + @Override + public void run(NPC respawn) throws CommandException { + if (respawn == null) { + if (args.argsLength() > 1) { + throw new CommandException(Messages.NO_NPC_WITH_ID_FOUND, args.getString(1)); + } else { + throw new CommandException(CommandMessages.MUST_HAVE_SELECTED); + } + } + if (respawn.isSpawned()) { + throw new CommandException(Messages.NPC_ALREADY_SPAWNED, respawn.getName()); + } + Location location = respawn.getTrait(CurrentLocation.class).getLocation(); + if (location == null || args.hasValueFlag("location")) { + if (args.getSenderLocation() == null) + throw new CommandException(Messages.NO_STORED_SPAWN_LOCATION); + + location = args.getSenderLocation(); + } + if (respawn.spawn(location)) { + selector.select(sender, respawn); + Messaging.sendTr(sender, Messages.NPC_SPAWNED, respawn.getName()); + } + } + }; + if (args.argsLength() > 1) { + NPCCommandSelector.startWithCallback(callback, npcRegistry, sender, args, args.getString(1)); + } else { + callback.run(npc); + } + } + + @Command( + aliases = { "npc" }, + usage = "speak message to speak --target npcid|player_name --type vocal_type", + desc = "Uses the NPCs SpeechController to talk", + modifiers = { "speak" }, + min = 2, + permission = "citizens.npc.speak") + public void speak(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String type = npc.getTrait(Speech.class).getDefaultVocalChord(); + String message = Colorizer.parseColors(args.getJoinedStrings(1)); + + if (message.length() <= 0) { + Messaging.send(sender, "Default Vocal Chord for " + npc.getName() + ": " + + npc.getTrait(Speech.class).getDefaultVocalChord()); + return; + } + + SpeechContext context = new SpeechContext(message); + + if (args.hasValueFlag("target")) { + if (args.getFlag("target").matches("\\d+")) { + NPC target = CitizensAPI.getNPCRegistry().getById(Integer.valueOf(args.getFlag("target"))); + if (target != null) + context.addRecipient(target.getEntity()); + } else { + Player player = Bukkit.getPlayer(args.getFlag("target")); + if (player != null) { + context.addRecipient((Entity) player); + } + } + } + + if (args.hasValueFlag("type")) { + if (CitizensAPI.getSpeechFactory().isRegistered(args.getFlag("type"))) + type = args.getFlag("type"); + } + + npc.getDefaultSpeechController().speak(context, type); + } + + @Command( + aliases = { "npc" }, + usage = "speed [speed]", + desc = "Sets the movement speed of an NPC as a percentage", + modifiers = { "speed" }, + min = 2, + max = 2, + permission = "citizens.npc.speed") + public void speed(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + float newSpeed = (float) Math.abs(args.getDouble(1)); + if (newSpeed >= Setting.MAX_SPEED.asDouble()) + throw new CommandException(Messages.SPEED_MODIFIER_ABOVE_LIMIT); + npc.getNavigator().getDefaultParameters().speedModifier(newSpeed); + + Messaging.sendTr(sender, Messages.SPEED_MODIFIER_SET, newSpeed); + } + + @Command( + aliases = { "npc" }, + usage = "swim (--set [true|false])", + desc = "Sets an NPC to swim or not", + modifiers = { "swim" }, + min = 1, + max = 1, + permission = "citizens.npc.swim") + public void swim(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + boolean swim = args.hasValueFlag("set") ? Boolean.parseBoolean(args.getFlag("set")) + : !npc.data().get(NPC.SWIMMING_METADATA, true); + npc.data().setPersistent(NPC.SWIMMING_METADATA, swim); + Messaging.sendTr(sender, swim ? Messages.SWIMMING_SET : Messages.SWIMMING_UNSET, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "targetable", + desc = "Toggles an NPC's targetability", + modifiers = { "targetable" }, + min = 1, + max = 1, + permission = "citizens.npc.targetable") + public void targetable(CommandContext args, CommandSender sender, NPC npc) { + boolean targetable = !npc.data().get(NPC.TARGETABLE_METADATA, + npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true)); + if (args.hasFlag('t')) { + npc.data().set(NPC.TARGETABLE_METADATA, targetable); + } else { + npc.data().setPersistent(NPC.TARGETABLE_METADATA, targetable); + } + Messaging.sendTr(sender, targetable ? Messages.TARGETABLE_SET : Messages.TARGETABLE_UNSET, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "tp", + desc = "Teleport to a NPC", + modifiers = { "tp", "teleport" }, + min = 1, + max = 1, + permission = "citizens.npc.tp") + public void tp(CommandContext args, Player player, NPC npc) { + Location to = npc.getTrait(CurrentLocation.class).getLocation(); + if (to == null) { + Messaging.sendError(player, Messages.TELEPORT_NPC_LOCATION_NOT_FOUND); + return; + } + player.teleport(to, TeleportCause.COMMAND); + Messaging.sendTr(player, Messages.TELEPORTED_TO_NPC, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "tphere", + desc = "Teleport a NPC to your location", + modifiers = { "tphere", "tph", "move" }, + min = 1, + max = 1, + permission = "citizens.npc.tphere") + public void tphere(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + if (args.getSenderLocation() == null) + throw new ServerCommandException(); + // Spawn the NPC if it isn't spawned to prevent NPEs + if (!npc.isSpawned()) { + npc.spawn(args.getSenderLocation()); + if (!sender.hasPermission("citizens.npc.tphere.multiworld") + && npc.getEntity().getLocation().getWorld() != args.getSenderLocation().getWorld()) { + npc.despawn(DespawnReason.REMOVAL); + throw new CommandException(Messages.CANNOT_TELEPORT_ACROSS_WORLDS); + } + } else { + if (!sender.hasPermission("citizens.npc.tphere.multiworld") + && npc.getEntity().getLocation().getWorld() != args.getSenderLocation().getWorld()) { + npc.despawn(DespawnReason.REMOVAL); + throw new CommandException(Messages.CANNOT_TELEPORT_ACROSS_WORLDS); + } + npc.teleport(args.getSenderLocation(), TeleportCause.COMMAND); + } + Messaging.sendTr(sender, Messages.NPC_TELEPORTED, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "tpto [player name|npc id] [player name|npc id]", + desc = "Teleport an NPC or player to another NPC or player", + modifiers = { "tpto" }, + min = 3, + max = 3, + permission = "citizens.npc.tpto") + @Requirements + public void tpto(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Entity from = null, to = null; + if (npc != null) { + from = npc.getEntity(); + } + boolean firstWasPlayer = false; + try { + int id = args.getInteger(1); + NPC fromNPC = CitizensAPI.getNPCRegistry().getById(id); + if (fromNPC != null) { + from = fromNPC.getEntity(); + } + } catch (NumberFormatException e) { + from = Bukkit.getPlayerExact(args.getString(1)); + firstWasPlayer = true; + } + try { + int id = args.getInteger(2); + NPC toNPC = CitizensAPI.getNPCRegistry().getById(id); + if (toNPC != null) { + to = toNPC.getEntity(); + } + } catch (NumberFormatException e) { + if (!firstWasPlayer) { + to = Bukkit.getPlayerExact(args.getString(2)); + } + } + if (from == null) + throw new CommandException(Messages.FROM_ENTITY_NOT_FOUND); + if (to == null) + throw new CommandException(Messages.TO_ENTITY_NOT_FOUND); + from.teleport(to); + Messaging.sendTr(sender, Messages.TPTO_SUCCESS); + } + + @Command( + aliases = { "npc" }, + usage = "type [type]", + desc = "Sets an NPC's entity type", + modifiers = { "type" }, + min = 2, + max = 2, + permission = "citizens.npc.type") + public void type(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + EntityType type = Util.matchEntityType(args.getString(1)); + if (type == null) + throw new CommandException(Messages.INVALID_ENTITY_TYPE, args.getString(1)); + npc.setBukkitEntityType(type); + Messaging.sendTr(sender, Messages.ENTITY_TYPE_SET, npc.getName(), args.getString(1)); + } + + @Command( + aliases = { "npc" }, + usage = "vulnerable (-t)", + desc = "Toggles an NPC's vulnerability", + modifiers = { "vulnerable" }, + min = 1, + max = 1, + flags = "t", + permission = "citizens.npc.vulnerable") + public void vulnerable(CommandContext args, CommandSender sender, NPC npc) { + boolean vulnerable = !npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true); + if (args.hasFlag('t')) { + npc.data().set(NPC.DEFAULT_PROTECTED_METADATA, vulnerable); + } else { + npc.data().setPersistent(NPC.DEFAULT_PROTECTED_METADATA, vulnerable); + } + String key = vulnerable ? Messages.VULNERABLE_STOPPED : Messages.VULNERABLE_SET; + Messaging.sendTr(sender, key, npc.getName()); + } + + @Command( + aliases = { "npc" }, + usage = "wither (--charged [charged])", + desc = "Sets wither modifiers", + modifiers = { "wither" }, + min = 1, + max = 1, + permission = "citizens.npc.wither") + @Requirements(selected = true, ownership = true, types = { EntityType.WITHER }) + public void wither(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + WitherTrait trait = npc.getTrait(WitherTrait.class); + boolean hasArg = false; + if (args.hasValueFlag("charged")) { + trait.setCharged(Boolean.valueOf(args.getFlag("charged"))); + hasArg = true; + } + if (!hasArg) { + throw new CommandException(); + } + } + + @Command( + aliases = { "npc" }, + usage = "wolf (-s(itting) a(ngry) t(amed)) --collar [hex rgb color|name]", + desc = "Sets wolf modifiers", + modifiers = { "wolf" }, + min = 1, + max = 1, + flags = "sat", + permission = "citizens.npc.wolf") + @Requirements(selected = true, ownership = true, types = EntityType.WOLF) + public void wolf(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + WolfModifiers trait = npc.getTrait(WolfModifiers.class); + trait.setAngry(args.hasFlag('a')); + trait.setSitting(args.hasFlag('s')); + trait.setTamed(args.hasFlag('t')); + if (args.hasValueFlag("collar")) { + String unparsed = args.getFlag("collar"); + DyeColor color = null; + try { + color = DyeColor.valueOf(unparsed.toUpperCase().replace(' ', '_')); + } catch (IllegalArgumentException e) { + try { + int rgb = Integer.parseInt(unparsed.replace("#", ""), 16); + color = DyeColor.getByColor(org.bukkit.Color.fromRGB(rgb)); + } catch (NumberFormatException ex) { + throw new CommandException(Messages.COLLAR_COLOUR_NOT_RECOGNISED, unparsed); + } + } + if (color == null) + throw new CommandException(Messages.COLLAR_COLOUR_NOT_SUPPORTED, unparsed); + trait.setCollarColor(color); + } + Messaging.sendTr(sender, Messages.WOLF_TRAIT_UPDATED, npc.getName(), args.hasFlag('a'), args.hasFlag('s'), + args.hasFlag('t'), trait.getCollarColor().name()); + } + + @Command( + aliases = { "npc" }, + usage = "zombiemod (-b(aby), -v(illager) --p(rofession) [profession])", + desc = "Sets a zombie NPC to be a baby or villager", + modifiers = { "zombie", "zombiemod" }, + flags = "bv", + min = 1, + max = 1, + permission = "citizens.npc.zombiemodifier") + @Requirements(selected = true, ownership = true, types = { EntityType.ZOMBIE, EntityType.PIG_ZOMBIE }) + public void zombieModifier(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + ZombieModifier trait = npc.getTrait(ZombieModifier.class); + if (args.hasFlag('b')) { + boolean isBaby = trait.toggleBaby(); + Messaging.sendTr(sender, isBaby ? Messages.ZOMBIE_BABY_SET : Messages.ZOMBIE_BABY_UNSET, npc.getName()); + } + if (args.hasFlag('v')) { + boolean isVillager = trait.toggleVillager(); + Messaging.sendTr(sender, isVillager ? Messages.ZOMBIE_VILLAGER_SET : Messages.ZOMBIE_VILLAGER_UNSET, + npc.getName()); + } + if (args.hasValueFlag("profession") || args.hasValueFlag("p")) { + Profession profession = Util.matchEnum(Profession.values(), args.getFlag("profession", args.getFlag("p"))); + if (profession == null) { + throw new CommandException(); + } + trait.setProfession(profession); + Messaging.sendTr(sender, Messages.ZOMBIE_VILLAGER_PROFESSION_SET, npc.getName(), + Util.prettyEnum(profession)); + } + } +} diff --git a/main/java/net/citizensnpcs/commands/TemplateCommands.java b/main/java/net/citizensnpcs/commands/TemplateCommands.java new file mode 100644 index 000000000..dc10b1679 --- /dev/null +++ b/main/java/net/citizensnpcs/commands/TemplateCommands.java @@ -0,0 +1,120 @@ +package net.citizensnpcs.commands; + +import java.util.List; + +import javax.annotation.Nullable; + +import net.citizensnpcs.Citizens; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.npc.Template; +import net.citizensnpcs.npc.Template.TemplateBuilder; +import net.citizensnpcs.util.Messages; + +import org.bukkit.command.CommandSender; + +import com.google.common.base.Function; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +@Requirements(selected = true, ownership = true) +public class TemplateCommands { + public TemplateCommands(Citizens plugin) { + } + + @Command( + aliases = { "template", "tpl" }, + usage = "apply [template name] (id id2...)", + desc = "Applies a template to the selected NPC", + modifiers = { "apply" }, + min = 2, + permission = "citizens.templates.apply") + @Requirements + public void apply(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Template template = Template.byName(args.getString(1)); + if (template == null) + throw new CommandException(Messages.TEMPLATE_MISSING); + int appliedCount = 0; + if (args.argsLength() == 2) { + if (npc == null) + throw new CommandException(Messaging.tr(Messages.COMMAND_MUST_HAVE_SELECTED)); + template.apply(npc); + appliedCount++; + } else { + String joined = args.getJoinedStrings(2, ','); + List ids = Lists.newArrayList(); + for (String id : Splitter.on(',').trimResults().split(joined)) { + int parsed = Integer.parseInt(id); + ids.add(parsed); + } + Iterable transformed = Iterables.transform(ids, new Function() { + @Override + public NPC apply(@Nullable Integer arg0) { + if (arg0 == null) + return null; + return CitizensAPI.getNPCRegistry().getById(arg0); + } + }); + for (NPC toApply : transformed) { + template.apply(toApply); + appliedCount++; + } + } + Messaging.sendTr(sender, Messages.TEMPLATE_APPLIED, appliedCount); + } + + @Command( + aliases = { "template", "tpl" }, + usage = "create [template name] (-o)", + desc = "Creates a template from the selected NPC", + modifiers = { "create" }, + min = 2, + max = 2, + flags = "o", + permission = "citizens.templates.create") + public void create(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String name = args.getString(1); + if (Template.byName(name) != null) + throw new CommandException(Messages.TEMPLATE_CONFLICT); + + TemplateBuilder.create(name).from(npc).override(args.hasFlag('o')).buildAndSave(); + Messaging.sendTr(sender, Messages.TEMPLATE_CREATED); + } + + @Command( + aliases = { "template", "tpl" }, + usage = "delete [template name]", + desc = "Deletes a template", + modifiers = { "delete" }, + min = 2, + max = 2, + permission = "citizens.templates.delete") + public void delete(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String name = args.getString(1); + if (Template.byName(name) == null) + throw new CommandException(Messages.TEMPLATE_MISSING); + Template.byName(name).delete(); + Messaging.sendTr(sender, Messages.TEMPLATE_DELETED, name); + } + + @Command( + aliases = { "template", "tpl" }, + usage = "list", + desc = "Lists available templates", + modifiers = { "list" }, + min = 1, + max = 1, + permission = "citizens.templates.list") + public void list(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Messaging.sendTr(sender, Messages.TEMPLATE_LIST_HEADER); + for (Template template : Template.allTemplates()) { + Messaging.send(sender, "[[-]] " + template.getName()); + } + } +} diff --git a/main/java/net/citizensnpcs/commands/TraitCommands.java b/main/java/net/citizensnpcs/commands/TraitCommands.java new file mode 100644 index 000000000..a328c5511 --- /dev/null +++ b/main/java/net/citizensnpcs/commands/TraitCommands.java @@ -0,0 +1,167 @@ +package net.citizensnpcs.commands; + +import java.util.List; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandConfigurable; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.command.exception.NoPermissionsException; +import net.citizensnpcs.api.event.NPCTraitCommandAttachEvent; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.Trait; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.StringHelper; + +@Requirements(selected = true, ownership = true) +public class TraitCommands { + @Command( + aliases = { "trait", "tr" }, + usage = "add [trait name]...", + desc = "Adds traits to the NPC", + modifiers = { "add", "a" }, + min = 2, + permission = "citizens.npc.trait") + public void add(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + List added = Lists.newArrayList(); + List failed = Lists.newArrayList(); + for (String traitName : Splitter.on(',').split(args.getJoinedStrings(1))) { + if (!sender.hasPermission("citizens.npc.trait." + traitName) + && !sender.hasPermission("citizens.npc.trait.*")) { + failed.add(String.format("%s: No permission", traitName)); + continue; + } + + Class clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName); + if (clazz == null) { + failed.add(String.format("%s: Trait not found", traitName)); + continue; + } + if (npc.hasTrait(clazz)) { + failed.add(String.format("%s: Already added", traitName)); + continue; + } + addTrait(npc, clazz, sender); + added.add(StringHelper.wrap(traitName)); + } + if (added.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_ADDED, Joiner.on(", ").join(added)); + if (failed.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_FAILED_TO_ADD, Joiner.on(", ").join(failed)); + } + + private void addTrait(NPC npc, Class clazz, CommandSender sender) { + npc.addTrait(clazz); + Bukkit.getPluginManager().callEvent(new NPCTraitCommandAttachEvent(npc, clazz, sender)); + } + + @Command( + aliases = { "traitc", "trc" }, + usage = "[trait name] (flags)", + desc = "Configures a trait", + modifiers = { "*" }, + min = 1, + flags = "*", + permission = "citizens.npc.trait-configure") + public void configure(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + String traitName = args.getString(0); + if (!sender.hasPermission("citizens.npc.trait-configure." + traitName) + && !sender.hasPermission("citizens.npc.trait-configure.*")) + throw new NoPermissionsException(); + Class clazz = CitizensAPI.getTraitFactory().getTraitClass(args.getString(0)); + if (clazz == null) + throw new CommandException(Messages.TRAIT_NOT_FOUND); + if (!CommandConfigurable.class.isAssignableFrom(clazz)) + throw new CommandException(Messages.TRAIT_NOT_CONFIGURABLE); + if (!npc.hasTrait(clazz)) + throw new CommandException(Messages.TRAIT_NOT_FOUND_ON_NPC); + CommandConfigurable trait = (CommandConfigurable) npc.getTrait(clazz); + trait.configure(args); + } + + @Command( + aliases = { "trait", "tr" }, + usage = "remove [trait name]...", + desc = "Removes traits on the NPC", + modifiers = { "remove", "rem", "r" }, + min = 1, + permission = "citizens.npc.trait") + public void remove(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + List removed = Lists.newArrayList(); + List failed = Lists.newArrayList(); + for (String traitName : Splitter.on(',').split(args.getJoinedStrings(0))) { + if (!sender.hasPermission("citizens.npc.trait." + traitName) + && !sender.hasPermission("citizens.npc.trait.*")) { + failed.add(String.format("%s: No permission", traitName)); + continue; + } + + Class clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName); + if (clazz == null) { + failed.add(String.format("%s: Trait not found", traitName)); + continue; + } + boolean hasTrait = npc.hasTrait(clazz); + if (!hasTrait) { + failed.add(String.format("%s: Trait not attached", traitName)); + continue; + } + npc.removeTrait(clazz); + removed.add(StringHelper.wrap(traitName)); + } + if (removed.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_REMOVED, Joiner.on(", ").join(removed)); + if (failed.size() > 0) + Messaging.sendTr(sender, Messages.FAILED_TO_REMOVE, Joiner.on(", ").join(failed)); + } + + @Command( + aliases = { "trait", "tr" }, + usage = "[trait name], [trait name]...", + desc = "Toggles traits on the NPC", + modifiers = { "*" }, + min = 1, + permission = "citizens.npc.trait") + public void toggle(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + List added = Lists.newArrayList(); + List removed = Lists.newArrayList(); + List failed = Lists.newArrayList(); + for (String traitName : Splitter.on(',').split(args.getJoinedStrings(0))) { + if (!sender.hasPermission("citizens.npc.trait." + traitName) + && !sender.hasPermission("citizens.npc.trait.*")) { + failed.add(String.format("%s: No permission", traitName)); + continue; + } + + Class clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName); + if (clazz == null) { + failed.add(String.format("%s: Trait not found", traitName)); + continue; + } + boolean remove = npc.hasTrait(clazz); + if (remove) { + npc.removeTrait(clazz); + removed.add(StringHelper.wrap(traitName)); + continue; + } + addTrait(npc, clazz, sender); + added.add(StringHelper.wrap(traitName)); + } + if (added.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_ADDED, Joiner.on(", ").join(added)); + if (removed.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_REMOVED, Joiner.on(", ").join(removed)); + if (failed.size() > 0) + Messaging.sendTr(sender, Messages.TRAITS_FAILED_TO_CHANGE, Joiner.on(", ").join(failed)); + } +} diff --git a/main/java/net/citizensnpcs/commands/WaypointCommands.java b/main/java/net/citizensnpcs/commands/WaypointCommands.java new file mode 100644 index 000000000..29503073c --- /dev/null +++ b/main/java/net/citizensnpcs/commands/WaypointCommands.java @@ -0,0 +1,58 @@ +package net.citizensnpcs.commands; + +import net.citizensnpcs.Citizens; +import net.citizensnpcs.api.command.Command; +import net.citizensnpcs.api.command.CommandContext; +import net.citizensnpcs.api.command.Requirements; +import net.citizensnpcs.api.command.exception.CommandException; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.trait.waypoint.Waypoints; +import net.citizensnpcs.util.Messages; + +import org.bukkit.command.CommandSender; + +@Requirements(ownership = true, selected = true) +public class WaypointCommands { + public WaypointCommands(Citizens plugin) { + } + + // TODO: refactor into a policy style system + @Command( + aliases = { "waypoints", "waypoint", "wp" }, + usage = "disableteleport", + desc = "Disables teleportation when stuck (temporary command)", + modifiers = { "disableteleport" }, + min = 1, + max = 1, + permission = "citizens.waypoints.disableteleport") + public void disableTeleporting(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + npc.getNavigator().getDefaultParameters().stuckAction(null); + Messaging.sendTr(sender, Messages.WAYPOINT_TELEPORTING_DISABLED); + } + + @Command( + aliases = { "waypoints", "waypoint", "wp" }, + usage = "provider [provider name] (-d)", + desc = "Sets the current waypoint provider", + modifiers = { "provider" }, + min = 1, + max = 2, + flags = "d", + permission = "citizens.waypoints.provider") + public void provider(CommandContext args, CommandSender sender, NPC npc) throws CommandException { + Waypoints waypoints = npc.getTrait(Waypoints.class); + if (args.argsLength() == 1) { + if (args.hasFlag('d')) { + waypoints.describeProviders(sender); + } else { + Messaging.sendTr(sender, Messages.CURRENT_WAYPOINT_PROVIDER, waypoints.getCurrentProviderName()); + } + return; + } + boolean success = waypoints.setWaypointProvider(args.getString(1)); + if (!success) + throw new CommandException("Provider not found."); + Messaging.sendTr(sender, Messages.WAYPOINT_PROVIDER_SET, args.getString(1)); + } +} diff --git a/main/java/net/citizensnpcs/editor/CopierEditor.java b/main/java/net/citizensnpcs/editor/CopierEditor.java new file mode 100644 index 000000000..3c8e82464 --- /dev/null +++ b/main/java/net/citizensnpcs/editor/CopierEditor.java @@ -0,0 +1,54 @@ +package net.citizensnpcs.editor; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.trait.CurrentLocation; +import net.citizensnpcs.util.Messages; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; + +public class CopierEditor extends Editor { + private final String name; + private final NPC npc; + private final Player player; + + public CopierEditor(Player player, NPC npc) { + this.player = player; + this.npc = npc; + this.name = npc.getFullName(); + } + + @Override + public void begin() { + Messaging.sendTr(player, Messages.COPIER_EDITOR_BEGIN); + } + + @Override + public void end() { + Messaging.sendTr(player, Messages.COPIER_EDITOR_END); + } + + @EventHandler + public void onBlockClick(PlayerInteractEvent event) { + if (event.getClickedBlock() == null) { + return; + } + NPC copy = npc.clone(); + if (!copy.getFullName().equals(name)) { + copy.setName(name); + } + + if (copy.isSpawned() && player.isOnline()) { + Location location = player.getLocation(); + location.getChunk().load(); + copy.teleport(location, TeleportCause.PLUGIN); + copy.getTrait(CurrentLocation.class).setLocation(location); + } + + Messaging.sendTr(player, Messages.NPC_COPIED, npc.getName()); + } +} diff --git a/main/java/net/citizensnpcs/editor/Editor.java b/main/java/net/citizensnpcs/editor/Editor.java new file mode 100644 index 000000000..a56579ffd --- /dev/null +++ b/main/java/net/citizensnpcs/editor/Editor.java @@ -0,0 +1,60 @@ +package net.citizensnpcs.editor; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; + +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; + +public abstract class Editor implements Listener { + public abstract void begin(); + + public abstract void end(); + + private static void enter(Player player, Editor editor) { + editor.begin(); + player.getServer().getPluginManager().registerEvents(editor, + player.getServer().getPluginManager().getPlugin("Citizens")); + EDITING.put(player.getName(), editor); + } + + public static void enterOrLeave(Player player, Editor editor) { + if (editor == null) + return; + Editor edit = EDITING.get(player.getName()); + if (edit == null) { + enter(player, editor); + } else if (edit.getClass() == editor.getClass()) { + leave(player); + } else { + Messaging.sendErrorTr(player, Messages.ALREADY_IN_EDITOR); + } + } + + public static boolean hasEditor(Player player) { + return EDITING.containsKey(player.getName()); + } + + public static void leave(Player player) { + if (!hasEditor(player)) + return; + Editor editor = EDITING.remove(player.getName()); + HandlerList.unregisterAll(editor); + editor.end(); + } + + public static void leaveAll() { + for (Entry entry : EDITING.entrySet()) { + entry.getValue().end(); + HandlerList.unregisterAll(entry.getValue()); + } + EDITING.clear(); + } + + private static final Map EDITING = new HashMap(); +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/editor/EndermanEquipper.java b/main/java/net/citizensnpcs/editor/EndermanEquipper.java new file mode 100644 index 000000000..64c20e0ee --- /dev/null +++ b/main/java/net/citizensnpcs/editor/EndermanEquipper.java @@ -0,0 +1,42 @@ +package net.citizensnpcs.editor; + +import org.bukkit.Material; +import org.bukkit.entity.Enderman; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.MaterialData; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.trait.Equipment; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; + +public class EndermanEquipper implements Equipper { + @Override + public void equip(Player equipper, NPC npc) { + ItemStack hand = equipper.getInventory().getItemInMainHand(); + if (!hand.getType().isBlock()) { + Messaging.sendErrorTr(equipper, Messages.EQUIPMENT_EDITOR_INVALID_BLOCK); + return; + } + + MaterialData carried = ((Enderman) npc.getEntity()).getCarriedMaterial(); + if (carried.getItemType() == Material.AIR) { + if (hand.getType() == Material.AIR) { + Messaging.sendErrorTr(equipper, Messages.EQUIPMENT_EDITOR_INVALID_BLOCK); + return; + } + } else { + equipper.getWorld().dropItemNaturally(npc.getEntity().getLocation(), carried.toItemStack(1)); + ((Enderman) npc.getEntity()).setCarriedMaterial(hand.getData()); + } + + ItemStack set = hand.clone(); + if (set.getType() != Material.AIR) { + set.setAmount(1); + hand.setAmount(hand.getAmount() - 1); + equipper.getInventory().setItemInMainHand(hand); + } + npc.getTrait(Equipment.class).set(0, set); + } +} diff --git a/main/java/net/citizensnpcs/editor/EquipmentEditor.java b/main/java/net/citizensnpcs/editor/EquipmentEditor.java new file mode 100644 index 000000000..8ba1dad89 --- /dev/null +++ b/main/java/net/citizensnpcs/editor/EquipmentEditor.java @@ -0,0 +1,113 @@ +package net.citizensnpcs.editor; + +import java.util.Map; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.Event.Result; +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 org.bukkit.inventory.ItemStack; + +import com.google.common.collect.Maps; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.trait.Equipment; +import net.citizensnpcs.api.trait.trait.Equipment.EquipmentSlot; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; + +public class EquipmentEditor extends Editor { + private final NPC npc; + private final Player player; + + public EquipmentEditor(Player player, NPC npc) { + this.player = player; + this.npc = npc; + } + + @Override + public void begin() { + Messaging.sendTr(player, Messages.EQUIPMENT_EDITOR_BEGIN); + } + + @Override + public void end() { + Messaging.sendTr(player, Messages.EQUIPMENT_EDITOR_END); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerChat(final AsyncPlayerChatEvent event) { + EquipmentSlot slot = null; + if (event.getMessage().equals("helmet") + && event.getPlayer().hasPermission("citizens.npc.edit.equip.any-helmet")) { + slot = EquipmentSlot.HELMET; + } + if (event.getMessage().equals("offhand") + && event.getPlayer().hasPermission("citizens.npc.edit.equip.offhand")) { + slot = EquipmentSlot.OFF_HAND; + } + if (slot == null) { + return; + } + final EquipmentSlot finalSlot = slot; + Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() { + @Override + public void run() { + if (!event.getPlayer().isValid()) + return; + ItemStack hand = event.getPlayer().getInventory().getItemInMainHand(); + if (hand.getType() == Material.AIR || hand.getAmount() <= 0) { + return; + } + ItemStack old = npc.getTrait(Equipment.class).get(finalSlot); + if (old != null && old.getType() != Material.AIR) { + event.getPlayer().getWorld().dropItemNaturally(event.getPlayer().getLocation(), old); + } + ItemStack newStack = hand.clone(); + newStack.setAmount(1); + npc.getTrait(Equipment.class).set(finalSlot, newStack); + hand.setAmount(hand.getAmount() - 1); + event.getPlayer().getInventory().setItemInMainHand(hand); + } + }); + event.setCancelled(true); + } + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_AIR && Editor.hasEditor(event.getPlayer())) { + event.setUseItemInHand(Result.DENY); + } + } + + @EventHandler + public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { + if (!npc.isSpawned() || !event.getPlayer().equals(player) + || event.getHand() != org.bukkit.inventory.EquipmentSlot.HAND + || !npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getRightClicked()))) + return; + + Equipper equipper = EQUIPPERS.get(npc.getEntity().getType()); + if (equipper == null) { + equipper = new GenericEquipper(); + } + equipper.equip(event.getPlayer(), npc); + event.setCancelled(true); + } + + private static final Map EQUIPPERS = Maps.newEnumMap(EntityType.class); + + static { + EQUIPPERS.put(EntityType.PIG, new PigEquipper()); + EQUIPPERS.put(EntityType.SHEEP, new SheepEquipper()); + EQUIPPERS.put(EntityType.ENDERMAN, new EndermanEquipper()); + EQUIPPERS.put(EntityType.HORSE, new HorseEquipper()); + } +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/editor/Equipper.java b/main/java/net/citizensnpcs/editor/Equipper.java new file mode 100644 index 000000000..f24ff64ba --- /dev/null +++ b/main/java/net/citizensnpcs/editor/Equipper.java @@ -0,0 +1,9 @@ +package net.citizensnpcs.editor; + +import net.citizensnpcs.api.npc.NPC; + +import org.bukkit.entity.Player; + +public interface Equipper { + public void equip(Player equipper, NPC toEquip); +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/editor/GenericEquipper.java b/main/java/net/citizensnpcs/editor/GenericEquipper.java new file mode 100644 index 000000000..062fbd3fc --- /dev/null +++ b/main/java/net/citizensnpcs/editor/GenericEquipper.java @@ -0,0 +1,94 @@ +package net.citizensnpcs.editor; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.trait.Equipment; +import net.citizensnpcs.api.trait.trait.Equipment.EquipmentSlot; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.util.Messages; + +public class GenericEquipper implements Equipper { + @Override + public void equip(Player equipper, NPC toEquip) { + ItemStack hand = equipper.getInventory().getItemInMainHand(); + Equipment trait = toEquip.getTrait(Equipment.class); + EquipmentSlot slot = EquipmentSlot.HAND; + Material type = hand == null ? Material.AIR : hand.getType(); + // First, determine the slot to edit + switch (type) { + case SKULL_ITEM: + case PUMPKIN: + case JACK_O_LANTERN: + case LEATHER_HELMET: + case CHAINMAIL_HELMET: + case GOLD_HELMET: + case IRON_HELMET: + case DIAMOND_HELMET: + if (!equipper.isSneaking()) { + slot = EquipmentSlot.HELMET; + } + break; + case ELYTRA: + case LEATHER_CHESTPLATE: + case CHAINMAIL_CHESTPLATE: + case GOLD_CHESTPLATE: + case IRON_CHESTPLATE: + case DIAMOND_CHESTPLATE: + if (!equipper.isSneaking()) { + slot = EquipmentSlot.CHESTPLATE; + } + break; + case LEATHER_LEGGINGS: + case CHAINMAIL_LEGGINGS: + case GOLD_LEGGINGS: + case IRON_LEGGINGS: + case DIAMOND_LEGGINGS: + if (!equipper.isSneaking()) { + slot = EquipmentSlot.LEGGINGS; + } + break; + case LEATHER_BOOTS: + case CHAINMAIL_BOOTS: + case GOLD_BOOTS: + case IRON_BOOTS: + case DIAMOND_BOOTS: + if (!equipper.isSneaking()) { + slot = EquipmentSlot.BOOTS; + } + break; + case AIR: + if (equipper.isSneaking()) { + for (int i = 0; i < 6; i++) { + if (trait.get(i) != null && trait.get(i).getType() != Material.AIR) { + equipper.getWorld().dropItemNaturally(toEquip.getEntity().getLocation(), trait.get(i)); + trait.set(i, null); + } + } + Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_ALL_ITEMS_REMOVED, toEquip.getName()); + } else { + return; + } + break; + default: + break; + } + // Drop any previous equipment on the ground + ItemStack equippedItem = trait.get(slot); + if (equippedItem != null && equippedItem.getType() != Material.AIR) { + equipper.getWorld().dropItemNaturally(toEquip.getEntity().getLocation(), equippedItem); + } + + // Now edit the equipment based on the slot + if (type != Material.AIR) { + // Set the proper slot with one of the item + ItemStack clone = hand.clone(); + clone.setAmount(1); + trait.set(slot, clone); + hand.setAmount(hand.getAmount() - 1); + equipper.getInventory().setItemInMainHand(hand); + } + } +} diff --git a/main/java/net/citizensnpcs/editor/HorseEquipper.java b/main/java/net/citizensnpcs/editor/HorseEquipper.java new file mode 100644 index 000000000..03e5b3ed2 --- /dev/null +++ b/main/java/net/citizensnpcs/editor/HorseEquipper.java @@ -0,0 +1,15 @@ +package net.citizensnpcs.editor; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.util.NMS; + +import org.bukkit.entity.Horse; +import org.bukkit.entity.Player; + +public class HorseEquipper implements Equipper { + @Override + public void equip(Player equipper, NPC toEquip) { + Horse horse = (Horse) toEquip.getEntity(); + NMS.openHorseScreen(horse, equipper); + } +} diff --git a/main/java/net/citizensnpcs/editor/PigEquipper.java b/main/java/net/citizensnpcs/editor/PigEquipper.java new file mode 100644 index 000000000..e81b64691 --- /dev/null +++ b/main/java/net/citizensnpcs/editor/PigEquipper.java @@ -0,0 +1,31 @@ +package net.citizensnpcs.editor; + +import org.bukkit.Material; +import org.bukkit.entity.Pig; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.trait.Saddle; +import net.citizensnpcs.util.Messages; + +public class PigEquipper implements Equipper { + @Override + public void equip(Player equipper, NPC toEquip) { + ItemStack hand = equipper.getInventory().getItemInMainHand(); + Pig pig = (Pig) toEquip.getEntity(); + if (hand.getType() == Material.SADDLE) { + if (!pig.hasSaddle()) { + toEquip.getTrait(Saddle.class).toggle(); + hand.setAmount(0); + Messaging.sendTr(equipper, Messages.SADDLED_SET, toEquip.getName()); + } + } else if (pig.hasSaddle()) { + equipper.getWorld().dropItemNaturally(pig.getLocation(), new ItemStack(Material.SADDLE, 1)); + toEquip.getTrait(Saddle.class).toggle(); + Messaging.sendTr(equipper, Messages.SADDLED_STOPPED, toEquip.getName()); + } + equipper.getInventory().setItemInMainHand(hand); + } +} diff --git a/main/java/net/citizensnpcs/editor/SheepEquipper.java b/main/java/net/citizensnpcs/editor/SheepEquipper.java new file mode 100644 index 000000000..d747f686e --- /dev/null +++ b/main/java/net/citizensnpcs/editor/SheepEquipper.java @@ -0,0 +1,40 @@ +package net.citizensnpcs.editor; + +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.entity.Sheep; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.Dye; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.trait.SheepTrait; +import net.citizensnpcs.trait.WoolColor; +import net.citizensnpcs.util.Messages; + +public class SheepEquipper implements Equipper { + @Override + public void equip(Player equipper, NPC toEquip) { + ItemStack hand = equipper.getInventory().getItemInMainHand(); + Sheep sheep = (Sheep) toEquip.getEntity(); + if (hand.getType() == Material.SHEARS) { + Messaging.sendTr(equipper, toEquip.getTrait(SheepTrait.class).toggleSheared() ? Messages.SHEARED_SET + : Messages.SHEARED_STOPPED, toEquip.getName()); + } else if (hand.getType() == Material.INK_SACK) { + Dye dye = (Dye) hand.getData(); + if (sheep.getColor() == dye.getColor()) + return; + DyeColor color = dye.getColor(); + toEquip.getTrait(WoolColor.class).setColor(color); + Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_SHEEP_COLOURED, toEquip.getName(), + color.name().toLowerCase().replace("_", " ")); + + hand.setAmount(hand.getAmount() - 1); + } else { + toEquip.getTrait(WoolColor.class).setColor(DyeColor.WHITE); + Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_SHEEP_COLOURED, toEquip.getName(), "white"); + } + equipper.getInventory().setItemInMainHand(hand); + } +} diff --git a/main/java/net/citizensnpcs/npc/AbstractEntityController.java b/main/java/net/citizensnpcs/npc/AbstractEntityController.java new file mode 100644 index 000000000..62113e5aa --- /dev/null +++ b/main/java/net/citizensnpcs/npc/AbstractEntityController.java @@ -0,0 +1,38 @@ +package net.citizensnpcs.npc; + +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.util.NMS; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +public abstract class AbstractEntityController implements EntityController { + private Entity bukkitEntity; + + public AbstractEntityController() { + } + + public AbstractEntityController(Class clazz) { + NMS.registerEntityClass(clazz); + } + + protected abstract Entity createEntity(Location at, NPC npc); + + @Override + public Entity getBukkitEntity() { + return bukkitEntity; + } + + @Override + public void remove() { + if (bukkitEntity == null) + return; + bukkitEntity.remove(); + bukkitEntity = null; + } + + @Override + public void spawn(Location at, NPC npc) { + bukkitEntity = createEntity(at, npc); + } +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/npc/CitizensNPC.java b/main/java/net/citizensnpcs/npc/CitizensNPC.java new file mode 100644 index 000000000..33b9f1caf --- /dev/null +++ b/main/java/net/citizensnpcs/npc/CitizensNPC.java @@ -0,0 +1,345 @@ +package net.citizensnpcs.npc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +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.entity.CreatureSpawnEvent.SpawnReason; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.scoreboard.Team; +import org.bukkit.scoreboard.Team.Option; +import org.bukkit.scoreboard.Team.OptionStatus; + +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; + +import net.citizensnpcs.NPCNeedsRespawnEvent; +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.ai.Navigator; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.event.NPCDespawnEvent; +import net.citizensnpcs.api.event.NPCSpawnEvent; +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.CitizensBlockBreaker; +import net.citizensnpcs.npc.ai.CitizensNavigator; +import net.citizensnpcs.npc.skin.SkinnableEntity; +import net.citizensnpcs.trait.CurrentLocation; +import net.citizensnpcs.util.Messages; +import net.citizensnpcs.util.NMS; +import net.citizensnpcs.util.Util; + +public class CitizensNPC extends AbstractNPC { + private EntityController entityController; + private final CitizensNavigator navigator = new CitizensNavigator(this); + private int updateCounter = 0; + + public CitizensNPC(UUID uuid, int id, String name, EntityController entityController, NPCRegistry registry) { + super(uuid, id, name, registry); + Preconditions.checkNotNull(entityController); + this.entityController = entityController; + } + + @Override + public boolean despawn(DespawnReason reason) { + if (!isSpawned() && reason != DespawnReason.DEATH) { + Messaging.debug("Tried to despawn", getId(), "while already despawned."); + if (reason == DespawnReason.REMOVAL) { + Bukkit.getPluginManager().callEvent(new NPCDespawnEvent(this, reason)); + } + if (reason == DespawnReason.RELOAD) { + unloadEvents(); + } + return false; + } + NPCDespawnEvent event = new NPCDespawnEvent(this, reason); + if (reason == DespawnReason.CHUNK_UNLOAD) { + event.setCancelled(Setting.KEEP_CHUNKS_LOADED.asBoolean()); + } + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) { + getEntity().getLocation().getChunk(); + Messaging.debug("Couldn't despawn", getId(), "due to despawn event cancellation. Force loaded chunk.", + getEntity().isValid()); + return false; + } + boolean keepSelected = getTrait(Spawned.class).shouldSpawn(); + if (!keepSelected) { + data().remove("selectors"); + } + navigator.onDespawn(); + if (reason == DespawnReason.RELOAD) { + unloadEvents(); + } + for (Trait trait : new ArrayList(traits.values())) { + trait.onDespawn(); + } + Messaging.debug("Despawned", getId(), "DespawnReason.", reason); + entityController.remove(); + + return true; + } + + @Override + public void faceLocation(Location location) { + if (!isSpawned()) + return; + Util.faceLocation(getEntity(), location); + } + + @Override + public BlockBreaker getBlockBreaker(Block targetBlock, BlockBreakerConfiguration config) { + return new CitizensBlockBreaker(getEntity(), targetBlock, config); + } + + @Override + public Entity getEntity() { + return entityController == null ? null : entityController.getBukkitEntity(); + } + + @Override + public Navigator getNavigator() { + return navigator; + } + + @Override + public Location getStoredLocation() { + return isSpawned() ? getEntity().getLocation() : getTrait(CurrentLocation.class).getLocation(); + } + + @Override + public boolean isFlyable() { + updateFlyableState(); + return super.isFlyable(); + } + + @Override + public void load(final DataKey root) { + super.load(root); + + // Spawn the NPC + CurrentLocation spawnLocation = getTrait(CurrentLocation.class); + if (getTrait(Spawned.class).shouldSpawn() && spawnLocation.getLocation() != null) { + spawn(spawnLocation.getLocation()); + } + if (getTrait(Spawned.class).shouldSpawn() && spawnLocation.getLocation() == null) { + Messaging.debug("Tried to spawn", getId(), "on load but world was null"); + } + + navigator.load(root.getRelative("navigator")); + } + + @Override + public void save(DataKey root) { + super.save(root); + if (!data().get(NPC.SHOULD_SAVE_METADATA, true)) + return; + navigator.save(root.getRelative("navigator")); + } + + @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 = isSpawned(); + Location prev = null; + if (wasSpawned) { + prev = getEntity().getLocation(); + despawn(DespawnReason.PENDING_RESPAWN); + } + entityController = newController; + if (wasSpawned) { + spawn(prev); + } + } + + @Override + public void setFlyable(boolean flyable) { + super.setFlyable(flyable); + updateFlyableState(); + } + + @Override + public boolean spawn(Location at) { + Preconditions.checkNotNull(at, "location cannot be null"); + if (isSpawned()) { + Messaging.debug("Tried to spawn", getId(), "while already spawned."); + return false; + } + data().get(NPC.DEFAULT_PROTECTED_METADATA, true); + + at = at.clone(); + getTrait(CurrentLocation.class).setLocation(at); + + entityController.spawn(at, this); + + getEntity().setMetadata(NPC_METADATA_MARKER, new FixedMetadataValue(CitizensAPI.getPlugin(), true)); + + boolean couldSpawn = !Util.isLoaded(at) ? false : NMS.addEntityToWorld(getEntity(), SpawnReason.CUSTOM); + + // send skin packets, if applicable, before other NMS packets are sent + if (couldSpawn) { + SkinnableEntity skinnable = getEntity() instanceof SkinnableEntity ? ((SkinnableEntity) getEntity()) : null; + if (skinnable != null) { + skinnable.getSkinTracker().onSpawnNPC(); + } + } + + getEntity().teleport(at); + + if (!couldSpawn) { + Messaging.debug("Retrying spawn of", getId(), "later due to chunk being unloaded.", + Util.isLoaded(at) ? "Util.isLoaded true" : "Util.isLoaded false"); + // we need to wait for a chunk load before trying to spawn + entityController.remove(); + Bukkit.getPluginManager().callEvent(new NPCNeedsRespawnEvent(this, at)); + return false; + } + + NMS.setHeadYaw(getEntity(), at.getYaw()); + + // Set the spawned state + getTrait(CurrentLocation.class).setLocation(at); + getTrait(Spawned.class).setSpawned(true); + + NPCSpawnEvent spawnEvent = new NPCSpawnEvent(this, at); + Bukkit.getPluginManager().callEvent(spawnEvent); + + if (spawnEvent.isCancelled()) { + entityController.remove(); + Messaging.debug("Couldn't spawn", getId(), "due to event cancellation."); + return false; + } + + navigator.onSpawn(); + + // Modify NPC using traits after the entity has been created + Collection onSpawn = traits.values(); + + // work around traits modifying the map during this iteration. + for (Trait trait : onSpawn.toArray(new Trait[onSpawn.size()])) { + try { + trait.onSpawn(); + } catch (Throwable ex) { + Messaging.severeTr(Messages.TRAIT_ONSPAWN_FAILED, trait.getName(), getId()); + ex.printStackTrace(); + } + } + + if (getEntity() instanceof LivingEntity) { + LivingEntity entity = (LivingEntity) getEntity(); + entity.setRemoveWhenFarAway(false); + + if (NMS.getStepHeight(entity) < 1) { + NMS.setStepHeight(entity, 1); + } + if (getEntity() instanceof Player) { + NMS.replaceTrackerEntry((Player) getEntity()); + } + } + + return true; + } + + @Override + public void update() { + try { + super.update(); + if (!isSpawned()) + return; + if (data().get(NPC.SWIMMING_METADATA, true)) { + NMS.trySwim(getEntity()); + } + navigator.run(); + + getEntity().setGlowing(data().get(NPC.GLOWING_METADATA, false)); + if (!getNavigator().isNavigating() && updateCounter++ > Setting.PACKET_UPDATE_DELAY.asInt()) { + updateCounter = 0; + if (getEntity() instanceof LivingEntity) { + OptionStatus nameVisibility = OptionStatus.NEVER; + if (!getEntity().isCustomNameVisible()) { + getEntity().setCustomName(""); + } else { + nameVisibility = OptionStatus.ALWAYS; + getEntity().setCustomName(getFullName()); + } + String teamName = data().get(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA, ""); + if (getEntity() instanceof Player + && Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName) != null) { + Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName); + if (!Setting.USE_SCOREBOARD_TEAMS.asBoolean()) { + team.unregister(); + data().remove(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA); + } else { + team.setOption(Option.NAME_TAG_VISIBILITY, nameVisibility); + if (data().has(NPC.GLOWING_COLOR_METADATA)) { + if (team.getPrefix() == null || team.getPrefix().length() == 0 + || (data().has("previous-glowing-color") + && !team.getPrefix().equals(data().get("previous-glowing-color")))) { + team.setPrefix(ChatColor.valueOf(data(). get(NPC.GLOWING_COLOR_METADATA)) + .toString()); + data().set("previous-glowing-color", team.getPrefix()); + } + } + } + } + } + Player player = getEntity() instanceof Player ? (Player) getEntity() : null; + NMS.sendPositionUpdate(player, getEntity(), getStoredLocation()); + } + + if (getEntity() instanceof LivingEntity) { + boolean nameplateVisible = data().get(NPC.NAMEPLATE_VISIBLE_METADATA, true); + ((LivingEntity) getEntity()).setCustomNameVisible(nameplateVisible); + + if (data().get(NPC.DEFAULT_PROTECTED_METADATA, true)) { + NMS.setKnockbackResistance((LivingEntity) getEntity(), 1D); + } else { + NMS.setKnockbackResistance((LivingEntity) getEntity(), 0D); + } + } + + if (data().has(NPC.SILENT_METADATA)) { + getEntity().setSilent(Boolean.parseBoolean(data().get(NPC.SILENT_METADATA).toString())); + } + } catch (Exception ex) { + Throwable error = Throwables.getRootCause(ex); + Messaging.logTr(Messages.EXCEPTION_UPDATING_NPC, getId(), error.getMessage()); + error.printStackTrace(); + } + } + + private void updateFlyableState() { + EntityType type = isSpawned() ? getEntity().getType() : getTrait(MobType.class).getType(); + if (type == null) + return; + if (Util.isAlwaysFlyable(type)) { + data().setPersistent(NPC.FLYABLE_METADATA, true); + } + } + + private static final String NPC_METADATA_MARKER = "NPC"; +} diff --git a/main/java/net/citizensnpcs/npc/CitizensNPCRegistry.java b/main/java/net/citizensnpcs/npc/CitizensNPCRegistry.java new file mode 100644 index 000000000..49cea2d80 --- /dev/null +++ b/main/java/net/citizensnpcs/npc/CitizensNPCRegistry.java @@ -0,0 +1,251 @@ +package net.citizensnpcs.npc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; + +import gnu.trove.map.hash.TIntObjectHashMap; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.event.DespawnReason; +import net.citizensnpcs.api.event.NPCCreateEvent; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.npc.NPCDataStore; +import net.citizensnpcs.api.npc.NPCRegistry; +import net.citizensnpcs.api.trait.Trait; +import net.citizensnpcs.npc.ai.NPCHolder; +import net.citizensnpcs.trait.ArmorStandTrait; +import net.citizensnpcs.util.NMS; + +public class CitizensNPCRegistry implements NPCRegistry { + private final NPCCollection npcs = TROVE_EXISTS ? new TroveNPCCollection() : new MapNPCCollection(); + private final NPCDataStore saves; + + public CitizensNPCRegistry(NPCDataStore store) { + saves = store; + } + + @Override + public NPC createNPC(EntityType type, String name) { + return createNPC(type, UUID.randomUUID(), generateUniqueId(), name); + } + + @Override + public NPC createNPC(EntityType type, UUID uuid, int id, String name) { + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(type, "type cannot be null"); + CitizensNPC npc = getByType(type, uuid, id, name); + + if (npc == null) + throw new IllegalStateException("Could not create NPC."); + npcs.put(npc.getId(), npc); + Bukkit.getPluginManager().callEvent(new NPCCreateEvent(npc)); + if (type == EntityType.ARMOR_STAND && !npc.hasTrait(ArmorStandTrait.class)) { + npc.addTrait(ArmorStandTrait.class); + } + return npc; + } + + @Override + public void deregister(NPC npc) { + npcs.remove(npc); + if (saves != null) { + saves.clearData(npc); + } + npc.despawn(DespawnReason.REMOVAL); + } + + @Override + public void deregisterAll() { + Iterator itr = iterator(); + while (itr.hasNext()) { + NPC npc = itr.next(); + itr.remove(); + npc.despawn(DespawnReason.REMOVAL); + for (Trait t : npc.getTraits()) { + t.onRemove(); + } + if (saves != null) { + saves.clearData(npc); + } + } + } + + private int generateUniqueId() { + return saves.createUniqueNPCId(this); + } + + @Override + public NPC getById(int id) { + if (id < 0) + throw new IllegalArgumentException("invalid id"); + return npcs.get(id); + } + + private CitizensNPC getByType(EntityType type, UUID uuid, int id, String name) { + return new CitizensNPC(uuid, id, name, EntityControllers.createForType(type), this); + } + + @Override + public NPC getByUniqueId(UUID uuid) { + return npcs.get(uuid); + } + + @Override + public NPC getByUniqueIdGlobal(UUID uuid) { + NPC npc = getByUniqueId(uuid); + if (npc != null) + return npc; + for (NPCRegistry registry : CitizensAPI.getNPCRegistries()) { + if (registry != this) { + NPC other = registry.getByUniqueId(uuid); + if (other != null) { + return other; + } + } + } + return null; + } + + @Override + public NPC getNPC(Entity entity) { + if (entity == null) + return null; + if (entity instanceof NPCHolder) + return ((NPCHolder) entity).getNPC(); + return NMS.getNPC(entity); + } + + @Override + public boolean isNPC(Entity entity) { + return getNPC(entity) != null; + } + + @Override + public Iterator iterator() { + return npcs.iterator(); + } + + @Override + public Iterable sorted() { + return npcs.sorted(); + } + + public static class MapNPCCollection implements NPCCollection { + private final Map npcs = Maps.newHashMap(); + private final Map uniqueNPCs = Maps.newHashMap(); + + @Override + public NPC get(int id) { + return npcs.get(id); + } + + @Override + public NPC get(UUID uuid) { + return uniqueNPCs.get(uuid); + } + + @Override + public Iterator iterator() { + return npcs.values().iterator(); + } + + @Override + public void put(int id, NPC npc) { + npcs.put(id, npc); + uniqueNPCs.put(npc.getUniqueId(), npc); + } + + @Override + public void remove(NPC npc) { + npcs.remove(npc.getId()); + uniqueNPCs.remove(npc.getUniqueId()); + } + + @Override + public Iterable sorted() { + List vals = new ArrayList(npcs.values()); + Collections.sort(vals, NPC_COMPARATOR); + return vals; + } + } + + public static interface NPCCollection extends Iterable { + public NPC get(int id); + + public NPC get(UUID uuid); + + public void put(int id, NPC npc); + + public void remove(NPC npc); + + public Iterable sorted(); + } + + public static class TroveNPCCollection implements NPCCollection { + private final TIntObjectHashMap npcs = new TIntObjectHashMap(); + private final Map uniqueNPCs = Maps.newHashMap(); + + @Override + public NPC get(int id) { + return npcs.get(id); + } + + @Override + public NPC get(UUID uuid) { + return uniqueNPCs.get(uuid); + } + + @Override + public Iterator iterator() { + return npcs.valueCollection().iterator(); + } + + @Override + public void put(int id, NPC npc) { + npcs.put(id, npc); + uniqueNPCs.put(npc.getUniqueId(), npc); + } + + @Override + public void remove(NPC npc) { + npcs.remove(npc.getId()); + uniqueNPCs.remove(npc.getUniqueId()); + } + + @Override + public Iterable sorted() { + List vals = new ArrayList(npcs.valueCollection()); + Collections.sort(vals, NPC_COMPARATOR); + return vals; + } + } + + private static final Comparator NPC_COMPARATOR = new Comparator() { + @Override + public int compare(NPC o1, NPC o2) { + return o1.getId() - o2.getId(); + } + }; + + private static boolean TROVE_EXISTS = false; + + static { + // allow trove dependency to be optional for debugging purposes + try { + Class.forName("gnu.trove.map.hash.TIntObjectHashMap").newInstance(); + TROVE_EXISTS = true; + } catch (Exception e) { + } + } +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/npc/CitizensTraitFactory.java b/main/java/net/citizensnpcs/npc/CitizensTraitFactory.java new file mode 100644 index 000000000..8a787de23 --- /dev/null +++ b/main/java/net/citizensnpcs/npc/CitizensTraitFactory.java @@ -0,0 +1,177 @@ +package net.citizensnpcs.npc; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import net.citizensnpcs.Metrics; +import net.citizensnpcs.Metrics.Graph; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.Trait; +import net.citizensnpcs.api.trait.TraitFactory; +import net.citizensnpcs.api.trait.TraitInfo; +import net.citizensnpcs.api.trait.trait.Equipment; +import net.citizensnpcs.api.trait.trait.Inventory; +import net.citizensnpcs.api.trait.trait.MobType; +import net.citizensnpcs.api.trait.trait.Owner; +import net.citizensnpcs.api.trait.trait.Spawned; +import net.citizensnpcs.api.trait.trait.Speech; +import net.citizensnpcs.trait.Age; +import net.citizensnpcs.trait.Anchors; +import net.citizensnpcs.trait.ArmorStandTrait; +import net.citizensnpcs.trait.BossBarTrait; +import net.citizensnpcs.trait.Controllable; +import net.citizensnpcs.trait.CurrentLocation; +import net.citizensnpcs.trait.Gravity; +import net.citizensnpcs.trait.HorseModifiers; +import net.citizensnpcs.trait.LookClose; +import net.citizensnpcs.trait.MountTrait; +import net.citizensnpcs.trait.NPCSkeletonType; +import net.citizensnpcs.trait.OcelotModifiers; +import net.citizensnpcs.trait.Poses; +import net.citizensnpcs.trait.Powered; +import net.citizensnpcs.trait.RabbitType; +import net.citizensnpcs.trait.Saddle; +import net.citizensnpcs.trait.ScriptTrait; +import net.citizensnpcs.trait.SheepTrait; +import net.citizensnpcs.trait.SkinLayers; +import net.citizensnpcs.trait.SlimeSize; +import net.citizensnpcs.trait.VillagerProfession; +import net.citizensnpcs.trait.WitherTrait; +import net.citizensnpcs.trait.WolfModifiers; +import net.citizensnpcs.trait.WoolColor; +import net.citizensnpcs.trait.ZombieModifier; +import net.citizensnpcs.trait.text.Text; +import net.citizensnpcs.trait.waypoint.Waypoints; + +public class CitizensTraitFactory implements TraitFactory { + private final List defaultTraits = Lists.newArrayList(); + private final Map registered = Maps.newHashMap(); + + public CitizensTraitFactory() { + registerTrait(TraitInfo.create(Age.class)); + registerTrait(TraitInfo.create(ArmorStandTrait.class)); + registerTrait(TraitInfo.create(Anchors.class)); + registerTrait(TraitInfo.create(BossBarTrait.class)); + registerTrait(TraitInfo.create(Controllable.class)); + registerTrait(TraitInfo.create(Equipment.class)); + registerTrait(TraitInfo.create(Gravity.class)); + registerTrait(TraitInfo.create(HorseModifiers.class)); + registerTrait(TraitInfo.create(Inventory.class)); + registerTrait(TraitInfo.create(CurrentLocation.class)); + registerTrait(TraitInfo.create(LookClose.class)); + registerTrait(TraitInfo.create(OcelotModifiers.class)); + registerTrait(TraitInfo.create(Owner.class)); + registerTrait(TraitInfo.create(Poses.class)); + registerTrait(TraitInfo.create(Powered.class)); + registerTrait(TraitInfo.create(RabbitType.class)); + registerTrait(TraitInfo.create(Saddle.class)); + registerTrait(TraitInfo.create(ScriptTrait.class)); + registerTrait(TraitInfo.create(SheepTrait.class)); + registerTrait(TraitInfo.create(SkinLayers.class)); + registerTrait(TraitInfo.create(MountTrait.class)); + registerTrait(TraitInfo.create(NPCSkeletonType.class)); + registerTrait(TraitInfo.create(SlimeSize.class)); + registerTrait(TraitInfo.create(Spawned.class)); + registerTrait(TraitInfo.create(Speech.class)); + registerTrait(TraitInfo.create(Text.class)); + registerTrait(TraitInfo.create(MobType.class).asDefaultTrait()); + registerTrait(TraitInfo.create(Waypoints.class)); + registerTrait(TraitInfo.create(WitherTrait.class)); + registerTrait(TraitInfo.create(WoolColor.class)); + registerTrait(TraitInfo.create(WolfModifiers.class)); + registerTrait(TraitInfo.create(VillagerProfession.class)); + registerTrait(TraitInfo.create(ZombieModifier.class)); + + for (String trait : registered.keySet()) { + INTERNAL_TRAITS.add(trait); + } + } + + @Override + public void addDefaultTraits(NPC npc) { + for (TraitInfo info : defaultTraits) { + npc.addTrait(create(info)); + } + } + + public void addPlotters(Graph graph) { + for (Map.Entry entry : registered.entrySet()) { + if (INTERNAL_TRAITS.contains(entry.getKey())) + continue; + final Class traitClass = entry.getValue().getTraitClass(); + graph.addPlotter(new Metrics.Plotter(entry.getKey()) { + @Override + public int getValue() { + int numberUsingTrait = 0; + for (NPC npc : CitizensAPI.getNPCRegistry()) { + if (npc.hasTrait(traitClass)) + ++numberUsingTrait; + } + return numberUsingTrait; + } + }); + } + } + + private T create(TraitInfo info) { + return info.tryCreateInstance(); + } + + @Override + public void deregisterTrait(TraitInfo info) { + Preconditions.checkNotNull(info, "info cannot be null"); + registered.remove(info.getTraitName()); + } + + @Override + public T getTrait(Class clazz) { + for (TraitInfo entry : registered.values()) { + if (clazz == entry.getTraitClass()) { + return create(entry); + } + } + return null; + } + + @Override + @SuppressWarnings("unchecked") + public T getTrait(String name) { + TraitInfo info = registered.get(name.toLowerCase()); + if (info == null) + return null; + return (T) create(info); + } + + @Override + public Class getTraitClass(String name) { + TraitInfo info = registered.get(name.toLowerCase()); + return info == null ? null : info.getTraitClass(); + } + + @Override + public boolean isInternalTrait(Trait trait) { + return INTERNAL_TRAITS.contains(trait.getName()); + } + + @Override + public void registerTrait(TraitInfo info) { + Preconditions.checkNotNull(info, "info cannot be null"); + if (registered.containsKey(info.getTraitName())) { + System.out.println(info.getTraitClass()); + throw new IllegalArgumentException("trait name already registered"); + } + registered.put(info.getTraitName(), info); + if (info.isDefaultTrait()) { + defaultTraits.add(info); + } + } + + private static final Set INTERNAL_TRAITS = Sets.newHashSet(); +} \ No newline at end of file diff --git a/main/java/net/citizensnpcs/npc/EntityController.java b/main/java/net/citizensnpcs/npc/EntityController.java new file mode 100644 index 000000000..65259c6c8 --- /dev/null +++ b/main/java/net/citizensnpcs/npc/EntityController.java @@ -0,0 +1,14 @@ +package net.citizensnpcs.npc; + +import net.citizensnpcs.api.npc.NPC; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +public interface EntityController { + Entity getBukkitEntity(); + + void remove(); + + void spawn(Location at, NPC npc); +} diff --git a/main/java/net/citizensnpcs/npc/EntityControllers.java b/main/java/net/citizensnpcs/npc/EntityControllers.java new file mode 100644 index 000000000..289a469c3 --- /dev/null +++ b/main/java/net/citizensnpcs/npc/EntityControllers.java @@ -0,0 +1,32 @@ +package net.citizensnpcs.npc; + +import java.util.Map; + +import org.bukkit.entity.EntityType; + +import com.google.common.base.Throwables; +import com.google.common.collect.Maps; + +public class EntityControllers { + public static boolean controllerExistsForType(EntityType type) { + return TYPES.containsKey(type); + } + + public static EntityController createForType(EntityType type) { + Class controllerClass = TYPES.get(type); + if (controllerClass == null) + throw new IllegalArgumentException("Unknown EntityType: " + type); + try { + return controllerClass.newInstance(); + } catch (Throwable ex) { + Throwables.getRootCause(ex).printStackTrace(); + return null; + } + } + + public static void setEntityControllerForType(EntityType type, Class controller) { + TYPES.put(type, controller); + } + + private static final Map> TYPES = Maps.newEnumMap(EntityType.class); +} diff --git a/main/java/net/citizensnpcs/npc/NPCSelector.java b/main/java/net/citizensnpcs/npc/NPCSelector.java new file mode 100644 index 000000000..bddd5ecdd --- /dev/null +++ b/main/java/net/citizensnpcs/npc/NPCSelector.java @@ -0,0 +1,148 @@ +package net.citizensnpcs.npc; + +import java.util.List; +import java.util.UUID; + +import net.citizensnpcs.Settings.Setting; +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.event.NPCRemoveEvent; +import net.citizensnpcs.api.event.NPCRightClickEvent; +import net.citizensnpcs.api.event.NPCSelectEvent; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.trait.Owner; +import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.editor.Editor; +import net.citizensnpcs.util.Util; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.metadata.MetadataValue; +import org.bukkit.metadata.Metadatable; +import org.bukkit.plugin.Plugin; + +import com.google.common.collect.Lists; + +public class NPCSelector implements Listener, net.citizensnpcs.api.npc.NPCSelector { + private UUID consoleSelectedNPC; + private final Plugin plugin; + + public NPCSelector(Plugin plugin) { + this.plugin = plugin; + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @Override + public NPC getSelected(CommandSender sender) { + if (sender instanceof Player) { + return getSelectedFromMetadatable((Player) sender); + } else if (sender instanceof BlockCommandSender) { + return getSelectedFromMetadatable(((BlockCommandSender) sender).getBlock()); + } else if (sender instanceof ConsoleCommandSender) { + if (consoleSelectedNPC == null) + return null; + return CitizensAPI.getNPCRegistry().getByUniqueIdGlobal(consoleSelectedNPC); + } + return null; + } + + private NPC getSelectedFromMetadatable(Metadatable sender) { + List metadata = sender.getMetadata("selected"); + if (metadata.size() == 0) + return null; + return CitizensAPI.getNPCRegistry().getByUniqueIdGlobal((UUID) metadata.get(0).value()); + } + + @EventHandler + public void onNPCRemove(NPCRemoveEvent event) { + NPC npc = event.getNPC(); + List selectors = npc.data().get("selectors"); + if (selectors == null) + return; + for (String value : selectors) { + if (value.equals("console")) { + consoleSelectedNPC = null; + } else if (value.startsWith("@")) { + String[] parts = value.substring(1, value.length()).split(":"); + World world = Bukkit.getWorld(parts[0]); + if (world != null) { + Block block = world.getBlockAt(Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), + Integer.parseInt(parts[3])); + removeMetadata(block); + } + } else { + Player search = Bukkit.getPlayerExact(value); + removeMetadata(search); + } + } + npc.data().remove("selectors"); + } + + @EventHandler + public void onNPCRightClick(NPCRightClickEvent event) { + Player player = event.getClicker(); + NPC npc = event.getNPC(); + List selected = player.getMetadata("selected"); + if (selected == null || selected.size() == 0 || selected.get(0).asInt() != npc.getId()) { + if (Util.matchesItemInHand(player, Setting.SELECTION_ITEM.asString()) + && npc.getTrait(Owner.class).isOwnedBy(player)) { + player.removeMetadata("selected", plugin); + select(player, npc); + Messaging.sendWithNPC(player, Setting.SELECTION_MESSAGE.asString(), npc); + if (!Setting.QUICK_SELECT.asBoolean()) + return; + } + } + } + + private void removeMetadata(Metadatable metadatable) { + if (metadatable != null) { + metadatable.removeMetadata("selected", plugin); + } + } + + public void select(CommandSender sender, NPC npc) { + // Remove existing selection if any + List selectors = npc.data().get("selectors"); + if (selectors == null) { + selectors = Lists.newArrayList(); + npc.data().set("selectors", selectors); + } + if (sender instanceof Player) { + Player player = (Player) sender; + setMetadata(npc, player); + selectors.add(sender.getName()); + + // Remove editor if the player has one + Editor.leave(player); + } else if (sender instanceof BlockCommandSender) { + Block block = ((BlockCommandSender) sender).getBlock(); + setMetadata(npc, block); + selectors.add(toName(block)); + } else if (sender instanceof ConsoleCommandSender) { + consoleSelectedNPC = npc.getUniqueId(); + selectors.add("console"); + } + + Bukkit.getPluginManager().callEvent(new NPCSelectEvent(npc, sender)); + } + + private void setMetadata(NPC npc, Metadatable metadatable) { + if (metadatable.hasMetadata("selected")) { + metadatable.removeMetadata("selected", plugin); + } + metadatable.setMetadata("selected", new FixedMetadataValue(plugin, npc.getUniqueId())); + } + + private String toName(Block block) { + return '@' + block.getWorld().getName() + ":" + Integer.toString(block.getX()) + ":" + + Integer.toString(block.getY()) + ":" + Integer.toString(block.getZ()); + } +} diff --git a/main/java/net/citizensnpcs/npc/Template.java b/main/java/net/citizensnpcs/npc/Template.java new file mode 100644 index 000000000..762628c55 --- /dev/null +++ b/main/java/net/citizensnpcs/npc/Template.java @@ -0,0 +1,138 @@ +package net.citizensnpcs.npc; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.util.DataKey; +import net.citizensnpcs.api.util.MemoryDataKey; +import net.citizensnpcs.api.util.YamlStorage; +import net.citizensnpcs.api.util.YamlStorage.YamlKey; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +public class Template { + private final String name; + private final boolean override; + private final Map replacements; + + private Template(String name, Map replacements, boolean override) { + this.replacements = replacements; + this.override = override; + this.name = name; + } + + @SuppressWarnings("unchecked") + public void apply(NPC npc) { + MemoryDataKey memoryKey = new MemoryDataKey(); + npc.save(memoryKey); + List queue = Lists.newArrayList(new Node("", replacements)); + for (int i = 0; i < queue.size(); i++) { + Node node = queue.get(i); + for (Entry entry : node.map.entrySet()) { + String fullKey = node.headKey.isEmpty() ? entry.getKey() : node.headKey + '.' + entry.getKey(); + if (entry.getValue() instanceof Map) { + queue.add(new Node(fullKey, (Map) entry.getValue())); + continue; + } + boolean overwrite = memoryKey.keyExists(fullKey) | override; + if (!overwrite || fullKey.equals("uuid")) + continue; + memoryKey.setRaw(fullKey, entry.getValue()); + } + } + npc.load(memoryKey); + } + + public void delete() { + templates.load(); + templates.getKey("").removeKey(name); + templates.save(); + } + + public String getName() { + return name; + } + + private static class Node { + String headKey; + Map map; + + private Node(String headKey, Map map) { + this.headKey = headKey; + this.map = map; + } + } + + public static class TemplateBuilder { + private final String name; + private boolean override; + private final Map replacements = Maps.newHashMap(); + + private TemplateBuilder(String name) { + this.name = name; + } + + public Template buildAndSave() { + save(); + return new Template(name, replacements, override); + } + + public TemplateBuilder from(NPC npc) { + replacements.clear(); + MemoryDataKey key = new MemoryDataKey(); + ((CitizensNPC) npc).save(key); + replacements.putAll(key.getValuesDeep()); + return this; + } + + public TemplateBuilder override(boolean override) { + this.override = override; + return this; + } + + public void save() { + templates.load(); + DataKey root = templates.getKey(name); + root.setBoolean("override", override); + root.setRaw("replacements", replacements); + templates.save(); + } + + public static TemplateBuilder create(String name) { + return new TemplateBuilder(name); + } + } + + public static Iterable