First pass at 1.11

This commit is contained in:
fullwall 2016-11-17 15:53:41 +08:00
parent 0c833e2566
commit 0189157400
232 changed files with 35683 additions and 11 deletions

13
dist/pom.xml vendored
View File

@ -4,7 +4,7 @@
<parent>
<groupId>net.citizensnpcs</groupId>
<artifactId>citizens-parent</artifactId>
<version>2.0.20-SNAPSHOT</version>
<version>2.0.21-SNAPSHOT</version>
</parent>
<artifactId>citizens</artifactId>
<packaging>pom</packaging>
@ -40,13 +40,20 @@
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>citizens-v1_10_R1</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>citizens-v1_11_R1</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<String, NPCRegistry> 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<NPC> 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<CommandInfo> 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<NPCRegistry> getNPCRegistries() {
return new Iterable<NPCRegistry>() {
@Override
public Iterator<NPCRegistry> iterator() {
return new Iterator<NPCRegistry>() {
Iterator<NPCRegistry> 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<Economy> 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;
}
}

View File

@ -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<String, NPCRegistry> registries;
private final SkinUpdateTracker skinUpdateTracker;
private final ListMultimap<ChunkCoord, NPC> toRespawn = ArrayListMultimap.create();
EventListen(Map<String, NPCRegistry> 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<NPC> getAllNPCs() {
return Iterables.filter(Iterables.<NPC> 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<String, Collection<Property>> 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<NPC> 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;
}
}
}

View File

@ -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<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
/**
* 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<Graph> 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<Graph> 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<Plotter> plotters = new LinkedHashSet<Plotter>();
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 <b>unmodifiable</b> set of the plotter objects in the graph
*
* @return an unmodifiable {@link java.util.Set} of the plotter objects
*/
public Set<Plotter> 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;
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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", "[<npc>]: <text>"),
CHAT_FORMAT_TO_BYSTANDERS("npc.chat.format.with-target-to-bystanders", "[<npc>] -> [<target>]: <text>"),
CHAT_FORMAT_TO_TARGET("npc.chat.format.to-target", "[<npc>] -> You: <text>"),
CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS("npc.chat.format.with-targets-to-bystanders",
"[<npc>] -> [<targets>]: <text>"),
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",
"<target>|, <target>| & <target>| & 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 <npc>!") {
@Override
public void loadFromKey(DataKey root) {
List<String> list = new ArrayList<String>();
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", "<e>"),
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", "<a>"),
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", "<b>You selected <a><npc><b>!"),
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<String> asList() {
if (!(value instanceof List)) {
value = Lists.newArrayList(value);
}
return (List<String>) 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);
}
}
}

View File

@ -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("<e>Citizens v" + plugin.getDescription().getVersion()));
Messaging.send(sender, " <7>-- <c>Written by fullwall and aPunch");
Messaging.send(sender, " <7>-- <c>Source Code: http://github.com/CitizensDev");
Messaging.send(sender, " <7>-- <c>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);
}
}

View File

@ -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));
}
}

View File

@ -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<NPC> choices;
public NPCCommandSelector(Callback callback, List<NPC> 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<NPC> 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<NPC> 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;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<Integer> ids = Lists.newArrayList();
for (String id : Splitter.on(',').trimResults().split(joined)) {
int parsed = Integer.parseInt(id);
ids.add(parsed);
}
Iterable<NPC> transformed = Iterables.transform(ids, new Function<Integer, NPC>() {
@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());
}
}
}

View File

@ -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<String> added = Lists.newArrayList();
List<String> 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<? extends Trait> 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<? extends Trait> 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<? extends Trait> 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<String> removed = Lists.newArrayList();
List<String> 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<? extends Trait> 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<String> added = Lists.newArrayList();
List<String> removed = Lists.newArrayList();
List<String> 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<? extends Trait> 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));
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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<String, Editor> entry : EDITING.entrySet()) {
entry.getValue().end();
HandlerList.unregisterAll(entry.getValue());
}
EDITING.clear();
}
private static final Map<String, Editor> EDITING = new HashMap<String, Editor>();
}

View File

@ -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);
}
}

View File

@ -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<EntityType, Equipper> 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());
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<Trait>(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<Trait> 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().<String> 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";
}

View File

@ -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<NPC> 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<NPC> iterator() {
return npcs.iterator();
}
@Override
public Iterable<NPC> sorted() {
return npcs.sorted();
}
public static class MapNPCCollection implements NPCCollection {
private final Map<Integer, NPC> npcs = Maps.newHashMap();
private final Map<UUID, NPC> 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<NPC> 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<NPC> sorted() {
List<NPC> vals = new ArrayList<NPC>(npcs.values());
Collections.sort(vals, NPC_COMPARATOR);
return vals;
}
}
public static interface NPCCollection extends Iterable<NPC> {
public NPC get(int id);
public NPC get(UUID uuid);
public void put(int id, NPC npc);
public void remove(NPC npc);
public Iterable<NPC> sorted();
}
public static class TroveNPCCollection implements NPCCollection {
private final TIntObjectHashMap<NPC> npcs = new TIntObjectHashMap<NPC>();
private final Map<UUID, NPC> 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<NPC> 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<NPC> sorted() {
List<NPC> vals = new ArrayList<NPC>(npcs.valueCollection());
Collections.sort(vals, NPC_COMPARATOR);
return vals;
}
}
private static final Comparator<NPC> NPC_COMPARATOR = new Comparator<NPC>() {
@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) {
}
}
}

View File

@ -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<TraitInfo> defaultTraits = Lists.newArrayList();
private final Map<String, TraitInfo> 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<String, TraitInfo> entry : registered.entrySet()) {
if (INTERNAL_TRAITS.contains(entry.getKey()))
continue;
final Class<? extends Trait> 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 extends Trait> 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 extends Trait> T getTrait(Class<T> clazz) {
for (TraitInfo entry : registered.values()) {
if (clazz == entry.getTraitClass()) {
return create(entry);
}
}
return null;
}
@Override
@SuppressWarnings("unchecked")
public <T extends Trait> T getTrait(String name) {
TraitInfo info = registered.get(name.toLowerCase());
if (info == null)
return null;
return (T) create(info);
}
@Override
public Class<? extends Trait> 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<String> INTERNAL_TRAITS = Sets.newHashSet();
}

View File

@ -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);
}

View File

@ -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<? extends EntityController> 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<? extends EntityController> controller) {
TYPES.put(type, controller);
}
private static final Map<EntityType, Class<? extends EntityController>> TYPES = Maps.newEnumMap(EntityType.class);
}

View File

@ -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<MetadataValue> 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<String> 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<MetadataValue> 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<String> 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());
}
}

View File

@ -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<String, Object> replacements;
private Template(String name, Map<String, Object> 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<Node> queue = Lists.newArrayList(new Node("", replacements));
for (int i = 0; i < queue.size(); i++) {
Node node = queue.get(i);
for (Entry<String, Object> 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<String, Object>) 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<String, Object> map;
private Node(String headKey, Map<String, Object> map) {
this.headKey = headKey;
this.map = map;
}
}
public static class TemplateBuilder {
private final String name;
private boolean override;
private final Map<String, Object> 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<Template> allTemplates() {
templates.load();
return Iterables.transform(templates.getKey("").getSubKeys(), new Function<DataKey, Template>() {
@Override
public Template apply(DataKey arg0) {
return Template.byName(arg0.name());
}
});
}
public static Template byName(String name) {
templates.load();
if (!templates.getKey("").keyExists(name))
return null;
YamlKey key = templates.getKey(name);
boolean override = key.getBoolean("override", false);
Map<String, Object> replacements = key.getRelative("replacements").getValuesDeep();
return new Template(name, replacements, override);
}
private static YamlStorage templates = new YamlStorage(new File(CitizensAPI.getDataFolder(), "templates.yml"));
static {
templates.load();
}
}

View File

@ -0,0 +1,120 @@
package net.citizensnpcs.npc.ai;
import java.util.List;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.util.Vector;
import com.google.common.collect.Lists;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.ai.AbstractPathStrategy;
import net.citizensnpcs.api.ai.NavigatorParameters;
import net.citizensnpcs.api.ai.TargetType;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.astar.AStarMachine;
import net.citizensnpcs.api.astar.pathfinder.ChunkBlockSource;
import net.citizensnpcs.api.astar.pathfinder.Path;
import net.citizensnpcs.api.astar.pathfinder.VectorGoal;
import net.citizensnpcs.api.astar.pathfinder.VectorNode;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.NMS;
public class AStarNavigationStrategy extends AbstractPathStrategy {
private final Location destination;
private final NPC npc;
private final NavigatorParameters params;
private Path plan;
private boolean planned = false;
private Vector vector;
public AStarNavigationStrategy(NPC npc, Iterable<Vector> path, NavigatorParameters params) {
super(TargetType.LOCATION);
List<Vector> list = Lists.newArrayList(path);
this.params = params;
this.destination = list.get(list.size() - 1).toLocation(npc.getStoredLocation().getWorld());
this.npc = npc;
setPlan(new Path(list));
}
public AStarNavigationStrategy(NPC npc, Location dest, NavigatorParameters params) {
super(TargetType.LOCATION);
this.params = params;
this.destination = dest;
this.npc = npc;
}
@Override
public Iterable<Vector> getPath() {
return plan == null ? null : plan.getPath();
}
@Override
public Location getTargetAsLocation() {
return destination;
}
public void setPlan(Path path) {
this.plan = path;
this.planned = true;
if (plan == null || plan.isComplete()) {
setCancelReason(CancelReason.STUCK);
} else {
vector = plan.getCurrentVector();
if (Setting.DEBUG_PATHFINDING.asBoolean()) {
plan.debug();
}
}
}
@Override
public void stop() {
if (plan != null && Setting.DEBUG_PATHFINDING.asBoolean()) {
plan.debugEnd();
}
plan = null;
}
@Override
public boolean update() {
if (!planned) {
Location location = npc.getEntity().getLocation();
VectorGoal goal = new VectorGoal(destination, (float) params.pathDistanceMargin());
setPlan(ASTAR.runFully(goal,
new VectorNode(goal, location, new ChunkBlockSource(location, params.range()), params.examiners()),
50000));
}
if (getCancelReason() != null || plan == null || plan.isComplete()) {
return true;
}
Location currLoc = npc.getEntity().getLocation(NPC_LOCATION);
if (currLoc.toVector().distanceSquared(vector) <= params.distanceMargin()) {
plan.update(npc);
if (plan.isComplete()) {
return true;
}
vector = plan.getCurrentVector();
}
double dX = vector.getBlockX() - currLoc.getX();
double dZ = vector.getBlockZ() - currLoc.getZ();
double dY = vector.getY() - currLoc.getY();
double xzDistance = dX * dX + dZ * dZ;
double distance = xzDistance + dY * dY;
if (Setting.DEBUG_PATHFINDING.asBoolean()) {
npc.getEntity().getWorld().playEffect(vector.toLocation(npc.getEntity().getWorld()), Effect.ENDER_SIGNAL,
0);
}
if (distance > 0 && dY > NMS.getStepHeight(npc.getEntity()) && xzDistance <= 2.75) {
NMS.setShouldJump(npc.getEntity());
}
double destX = vector.getX() + 0.5, destZ = vector.getZ() + 0.5;
NMS.setDestination(npc.getEntity(), destX, vector.getY(), destZ, params.speed());
params.run();
plan.run(npc);
return false;
}
private static final AStarMachine<VectorNode, Path> ASTAR = AStarMachine.createWithDefaultStorage();
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,172 @@
package net.citizensnpcs.npc.ai;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_10_R1.entity.CraftEntity;
import org.bukkit.craftbukkit.v1_10_R1.inventory.CraftItemStack;
import org.bukkit.entity.Player;
import net.citizensnpcs.api.ai.tree.BehaviorStatus;
import net.citizensnpcs.api.npc.BlockBreaker;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.PlayerAnimation;
import net.citizensnpcs.util.Util;
import net.minecraft.server.v1_10_R1.BlockPosition;
import net.minecraft.server.v1_10_R1.Blocks;
import net.minecraft.server.v1_10_R1.EnchantmentManager;
import net.minecraft.server.v1_10_R1.Entity;
import net.minecraft.server.v1_10_R1.EntityLiving;
import net.minecraft.server.v1_10_R1.EntityPlayer;
import net.minecraft.server.v1_10_R1.EnumItemSlot;
import net.minecraft.server.v1_10_R1.IBlockData;
import net.minecraft.server.v1_10_R1.ItemStack;
import net.minecraft.server.v1_10_R1.Material;
import net.minecraft.server.v1_10_R1.MobEffects;
public class CitizensBlockBreaker extends BlockBreaker {
private final BlockBreakerConfiguration configuration;
private int currentDamage;
private int currentTick;
private final Entity entity;
private boolean isDigging = true;
private final Location location;
private int startDigTick;
private final int x, y, z;
public CitizensBlockBreaker(org.bukkit.entity.Entity entity, org.bukkit.block.Block target,
BlockBreakerConfiguration config) {
this.entity = ((CraftEntity) entity).getHandle();
this.x = target.getX();
this.y = target.getY();
this.z = target.getZ();
this.location = target.getLocation();
this.startDigTick = (int) (System.currentTimeMillis() / 50);
this.configuration = config;
}
private double distanceSquared() {
return Math.pow(entity.locX - x, 2) + Math.pow(entity.locY - y, 2) + Math.pow(entity.locZ - z, 2);
}
private net.minecraft.server.v1_10_R1.ItemStack getCurrentItem() {
return configuration.item() != null ? CraftItemStack.asNMSCopy(configuration.item())
: entity instanceof EntityLiving ? ((EntityLiving) entity).getEquipment(EnumItemSlot.MAINHAND) : null;
}
private float getStrength(IBlockData block) {
float base = block.getBlock().b(block, null, new BlockPosition(0, 0, 0));
return base < 0.0F ? 0.0F : (!isDestroyable(block) ? 1.0F / base / 100.0F : strengthMod(block) / base / 30.0F);
}
private boolean isDestroyable(IBlockData block) {
if (block.getMaterial().isAlwaysDestroyable()) {
return true;
} else {
ItemStack current = getCurrentItem();
return current != null ? current.b(block) : false;
}
}
@Override
public void reset() {
if (configuration.callback() != null) {
configuration.callback().run();
}
isDigging = false;
setBlockDamage(currentDamage = -1);
}
@Override
public BehaviorStatus run() {
if (entity.dead) {
return BehaviorStatus.FAILURE;
}
if (!isDigging) {
return BehaviorStatus.SUCCESS;
}
currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit
if (configuration.radiusSquared() > 0 && distanceSquared() >= configuration.radiusSquared()) {
startDigTick = currentTick;
if (entity instanceof NPCHolder) {
NPC npc = ((NPCHolder) entity).getNPC();
if (npc != null && !npc.getNavigator().isNavigating()) {
npc.getNavigator()
.setTarget(entity.world.getWorld().getBlockAt(x, y, z).getLocation().add(0, 1, 0));
}
}
return BehaviorStatus.RUNNING;
}
Util.faceLocation(entity.getBukkitEntity(), location);
if (entity instanceof EntityPlayer) {
PlayerAnimation.ARM_SWING.play((Player) entity.getBukkitEntity());
}
IBlockData block = entity.world.getType(new BlockPosition(x, y, z));
if (block == null || block == Blocks.AIR) {
return BehaviorStatus.SUCCESS;
} else {
int tickDifference = currentTick - startDigTick;
float damage = getStrength(block) * (tickDifference + 1) * configuration.blockStrengthModifier();
if (damage >= 1F) {
entity.world.getWorld().getBlockAt(x, y, z)
.breakNaturally(CraftItemStack.asCraftMirror(getCurrentItem()));
return BehaviorStatus.SUCCESS;
}
int modifiedDamage = (int) (damage * 10.0F);
if (modifiedDamage != currentDamage) {
setBlockDamage(modifiedDamage);
currentDamage = modifiedDamage;
}
}
return BehaviorStatus.RUNNING;
}
private void setBlockDamage(int modifiedDamage) {
entity.world.c(entity.getId(), new BlockPosition(x, y, z), modifiedDamage);
}
@Override
public boolean shouldExecute() {
return entity.world.getType(new BlockPosition(x, y, z)).getBlock() != Blocks.AIR;
}
private float strengthMod(IBlockData block) {
ItemStack itemstack = getCurrentItem();
float f = itemstack.a(block);
if (entity instanceof EntityLiving) {
EntityLiving handle = (EntityLiving) entity;
if (f > 1.0F) {
int i = EnchantmentManager.getDigSpeedEnchantmentLevel(handle);
if (i > 0) {
f += i * i + 1;
}
}
if (handle.hasEffect(MobEffects.FASTER_DIG)) {
f *= (1.0F + (handle.getEffect(MobEffects.FASTER_DIG).getAmplifier() + 1) * 0.2F);
}
if (handle.hasEffect(MobEffects.SLOWER_DIG)) {
float f1 = 1.0F;
switch (handle.getEffect(MobEffects.SLOWER_DIG).getAmplifier()) {
case 0:
f1 = 0.3F;
break;
case 1:
f1 = 0.09F;
break;
case 2:
f1 = 0.0027F;
break;
case 3:
default:
f1 = 8.1E-4F;
}
f *= f1;
}
if ((handle.a(Material.WATER)) && (!EnchantmentManager.i(handle))) {
f /= 5.0F;
}
}
if (!entity.onGround) {
f /= 5.0F;
}
return f;
}
}

View File

@ -0,0 +1,407 @@
package net.citizensnpcs.npc.ai;
import java.util.Iterator;
import java.util.ListIterator;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.util.Vector;
import com.google.common.collect.Iterables;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.ai.EntityTarget;
import net.citizensnpcs.api.ai.Navigator;
import net.citizensnpcs.api.ai.NavigatorParameters;
import net.citizensnpcs.api.ai.PathStrategy;
import net.citizensnpcs.api.ai.StuckAction;
import net.citizensnpcs.api.ai.TargetType;
import net.citizensnpcs.api.ai.TeleportStuckAction;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.ai.event.NavigationBeginEvent;
import net.citizensnpcs.api.ai.event.NavigationCancelEvent;
import net.citizensnpcs.api.ai.event.NavigationCompleteEvent;
import net.citizensnpcs.api.ai.event.NavigationReplaceEvent;
import net.citizensnpcs.api.ai.event.NavigationStuckEvent;
import net.citizensnpcs.api.ai.event.NavigatorCallback;
import net.citizensnpcs.api.astar.pathfinder.BlockExaminer;
import net.citizensnpcs.api.astar.pathfinder.BlockSource;
import net.citizensnpcs.api.astar.pathfinder.MinecraftBlockExaminer;
import net.citizensnpcs.api.astar.pathfinder.PathPoint;
import net.citizensnpcs.api.astar.pathfinder.PathPoint.PathCallback;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.util.NMS;
import net.citizensnpcs.util.Util;
public class CitizensNavigator implements Navigator, Runnable {
private final NavigatorParameters defaultParams = new NavigatorParameters().baseSpeed(UNINITIALISED_SPEED)
.range(Setting.DEFAULT_PATHFINDING_RANGE.asFloat())
.defaultAttackStrategy(MCTargetStrategy.DEFAULT_ATTACK_STRATEGY)
.attackRange(Setting.NPC_ATTACK_DISTANCE.asDouble())
.updatePathRate(Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt())
.distanceMargin(Setting.DEFAULT_DISTANCE_MARGIN.asDouble())
.stationaryTicks(Setting.DEFAULT_STATIONARY_TICKS.asInt()).stuckAction(TeleportStuckAction.INSTANCE)
.examiner(new MinecraftBlockExaminer()).useNewPathfinder(Setting.USE_NEW_PATHFINDER.asBoolean());
private PathStrategy executing;
private int lastX, lastY, lastZ;
private NavigatorParameters localParams = defaultParams;
private final NPC npc;
private boolean paused;
private int stationaryTicks;
public CitizensNavigator(NPC npc) {
this.npc = npc;
if (Setting.NEW_PATHFINDER_OPENS_DOORS.asBoolean()) {
defaultParams.examiner(new DoorExaminer());
}
}
@Override
public void cancelNavigation() {
stopNavigating(CancelReason.PLUGIN);
}
@Override
public NavigatorParameters getDefaultParameters() {
return defaultParams;
}
@Override
public EntityTarget getEntityTarget() {
return executing instanceof EntityTarget ? (EntityTarget) executing : null;
}
@Override
public NavigatorParameters getLocalParameters() {
if (!isNavigating()) {
return defaultParams;
}
return localParams;
}
@Override
public NPC getNPC() {
return npc;
}
@Override
public PathStrategy getPathStrategy() {
return executing;
}
@Override
public Location getTargetAsLocation() {
return isNavigating() ? executing.getTargetAsLocation() : null;
}
@Override
public TargetType getTargetType() {
return isNavigating() ? executing.getTargetType() : null;
}
@Override
public boolean isNavigating() {
return executing != null;
}
@Override
public boolean isPaused() {
return paused;
}
public void load(DataKey root) {
if (root.keyExists("pathfindingrange")) {
defaultParams.range((float) root.getDouble("pathfindingrange"));
}
if (root.keyExists("stationaryticks")) {
defaultParams.stationaryTicks(root.getInt("stationaryticks"));
}
if (root.keyExists("distancemargin")) {
defaultParams.distanceMargin(root.getDouble("distancemargin"));
}
if (root.keyExists("updatepathrate")) {
defaultParams.updatePathRate(root.getInt("updatepathrate"));
}
defaultParams.speedModifier((float) root.getDouble("speedmodifier", 1F));
defaultParams.avoidWater(root.getBoolean("avoidwater"));
if (!root.getBoolean("usedefaultstuckaction") && defaultParams.stuckAction() == TeleportStuckAction.INSTANCE) {
defaultParams.stuckAction(null);
}
}
public void onDespawn() {
stopNavigating(CancelReason.NPC_DESPAWNED);
}
public void onSpawn() {
if (defaultParams.baseSpeed() == UNINITIALISED_SPEED) {
defaultParams.baseSpeed(NMS.getSpeedFor(npc));
}
updatePathfindingRange();
}
@Override
public void run() {
updateMountedStatus();
if (!isNavigating() || !npc.isSpawned() || paused)
return;
if (Math.pow(localParams.range(), 2) < npc.getStoredLocation().distanceSquared(getTargetAsLocation())) {
stopNavigating(CancelReason.STUCK);
return;
}
if (updateStationaryStatus())
return;
updatePathfindingRange();
boolean finished = executing.update();
if (localParams.lookAtFunction() != null) {
Util.faceLocation(npc.getEntity(), localParams.lookAtFunction().apply(this), true);
}
if (!finished) {
return;
}
if (executing.getCancelReason() != null) {
stopNavigating(executing.getCancelReason());
} else {
NavigationCompleteEvent event = new NavigationCompleteEvent(this);
PathStrategy old = executing;
Bukkit.getPluginManager().callEvent(event);
if (old == executing) {
stopNavigating(null);
}
}
}
public void save(DataKey root) {
if (defaultParams.range() != Setting.DEFAULT_PATHFINDING_RANGE.asFloat()) {
root.setDouble("pathfindingrange", defaultParams.range());
} else {
root.removeKey("pathfindingrange");
}
if (defaultParams.stationaryTicks() != Setting.DEFAULT_STATIONARY_TICKS.asInt()) {
root.setInt("stationaryticks", defaultParams.stationaryTicks());
} else {
root.removeKey("stationaryticks");
}
if (defaultParams.distanceMargin() != Setting.DEFAULT_DISTANCE_MARGIN.asDouble()) {
root.setDouble("distancemargin", defaultParams.distanceMargin());
} else {
root.removeKey("distancemargin");
}
if (defaultParams.updatePathRate() != Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt()) {
root.setInt("updatepathrate", defaultParams.updatePathRate());
} else {
root.removeKey("updatepathrate");
}
root.setDouble("speedmodifier", defaultParams.speedModifier());
root.setBoolean("avoidwater", defaultParams.avoidWater());
root.setBoolean("usedefaultstuckaction", defaultParams.stuckAction() == TeleportStuckAction.INSTANCE);
}
@Override
public void setPaused(boolean paused) {
this.paused = paused;
}
@Override
public void setTarget(Entity target, boolean aggressive) {
if (!npc.isSpawned())
throw new IllegalStateException("npc is not spawned");
if (target == null) {
cancelNavigation();
return;
}
switchParams();
updatePathfindingRange();
PathStrategy newStrategy = new MCTargetStrategy(npc, target, aggressive, localParams);
switchStrategyTo(newStrategy);
}
@Override
public void setTarget(Iterable<Vector> path) {
if (!npc.isSpawned())
throw new IllegalStateException("npc is not spawned");
if (path == null || Iterables.size(path) == 0) {
cancelNavigation();
return;
}
switchParams();
updatePathfindingRange();
PathStrategy newStrategy;
if (npc.isFlyable()) {
newStrategy = new FlyingAStarNavigationStrategy(npc, path, localParams);
} else if (localParams.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)) {
newStrategy = new AStarNavigationStrategy(npc, path, localParams);
} else {
newStrategy = new MCNavigationStrategy(npc, path, localParams);
}
switchStrategyTo(newStrategy);
}
@Override
public void setTarget(Location target) {
if (!npc.isSpawned())
throw new IllegalStateException("npc is not spawned");
if (target == null) {
cancelNavigation();
return;
}
switchParams();
updatePathfindingRange();
PathStrategy newStrategy;
if (npc.isFlyable()) {
newStrategy = new FlyingAStarNavigationStrategy(npc, target, localParams);
} else if (localParams.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)) {
newStrategy = new AStarNavigationStrategy(npc, target, localParams);
} else {
newStrategy = new MCNavigationStrategy(npc, target, localParams);
}
switchStrategyTo(newStrategy);
}
private void stopNavigating() {
if (executing != null) {
executing.stop();
}
executing = null;
localParams = defaultParams;
stationaryTicks = 0;
if (npc.isSpawned()) {
Vector velocity = npc.getEntity().getVelocity();
velocity.setX(0).setY(0).setZ(0);
npc.getEntity().setVelocity(velocity);
}
}
private void stopNavigating(CancelReason reason) {
if (!isNavigating())
return;
Iterator<NavigatorCallback> itr = localParams.callbacks().iterator();
while (itr.hasNext()) {
itr.next().onCompletion(reason);
itr.remove();
}
if (reason == null) {
stopNavigating();
return;
}
if (reason == CancelReason.STUCK) {
StuckAction action = localParams.stuckAction();
NavigationStuckEvent event = new NavigationStuckEvent(this, action);
Bukkit.getPluginManager().callEvent(event);
action = event.getAction();
boolean shouldContinue = action != null ? action.run(npc, this) : false;
if (shouldContinue) {
stationaryTicks = 0;
executing.clearCancelReason();
return;
}
}
NavigationCancelEvent event = new NavigationCancelEvent(this, reason);
PathStrategy old = executing;
Bukkit.getPluginManager().callEvent(event);
if (old == executing) {
stopNavigating();
}
}
private void switchParams() {
localParams = defaultParams.clone();
}
private void switchStrategyTo(PathStrategy newStrategy) {
if (executing != null) {
Bukkit.getPluginManager().callEvent(new NavigationReplaceEvent(this));
}
executing = newStrategy;
stationaryTicks = 0;
if (npc.isSpawned()) {
NMS.updateNavigationWorld(npc.getEntity(), npc.getEntity().getWorld());
}
Bukkit.getPluginManager().callEvent(new NavigationBeginEvent(this));
}
private void updateMountedStatus() {
if (!isNavigating())
return;
Entity vehicle = NMS.getVehicle(npc.getEntity());
if (!(vehicle instanceof NPCHolder))
return;
NPC mount = ((NPCHolder) vehicle).getNPC();
switch (getTargetType()) {
case ENTITY:
mount.getNavigator().setTarget(getEntityTarget().getTarget(), getEntityTarget().isAggressive());
break;
case LOCATION:
mount.getNavigator().setTarget(getTargetAsLocation());
break;
default:
return;
}
cancelNavigation();
}
private void updatePathfindingRange() {
NMS.updatePathfindingRange(npc, localParams.range());
}
private boolean updateStationaryStatus() {
if (localParams.stationaryTicks() < 0)
return false;
Location current = npc.getEntity().getLocation(STATIONARY_LOCATION);
if (current.getY() < -5) {
stopNavigating(CancelReason.STUCK);
return true;
}
if (lastX == current.getBlockX() && lastY == current.getBlockY() && lastZ == current.getBlockZ()) {
if (++stationaryTicks >= localParams.stationaryTicks()) {
stopNavigating(CancelReason.STUCK);
return true;
}
} else
stationaryTicks = 0;
lastX = current.getBlockX();
lastY = current.getBlockY();
lastZ = current.getBlockZ();
return false;
}
public static class DoorExaminer implements BlockExaminer {
@Override
public float getCost(BlockSource source, PathPoint point) {
return 0F;
}
@Override
public PassableState isPassable(BlockSource source, PathPoint point) {
Material in = source.getMaterialAt(point.getVector());
if (MinecraftBlockExaminer.isDoor(in)) {
point.addCallback(new DoorOpener());
return PassableState.PASSABLE;
}
return PassableState.IGNORE;
}
}
private static class DoorOpener implements PathCallback {
@Override
@SuppressWarnings("deprecation")
public void run(NPC npc, Block point, ListIterator<Block> path) {
if (npc.getStoredLocation().distance(point.getLocation()) < 2) {
boolean bottom = (point.getData() & 8) == 0;
Block set = bottom ? point : point.getRelative(BlockFace.DOWN);
set.setData((byte) ((set.getData() & 7) | 4));
}
}
}
private static final Location STATIONARY_LOCATION = new Location(null, 0, 0, 0);
private static int UNINITIALISED_SPEED = Integer.MIN_VALUE;
}

View File

@ -0,0 +1,44 @@
package net.citizensnpcs.npc.ai;
import java.util.List;
import org.bukkit.util.Vector;
import com.google.common.collect.Lists;
import net.citizensnpcs.api.astar.pathfinder.BlockSource;
import net.citizensnpcs.api.astar.pathfinder.NeighbourGeneratorBlockExaminer;
import net.citizensnpcs.api.astar.pathfinder.PathPoint;
public class EnhancedMovementExaminer implements NeighbourGeneratorBlockExaminer {
@Override
public float getCost(BlockSource source, PathPoint point) {
return 0;
}
@Override
public List<PathPoint> getNeighbours(BlockSource source, PathPoint point) {
Vector location = point.getVector();
List<PathPoint> neighbours = Lists.newArrayList();
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
for (int z = -1; z <= 1; z++) {
if (x == 0 && y == 0 && z == 0)
continue;
if (x != 0 && z != 0)
continue;
Vector mod = location.clone().add(new Vector(x, y, z));
if (mod.equals(location))
continue;
neighbours.add(point.createAtOffset(mod));
}
}
}
return null;
}
@Override
public PassableState isPassable(BlockSource source, PathPoint point) {
return null;
}
}

View File

@ -0,0 +1,148 @@
package net.citizensnpcs.npc.ai;
import java.util.List;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.util.Vector;
import com.google.common.collect.Lists;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.ai.AbstractPathStrategy;
import net.citizensnpcs.api.ai.NavigatorParameters;
import net.citizensnpcs.api.ai.TargetType;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.astar.AStarMachine;
import net.citizensnpcs.api.astar.pathfinder.BlockExaminer;
import net.citizensnpcs.api.astar.pathfinder.ChunkBlockSource;
import net.citizensnpcs.api.astar.pathfinder.FlyingBlockExaminer;
import net.citizensnpcs.api.astar.pathfinder.Path;
import net.citizensnpcs.api.astar.pathfinder.VectorGoal;
import net.citizensnpcs.api.astar.pathfinder.VectorNode;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.NMS;
public class FlyingAStarNavigationStrategy extends AbstractPathStrategy {
private final NPC npc;
private final NavigatorParameters parameters;
private Path plan;
private boolean planned;
private final Location target;
private Vector vector;
public FlyingAStarNavigationStrategy(NPC npc, Iterable<Vector> path, NavigatorParameters params) {
super(TargetType.LOCATION);
List<Vector> list = Lists.newArrayList(path);
this.target = list.get(list.size() - 1).toLocation(npc.getStoredLocation().getWorld());
this.parameters = params;
this.npc = npc;
setPlan(new Path(list));
}
public FlyingAStarNavigationStrategy(final NPC npc, Location dest, NavigatorParameters params) {
super(TargetType.LOCATION);
this.target = dest;
this.parameters = params;
this.npc = npc;
}
@Override
public Iterable<Vector> getPath() {
return plan == null ? null : plan.getPath();
}
@Override
public Location getTargetAsLocation() {
return target;
}
public void setPlan(Path path) {
this.plan = path;
if (plan == null || plan.isComplete()) {
setCancelReason(CancelReason.STUCK);
} else {
vector = plan.getCurrentVector();
if (Setting.DEBUG_PATHFINDING.asBoolean()) {
plan.debug();
}
}
planned = true;
}
@Override
public void stop() {
if (plan != null && Setting.DEBUG_PATHFINDING.asBoolean()) {
plan.debugEnd();
}
plan = null;
}
@Override
public boolean update() {
if (!planned) {
Location location = npc.getEntity().getLocation();
VectorGoal goal = new VectorGoal(target, (float) parameters.pathDistanceMargin());
boolean found = false;
for (BlockExaminer examiner : parameters.examiners()) {
if (examiner instanceof FlyingBlockExaminer) {
found = true;
break;
}
}
if (!found) {
parameters.examiner(new FlyingBlockExaminer());
}
setPlan(ASTAR.runFully(goal, new VectorNode(goal, location,
new ChunkBlockSource(location, parameters.range()), parameters.examiners()), 50000));
}
if (getCancelReason() != null || plan == null || plan.isComplete()) {
return true;
}
Location current = npc.getEntity().getLocation(NPC_LOCATION);
if (current.toVector().distanceSquared(vector) <= parameters.distanceMargin()) {
plan.update(npc);
if (plan.isComplete()) {
return true;
}
vector = plan.getCurrentVector();
}
double d0 = vector.getX() + 0.5D - current.getX();
double d1 = vector.getY() + 0.1D - current.getY();
double d2 = vector.getZ() + 0.5D - current.getZ();
Vector velocity = npc.getEntity().getVelocity();
double motX = velocity.getX(), motY = velocity.getY(), motZ = velocity.getZ();
motX += (Math.signum(d0) * 0.5D - motX) * 0.1;
motY += (Math.signum(d1) * 0.7D - motY) * 0.1;
motZ += (Math.signum(d2) * 0.5D - motZ) * 0.1;
float targetYaw = (float) (Math.atan2(motZ, motX) * 180.0D / Math.PI) - 90.0F;
float normalisedTargetYaw = (targetYaw - current.getYaw()) % 360;
if (normalisedTargetYaw >= 180.0F) {
normalisedTargetYaw -= 360.0F;
}
if (normalisedTargetYaw < -180.0F) {
normalisedTargetYaw += 360.0F;
}
velocity.setX(motX).setY(motY).setZ(motZ).multiply(parameters.speed());
npc.getEntity().setVelocity(velocity);
if (npc.getEntity().getType() != EntityType.ENDER_DRAGON) {
NMS.setVerticalMovement(npc.getEntity(), 0.5);
float newYaw = current.getYaw() + normalisedTargetYaw;
current.setYaw(newYaw);
NMS.setHeadYaw(npc.getEntity(), newYaw);
npc.teleport(current, TeleportCause.PLUGIN);
}
parameters.run();
plan.run(npc);
return false;
}
private static final AStarMachine<VectorNode, Path> ASTAR = AStarMachine.createWithDefaultStorage();
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,97 @@
package net.citizensnpcs.npc.ai;
import java.util.List;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import com.google.common.collect.Lists;
import net.citizensnpcs.api.ai.AbstractPathStrategy;
import net.citizensnpcs.api.ai.NavigatorParameters;
import net.citizensnpcs.api.ai.TargetType;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.NMS;
public class MCNavigationStrategy extends AbstractPathStrategy {
private final Entity handle;
private final MCNavigator navigator;
private final NavigatorParameters parameters;
private final Location target;
MCNavigationStrategy(final NPC npc, Iterable<Vector> path, NavigatorParameters params) {
super(TargetType.LOCATION);
List<Vector> list = Lists.newArrayList(path);
this.target = list.get(list.size() - 1).toLocation(npc.getStoredLocation().getWorld());
this.parameters = params;
handle = npc.getEntity();
this.navigator = NMS.getTargetNavigator(npc.getEntity(), list, params);
}
MCNavigationStrategy(final NPC npc, Location dest, NavigatorParameters params) {
super(TargetType.LOCATION);
this.target = dest;
this.parameters = params;
handle = npc.getEntity();
this.navigator = NMS.getTargetNavigator(npc.getEntity(), dest, params);
}
private double distanceSquared() {
return handle.getLocation(HANDLE_LOCATION).distanceSquared(target);
}
@Override
public Iterable<Vector> getPath() {
return navigator.getPath();
}
@Override
public Location getTargetAsLocation() {
return target;
}
@Override
public TargetType getTargetType() {
return TargetType.LOCATION;
}
@Override
public void stop() {
navigator.stop();
}
@Override
public String toString() {
return "MCNavigationStrategy [target=" + target + "]";
}
@Override
public boolean update() {
if (navigator.getCancelReason() != null) {
setCancelReason(navigator.getCancelReason());
}
if (getCancelReason() != null)
return true;
boolean wasFinished = navigator.update();
parameters.run();
if (distanceSquared() < parameters.distanceMargin()) {
stop();
return true;
}
return wasFinished;
}
public static interface MCNavigator {
CancelReason getCancelReason();
Iterable<Vector> getPath();
void stop();
boolean update();
}
private static final Location HANDLE_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,204 @@
package net.citizensnpcs.npc.ai;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.util.Vector;
import net.citizensnpcs.api.ai.AttackStrategy;
import net.citizensnpcs.api.ai.EntityTarget;
import net.citizensnpcs.api.ai.NavigatorParameters;
import net.citizensnpcs.api.ai.PathStrategy;
import net.citizensnpcs.api.ai.TargetType;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.util.BoundingBox;
import net.citizensnpcs.util.NMS;
public class MCTargetStrategy implements PathStrategy, EntityTarget {
private final boolean aggro;
private int attackTicks;
private CancelReason cancelReason;
private final Entity handle;
private final NPC npc;
private final NavigatorParameters parameters;
private final Entity target;
private final TargetNavigator targetNavigator;
private int updateCounter = -1;
public MCTargetStrategy(NPC npc, org.bukkit.entity.Entity target, boolean aggro, NavigatorParameters params) {
this.npc = npc;
this.parameters = params;
this.handle = npc.getEntity();
this.target = target;
TargetNavigator nav = NMS.getTargetNavigator(npc.getEntity(), target, params);
this.targetNavigator = nav != null && !params.useNewPathfinder() ? nav : new AStarTargeter();
this.aggro = aggro;
}
private boolean canAttack() {
BoundingBox handleBB = NMS.getBoundingBox(handle), targetBB = NMS.getBoundingBox(target);
return attackTicks == 0 && (handleBB.maxY > targetBB.minY && handleBB.minY < targetBB.maxY)
&& closeEnough(distanceSquared()) && hasLineOfSight();
}
@Override
public void clearCancelReason() {
cancelReason = null;
}
private boolean closeEnough(double distance) {
return distance <= parameters.attackRange();
}
private double distanceSquared() {
return handle.getLocation(HANDLE_LOCATION).distanceSquared(target.getLocation(TARGET_LOCATION));
}
@Override
public CancelReason getCancelReason() {
return cancelReason;
}
@Override
public Iterable<Vector> getPath() {
return targetNavigator.getPath();
}
@Override
public org.bukkit.entity.Entity getTarget() {
return target;
}
@Override
public Location getTargetAsLocation() {
return getTarget().getLocation();
}
@Override
public TargetType getTargetType() {
return TargetType.ENTITY;
}
private boolean hasLineOfSight() {
return ((LivingEntity) handle).hasLineOfSight(target);
}
@Override
public boolean isAggressive() {
return aggro;
}
@Override
public void stop() {
targetNavigator.stop();
}
@Override
public String toString() {
return "MCTargetStrategy [target=" + target + "]";
}
@Override
public boolean update() {
if (target == null || !target.isValid()) {
cancelReason = CancelReason.TARGET_DIED;
return true;
}
if (target.getWorld() != handle.getWorld()) {
cancelReason = CancelReason.TARGET_MOVED_WORLD;
return true;
}
if (cancelReason != null) {
return true;
}
if (!aggro && distanceSquared() <= parameters.distanceMargin()) {
stop();
return false;
} else if (updateCounter == -1 || updateCounter++ > parameters.updatePathRate()) {
targetNavigator.setPath();
updateCounter = 0;
}
targetNavigator.update();
NMS.look(handle, target);
if (aggro && canAttack()) {
AttackStrategy strategy = parameters.attackStrategy();
if (strategy != null && strategy.handle((LivingEntity) handle, (LivingEntity) getTarget())) {
} else if (strategy != parameters.defaultAttackStrategy()) {
parameters.defaultAttackStrategy().handle((LivingEntity) handle, (LivingEntity) getTarget());
}
attackTicks = parameters.attackDelayTicks();
}
if (attackTicks > 0) {
attackTicks--;
}
return false;
}
private class AStarTargeter implements TargetNavigator {
private int failureTimes = 0;
private PathStrategy strategy;
@Override
public Iterable<Vector> getPath() {
return strategy.getPath();
}
@Override
public void setPath() {
setStrategy();
strategy.update();
CancelReason subReason = strategy.getCancelReason();
if (subReason == CancelReason.STUCK) {
if (failureTimes++ > 10) {
cancelReason = strategy.getCancelReason();
}
} else {
failureTimes = 0;
cancelReason = strategy.getCancelReason();
}
}
private void setStrategy() {
Location location = parameters.entityTargetLocationMapper().apply(target);
if (location == null) {
throw new IllegalStateException("mapper should not return null");
}
strategy = npc.isFlyable() ? new FlyingAStarNavigationStrategy(npc, location, parameters)
: new AStarNavigationStrategy(npc, location, parameters);
}
@Override
public void stop() {
strategy.stop();
}
@Override
public void update() {
strategy.update();
}
}
public static interface TargetNavigator {
Iterable<Vector> getPath();
void setPath();
void stop();
void update();
}
static final AttackStrategy DEFAULT_ATTACK_STRATEGY = new AttackStrategy() {
@Override
public boolean handle(LivingEntity attacker, LivingEntity bukkitTarget) {
NMS.attack(attacker, bukkitTarget);
return false;
}
};
private static final Location HANDLE_LOCATION = new Location(null, 0, 0, 0);
private static final Location TARGET_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,7 @@
package net.citizensnpcs.npc.ai;
import net.citizensnpcs.api.npc.NPC;
public interface NPCHolder {
public NPC getNPC();
}

View File

@ -0,0 +1,133 @@
package net.citizensnpcs.npc.ai.speech;
import java.util.ArrayList;
import java.util.List;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.ai.speech.Talkable;
import net.citizensnpcs.api.ai.speech.VocalChord;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Messaging;
import org.bukkit.entity.Entity;
public class Chat implements VocalChord {
public final String VOCAL_CHORD_NAME = "chat";
@Override
public String getName() {
return VOCAL_CHORD_NAME;
}
@Override
public void talk(SpeechContext context) {
if (context.getTalker() == null)
return;
NPC npc = CitizensAPI.getNPCRegistry().getNPC(context.getTalker().getEntity());
if (npc == null)
return;
// chat to the world with CHAT_FORMAT and CHAT_RANGE settings
if (!context.hasRecipients()) {
String text = Setting.CHAT_FORMAT.asString().replace("<npc>", npc.getName()).replace("<text>",
context.getMessage());
talkToBystanders(npc, text, context);
return;
}
// Assumed recipients at this point
else if (context.size() <= 1) {
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>",
context.getMessage());
String targetName = "";
// For each recipient
for (Talkable entity : context) {
entity.talkTo(context, text, this);
targetName = entity.getName();
}
// Check if bystanders hear targeted chat
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean())
return;
// Format message with config setting and send to bystanders
String bystanderText = Setting.CHAT_FORMAT_TO_BYSTANDERS.asString().replace("<npc>", npc.getName())
.replace("<target>", targetName).replace("<text>", context.getMessage());
talkToBystanders(npc, bystanderText, context);
return;
}
else { // Multiple recipients
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>",
context.getMessage());
List<String> targetNames = new ArrayList<String>();
// Talk to each recipient
for (Talkable entity : context) {
entity.talkTo(context, text, this);
targetNames.add(entity.getName());
}
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean())
return;
String targets = "";
int max = Setting.CHAT_MAX_NUMBER_OF_TARGETS.asInt();
String[] format = Setting.CHAT_MULTIPLE_TARGETS_FORMAT.asString().split("\\|");
if (format.length != 4)
Messaging.severe("npc.chat.options.multiple-targets-format invalid!");
if (max == 1) {
targets = format[0].replace("<target>", targetNames.get(0)) + format[3];
} else if (max == 2 || targetNames.size() == 2) {
if (targetNames.size() == 2) {
targets = format[0].replace("<target>", targetNames.get(0))
+ format[2].replace("<target>", targetNames.get(1));
} else
targets = format[0].replace("<target>", targetNames.get(0))
+ format[1].replace("<target>", targetNames.get(1)) + format[3];
} else if (max >= 3) {
targets = format[0].replace("<target>", targetNames.get(0));
int x = 1;
for (x = 1; x < max - 1; x++) {
if (targetNames.size() - 1 == x)
break;
targets = targets + format[1].replace("<npc>", targetNames.get(x));
}
if (targetNames.size() == max) {
targets = targets + format[2].replace("<npc>", targetNames.get(x));
} else
targets = targets + format[3];
}
String bystanderText = Setting.CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS.asString()
.replace("<npc>", npc.getName()).replace("<targets>", targets)
.replace("<text>", context.getMessage());
talkToBystanders(npc, bystanderText, context);
}
}
private void talkToBystanders(NPC npc, String text, SpeechContext context) {
// Get list of nearby entities
List<Entity> bystanderEntities = npc.getEntity().getNearbyEntities(Setting.CHAT_RANGE.asDouble(),
Setting.CHAT_RANGE.asDouble(), Setting.CHAT_RANGE.asDouble());
for (Entity bystander : bystanderEntities) {
// Continue if a LivingEntity, which is compatible with
// TalkableEntity
boolean shouldTalk = true;
// Exclude targeted recipients
if (context.hasRecipients()) {
for (Talkable target : context) {
if (target.getEntity().equals(bystander)) {
shouldTalk = false;
break;
}
}
}
// Found a nearby LivingEntity, make it Talkable and
// talkNear it if 'should_talk'
if (shouldTalk) {
new TalkableEntity(bystander).talkNear(context, text, this);
}
}
}
}

View File

@ -0,0 +1,82 @@
package net.citizensnpcs.npc.ai.speech;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import net.citizensnpcs.api.ai.speech.SpeechFactory;
import net.citizensnpcs.api.ai.speech.Talkable;
import net.citizensnpcs.api.ai.speech.VocalChord;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import com.google.common.base.Preconditions;
public class CitizensSpeechFactory implements SpeechFactory {
Map<String, Class<? extends VocalChord>> registered = new HashMap<String, Class<? extends VocalChord>>();
@Override
public VocalChord getVocalChord(Class<? extends VocalChord> clazz) {
// Return a new instance of the VocalChord specified
try {
return clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
@Override
public VocalChord getVocalChord(String name) {
// Check if VocalChord name is a registered type
if (isRegistered(name))
// Return a new instance of the VocalChord specified
try {
return registered.get(name.toLowerCase()).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
@Override
public String getVocalChordName(Class<? extends VocalChord> clazz) {
// Get the name of a VocalChord class that has been registered
for (Entry<String, Class<? extends VocalChord>> vocalChord : registered.entrySet())
if (vocalChord.getValue() == clazz)
return vocalChord.getKey();
return null;
}
@Override
public boolean isRegistered(String name) {
return registered.containsKey(name.toLowerCase());
}
@Override
public Talkable newTalkableEntity(Entity entity) {
if (entity == null)
return null;
return new TalkableEntity(entity);
}
public Talkable newTalkableEntity(LivingEntity entity) {
return newTalkableEntity((Entity) entity);
}
@Override
public void register(Class<? extends VocalChord> clazz, String name) {
Preconditions.checkNotNull(name, "info cannot be null");
Preconditions.checkNotNull(clazz, "vocalchord cannot be null");
if (registered.containsKey(name.toLowerCase()))
throw new IllegalArgumentException("vocalchord name already registered");
registered.put(name.toLowerCase(), clazz);
}
}

View File

@ -0,0 +1,92 @@
package net.citizensnpcs.npc.ai.speech;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.ai.speech.Talkable;
import net.citizensnpcs.api.ai.speech.VocalChord;
import net.citizensnpcs.api.ai.speech.event.SpeechBystanderEvent;
import net.citizensnpcs.api.ai.speech.event.SpeechTargetedEvent;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Messaging;
import org.bukkit.Bukkit;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
public class TalkableEntity implements Talkable {
Entity entity;
public TalkableEntity(Entity entity) {
this.entity = entity;
}
public TalkableEntity(NPC npc) {
entity = npc.getEntity();
}
public TalkableEntity(Player player) {
entity = player;
}
/**
* Used to compare a LivingEntity to this TalkableEntity
*
* @return 0 if the Entities are the same, 1 if they are not, -1 if the object compared is not a valid LivingEntity
*/
@Override
public int compareTo(Object o) {
// If not living entity, return -1
if (!(o instanceof Entity)) {
return -1;
// If NPC and matches, return 0
} else if (CitizensAPI.getNPCRegistry().isNPC((Entity) o) && CitizensAPI.getNPCRegistry().isNPC(entity)
&& CitizensAPI.getNPCRegistry().getNPC((Entity) o).getUniqueId()
.equals(CitizensAPI.getNPCRegistry().getNPC(entity).getUniqueId())) {
return 0;
} else if (entity.equals(o)) {
return 0;
} else {
return 1;
}
}
@Override
public Entity getEntity() {
return entity;
}
@Override
public String getName() {
if (CitizensAPI.getNPCRegistry().isNPC(entity)) {
return CitizensAPI.getNPCRegistry().getNPC(entity).getName();
} else if (entity instanceof Player) {
return ((Player) entity).getName();
} else {
return entity.getType().name().replace("_", " ");
}
}
private void talk(String message) {
if (entity instanceof Player && !CitizensAPI.getNPCRegistry().isNPC(entity))
Messaging.send((Player) entity, message);
}
@Override
public void talkNear(SpeechContext context, String text, VocalChord vocalChord) {
SpeechBystanderEvent event = new SpeechBystanderEvent(this, context, text, vocalChord);
Bukkit.getServer().getPluginManager().callEvent(event);
if (event.isCancelled())
return;
talk(event.getMessage());
}
@Override
public void talkTo(SpeechContext context, String text, VocalChord vocalChord) {
SpeechTargetedEvent event = new SpeechTargetedEvent(this, context, text, vocalChord);
Bukkit.getServer().getPluginManager().callEvent(event);
if (event.isCancelled())
return;
talk(event.getMessage());
}
}

View File

@ -0,0 +1,14 @@
package net.citizensnpcs.npc.profile;
/**
* Interface for a subscriber of the results of a profile fetch.
*/
public interface ProfileFetchHandler {
/**
* Invoked when a result for a profile is ready.
*
* @param request
* The profile request that was handled.
*/
void onResult(ProfileRequest request);
}

View File

@ -0,0 +1,29 @@
package net.citizensnpcs.npc.profile;
/**
* The result status of a profile fetch.
*/
public enum ProfileFetchResult {
/**
* The profile has not been fetched yet.
*/
PENDING,
/**
* The profile was successfully fetched.
*/
SUCCESS,
/**
* The profile request failed for unknown reasons.
*/
FAILED,
/**
* The profile request failed because the profile
* was not found.
*/
NOT_FOUND,
/**
* The profile request failed because too many requests
* were sent.
*/
TOO_MANY_REQUESTS
}

View File

@ -0,0 +1,107 @@
package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import com.google.common.base.Preconditions;
import net.citizensnpcs.api.CitizensAPI;
/**
* Thread used to fetch profiles from the Mojang servers.
*
* <p>
* Maintains a cache of profiles so that no profile is ever requested more than once during a single server session.
* </p>
*
* @see ProfileFetcher
*/
class ProfileFetchThread implements Runnable {
private final ProfileFetcher profileFetcher = new ProfileFetcher();
private final Deque<ProfileRequest> queue = new ArrayDeque<ProfileRequest>();
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35);
private final Object sync = new Object(); // sync for queue & requested fields
ProfileFetchThread() {
}
/**
* Fetch a profile.
*
* @param name
* The name of the player the profile belongs to.
* @param handler
* Optional handler to handle result fetch result. Handler always invoked from the main thread.
*
* @see ProfileFetcher#fetch
*/
void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name);
name = name.toLowerCase();
ProfileRequest request;
synchronized (sync) {
request = requested.get(name);
if (request == null) {
request = new ProfileRequest(name, handler);
queue.add(request);
requested.put(name, request);
return;
} else if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
queue.add(request);
}
}
if (handler != null) {
if (request.getResult() == ProfileFetchResult.PENDING
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
addHandler(request, handler);
} else {
sendResult(handler, request);
}
}
}
@Override
public void run() {
List<ProfileRequest> requests;
synchronized (sync) {
if (queue.isEmpty())
return;
requests = new ArrayList<ProfileRequest>(queue);
queue.clear();
}
profileFetcher.fetchRequests(requests);
}
private static void addHandler(final ProfileRequest request, final ProfileFetchHandler handler) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
request.addHandler(handler);
}
}, 1);
}
private static void sendResult(final ProfileFetchHandler handler, final ProfileRequest request) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
handler.onResult(request);
}
}, 1);
}
}

View File

@ -0,0 +1,167 @@
package net.citizensnpcs.npc.profile;
import java.util.Collection;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.mojang.authlib.Agent;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.GameProfileRepository;
import com.mojang.authlib.ProfileLookupCallback;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.NMS;
/**
* Fetches game profiles that include skin data from Mojang servers.
*
* @see ProfileFetchThread
*/
public class ProfileFetcher {
ProfileFetcher() {
}
/**
* Fetch one or more profiles.
*
* @param requests
* The profile requests.
*/
void fetchRequests(final Collection<ProfileRequest> requests) {
Preconditions.checkNotNull(requests);
final GameProfileRepository repo = NMS.getGameProfileRepository();
String[] playerNames = new String[requests.size()];
int i = 0;
for (ProfileRequest request : requests) {
playerNames[i++] = request.getPlayerName();
}
repo.findProfilesByNames(playerNames, Agent.MINECRAFT, new ProfileLookupCallback() {
@Override
public void onProfileLookupFailed(GameProfile profile, Exception e) {
if (Messaging.isDebugging()) {
Messaging.debug(
"Profile lookup for player '" + profile.getName() + "' failed2: " + getExceptionMsg(e));
Messaging.debug(Throwables.getStackTraceAsString(e));
}
ProfileRequest request = findRequest(profile.getName(), requests);
if (request == null)
return;
if (isProfileNotFound(e)) {
request.setResult(null, ProfileFetchResult.NOT_FOUND);
} else if (isTooManyRequests(e)) {
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
} else {
request.setResult(null, ProfileFetchResult.FAILED);
}
}
@Override
public void onProfileLookupSucceeded(final GameProfile profile) {
if (Messaging.isDebugging()) {
Messaging.debug("Fetched profile " + profile.getId() + " for player " + profile.getName());
}
ProfileRequest request = findRequest(profile.getName(), requests);
if (request == null)
return;
try {
request.setResult(NMS.fillProfileProperties(profile, true), ProfileFetchResult.SUCCESS);
} catch (Exception e) {
if (Messaging.isDebugging()) {
Messaging.debug(
"Profile lookup for player '" + profile.getName() + "' failed2: " + getExceptionMsg(e));
Messaging.debug(Throwables.getStackTraceAsString(e));
}
if (isTooManyRequests(e)) {
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
} else {
request.setResult(null, ProfileFetchResult.FAILED);
}
}
}
});
}
/**
* Fetch a profile.
*
* @param name
* The name of the player the profile belongs to.
* @param handler
* Optional handler to handle the result. Handler always invoked from the main thread.
*/
public static void fetch(String name, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(name);
if (PROFILE_THREAD == null) {
initThread();
}
PROFILE_THREAD.fetch(name, handler);
}
@Nullable
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
name = name.toLowerCase();
for (ProfileRequest request : requests) {
if (request.getPlayerName().equals(name)) {
return request;
}
}
return null;
}
private static String getExceptionMsg(Exception e) {
return Throwables.getRootCause(e).getMessage();
}
private static void initThread() {
if (THREAD_TASK != null) {
THREAD_TASK.cancel();
}
PROFILE_THREAD = new ProfileFetchThread();
THREAD_TASK = Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, 21, 20);
}
private static boolean isProfileNotFound(Exception e) {
String message = e.getMessage();
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
return (message != null && message.contains("did not find"))
|| (cause != null && cause.contains("did not find"));
}
private static boolean isTooManyRequests(Exception e) {
String message = e.getMessage();
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
return (message != null && message.contains("too many requests"))
|| (cause != null && cause.contains("too many requests"));
}
/**
* Clear all queued and cached requests.
*/
public static void reset() {
initThread();
}
private static ProfileFetchThread PROFILE_THREAD;
private static BukkitTask THREAD_TASK;
}

View File

@ -0,0 +1,127 @@
package net.citizensnpcs.npc.profile;
import java.util.ArrayDeque;
import java.util.Deque;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.api.CitizensAPI;
/**
* Stores basic information about a single profile used to request profiles from the Mojang servers.
*
* <p>
* Also stores the result of the request.
* </p>
*/
public class ProfileRequest {
private Deque<ProfileFetchHandler> handlers;
private final String playerName;
private GameProfile profile;
private volatile ProfileFetchResult result = ProfileFetchResult.PENDING;
/**
* Constructor.
*
* @param playerName
* The name of the player whose profile is being requested.
* @param handler
* Optional handler to handle the result for the profile. Handler always invoked from the main thread.
*/
ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
Preconditions.checkNotNull(playerName);
this.playerName = playerName;
if (handler != null) {
addHandler(handler);
}
}
/**
* Add one time result handler.
*
* <p>
* Handler is always invoked from the main thread.
* </p>
*
* @param handler
* The result handler.
*/
public void addHandler(ProfileFetchHandler handler) {
Preconditions.checkNotNull(handler);
if (result != ProfileFetchResult.PENDING) {
handler.onResult(this);
return;
}
if (handlers == null)
handlers = new ArrayDeque<ProfileFetchHandler>();
handlers.addLast(handler);
}
/**
* Get the name of the player the requested profile belongs to.
*/
public String getPlayerName() {
return playerName;
}
/**
* Get the game profile that was requested.
*
* @return The game profile or null if the profile has not been retrieved yet or there was an error while retrieving
* the profile.
*/
@Nullable
public GameProfile getProfile() {
return profile;
}
/**
* Get the result of the profile fetch.
*/
public ProfileFetchResult getResult() {
return result;
}
/**
* Invoked to set the profile result.
*
* <p>
* Can be invoked from any thread, always executes on the main thread.
* </p>
*
* @param profile
* The profile. Null if there was an error.
* @param result
* The result of the request.
*/
void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) {
if (!CitizensAPI.hasImplementation())
return;
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
ProfileRequest.this.profile = profile;
ProfileRequest.this.result = result;
if (handlers == null)
return;
while (!handlers.isEmpty()) {
handlers.removeFirst().onResult(ProfileRequest.this);
}
handlers = null;
}
});
}
}

View File

@ -0,0 +1,341 @@
package net.citizensnpcs.npc.skin;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
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.event.DespawnReason;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.npc.profile.ProfileFetchHandler;
import net.citizensnpcs.npc.profile.ProfileFetcher;
import net.citizensnpcs.npc.profile.ProfileRequest;
/**
* Stores data for a single skin.
*/
public class Skin {
private int fetchRetries = -1;
private boolean hasFetched;
private volatile boolean isValid = true;
private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(15);
private BukkitTask retryTask;
private volatile Property skinData;
private volatile UUID skinId;
private final String skinName;
/**
* Constructor.
*
* @param skinName
* The name of the player the skin belongs to.
* @param forceUpdate
*/
Skin(String skinName) {
this.skinName = skinName.toLowerCase();
synchronized (CACHE) {
if (CACHE.containsKey(this.skinName))
throw new IllegalArgumentException("There is already a skin named " + skinName);
CACHE.put(this.skinName, this);
}
fetch();
}
/**
* Apply the skin data to the specified skinnable entity.
*
* <p>
* If invoked before the skin data is ready, the skin is retrieved and the skin is automatically applied to the
* entity at a later time.
* </p>
*
* @param entity
* The skinnable entity.
*
* @return True if skin was applied, false if the data is being retrieved.
*/
public boolean apply(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
NPC npc = entity.getNPC();
// Use npc cached skin if available.
// If npc requires latest skin, cache is used for faster
// availability until the latest skin can be loaded.
String cachedName = npc.data().get(CACHED_SKIN_UUID_NAME_METADATA);
String texture = npc.data().get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA, "cache");
if (this.skinName.equals(cachedName) && !texture.equals("cache")) {
Property localData = new Property("textures", texture,
npc.data().<String> get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA));
setNPCTexture(entity, localData);
// check if NPC prefers to use cached skin over the latest skin.
if (!entity.getNPC().data().get(NPC.PLAYER_SKIN_USE_LATEST, Setting.NPC_SKIN_USE_LATEST.asBoolean())) {
// cache preferred
return true;
}
}
if (!hasSkinData()) {
if (hasFetched) {
return true;
} else {
pending.put(entity, null);
return false;
}
}
setNPCSkinData(entity, skinName, skinId, skinData);
return true;
}
/**
* Apply the skin data to the specified skinnable entity and respawn the NPC.
*
* @param entity
* The skinnable entity.
*/
public void applyAndRespawn(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
if (!apply(entity))
return;
NPC npc = entity.getNPC();
if (npc.isSpawned()) {
npc.despawn(DespawnReason.PENDING_RESPAWN);
npc.spawn(npc.getStoredLocation());
}
}
private void fetch() {
final int maxRetries = Setting.MAX_NPC_SKIN_RETRIES.asInt();
if (maxRetries > -1 && fetchRetries >= maxRetries) {
if (Messaging.isDebugging()) {
Messaging.debug("Reached max skin fetch retries for '" + skinName + "'");
}
return;
}
ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() {
@Override
public void onResult(ProfileRequest request) {
hasFetched = true;
switch (request.getResult()) {
case NOT_FOUND:
isValid = false;
break;
case TOO_MANY_REQUESTS:
if (maxRetries == 0) {
break;
}
fetchRetries++;
long delay = Setting.NPC_SKIN_RETRY_DELAY.asLong();
retryTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
fetch();
}
}, delay);
if (Messaging.isDebugging()) {
Messaging.debug("Retrying skin fetch for '" + skinName + "' in " + delay + " ticks.");
}
break;
case SUCCESS:
GameProfile profile = request.getProfile();
setData(profile);
break;
default:
break;
}
}
});
}
/**
* Get the ID of the player the skin belongs to.
*
* @return The skin ID or null if it has not been retrieved yet or the skin is invalid.
*/
@Nullable
public UUID getSkinId() {
return skinId;
}
/**
* Get the name of the skin.
*/
public String getSkinName() {
return skinName;
}
/**
* Determine if the skin data has been retrieved.
*/
public boolean hasSkinData() {
return skinData != null;
}
/**
* Determine if the skin is valid.
*/
public boolean isValid() {
return isValid;
}
private void setData(@Nullable GameProfile profile) {
if (profile == null) {
isValid = false;
return;
}
if (!profile.getName().toLowerCase().equals(skinName)) {
throw new IllegalArgumentException(
"GameProfile name (" + profile.getName() + ") and " + "skin name (" + skinName + ") do not match.");
}
skinId = profile.getId();
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
List<SkinnableEntity> entities = new ArrayList<SkinnableEntity>(pending.keySet());
for (SkinnableEntity entity : entities) {
applyAndRespawn(entity);
}
pending.clear();
}
/**
* Clear all cached skins.
*/
public static void clearCache() {
synchronized (CACHE) {
for (Skin skin : CACHE.values()) {
skin.pending.clear();
if (skin.retryTask != null) {
skin.retryTask.cancel();
}
}
CACHE.clear();
}
}
/**
* Get a skin for a skinnable entity.
*
* <p>
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
* </p>
*
* @param entity
* The skinnable entity.
*/
public static Skin get(SkinnableEntity entity) {
return get(entity, false);
}
/**
* Get a skin for a skinnable entity.
*
* <p>
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
* </p>
*
* @param entity
* The skinnable entity.
* @param forceUpdate
* if the skin should be checked via the cache
*/
public static Skin get(SkinnableEntity entity, boolean forceUpdate) {
Preconditions.checkNotNull(entity);
String skinName = entity.getSkinName().toLowerCase();
return get(skinName, forceUpdate);
}
/**
* Get a player skin.
*
* <p>
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
* </p>
*
* @param skinName
* The name of the skin.
*/
public static Skin get(String skinName, boolean forceUpdate) {
Preconditions.checkNotNull(skinName);
skinName = skinName.toLowerCase();
Skin skin;
synchronized (CACHE) {
skin = CACHE.get(skinName);
}
if (skin == null) {
skin = new Skin(skinName);
} else if (forceUpdate) {
skin.fetch();
}
return skin;
}
private static void setNPCSkinData(SkinnableEntity entity, String skinName, UUID skinId, Property skinProperty) {
NPC npc = entity.getNPC();
// cache skins for faster initial skin availability and
// for use when the latest skin is not required.
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName);
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString());
if (skinProperty.getValue() != null) {
npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA, skinProperty.getValue());
npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA, skinProperty.getSignature());
setNPCTexture(entity, skinProperty);
} else {
npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA);
npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA);
}
}
private static void setNPCTexture(SkinnableEntity entity, Property skinProperty) {
GameProfile profile = entity.getProfile();
// don't set property if already set since this sometimes causes
// packet errors that disconnect the client.
Property current = Iterables.getFirst(profile.getProperties().get("textures"), null);
if (current != null && current.getValue().equals(skinProperty.getValue())
&& (current.getSignature() != null && current.getSignature().equals(skinProperty.getSignature()))) {
return;
}
profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties.
profile.getProperties().put("textures", skinProperty);
}
private static final Map<String, Skin> CACHE = new HashMap<String, Skin>(20);
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
}

View File

@ -0,0 +1,255 @@
package net.citizensnpcs.npc.skin;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import com.google.common.base.Preconditions;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.util.NMS;
/**
* Handles and synchronizes add and remove packets for Player type NPC's in order to properly apply the NPC skin.
*
* <p>
* Used as one instance per NPC entity.
* </p>
*/
public class SkinPacketTracker {
private final SkinnableEntity entity;
private final Map<UUID, PlayerEntry> inProgress = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
private boolean isRemoved;
private Skin skin;
/**
* Constructor.
*
* @param entity
* The skinnable entity the instance belongs to.
*/
public SkinPacketTracker(SkinnableEntity entity) {
Preconditions.checkNotNull(entity);
this.entity = entity;
this.skin = Skin.get(entity);
if (LISTENER == null) {
LISTENER = new PlayerListener();
Bukkit.getPluginManager().registerEvents(LISTENER, CitizensAPI.getPlugin());
}
}
/**
* Get the NPC skin.
*/
public Skin getSkin() {
return skin;
}
/**
* Notify the tracker that a remove packet has been sent to the specified player.
*
* @param playerId
* The ID of the player.
*/
void notifyRemovePacketCancelled(UUID playerId) {
inProgress.remove(playerId);
}
/**
* Notify the tracker that a remove packet has been sent to the specified player.
*
* @param playerId
* The ID of the player.
*/
void notifyRemovePacketSent(UUID playerId) {
PlayerEntry entry = inProgress.get(playerId);
if (entry == null)
return;
if (entry.removeCount == 0)
return;
entry.removeCount -= 1;
if (entry.removeCount == 0) {
inProgress.remove(playerId);
} else {
scheduleRemovePacket(entry);
}
}
/**
* Notify that the NPC skin has been changed.
*/
public void notifySkinChange(boolean forceUpdate) {
this.skin = Skin.get(entity, forceUpdate);
skin.applyAndRespawn(entity);
}
/**
* Invoke when the NPC entity is removed.
*
* <p>
* Sends remove packets to all players.
* </p>
*/
public void onRemoveNPC() {
isRemoved = true;
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
// send packet now and later to ensure removal from player list
NMS.sendTabListRemove(player, entity.getBukkitEntity());
TAB_LIST_REMOVER.sendPacket(player, entity);
}
}
/**
* Invoke when the NPC entity is spawned.
*/
public void onSpawnNPC() {
isRemoved = false;
new BukkitRunnable() {
@Override
public void run() {
if (!entity.getNPC().isSpawned())
return;
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
updateNearbyViewers(viewDistance);
}
}.runTaskLater(CitizensAPI.getPlugin(), 20);
}
private void scheduleRemovePacket(final PlayerEntry entry) {
if (isRemoved)
return;
entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
if (shouldRemoveFromTabList()) {
TAB_LIST_REMOVER.sendPacket(entry.player, entity);
}
}
}, PACKET_DELAY_REMOVE);
}
private void scheduleRemovePacket(PlayerEntry entry, int count) {
if (!shouldRemoveFromTabList())
return;
entry.removeCount = count;
scheduleRemovePacket(entry);
}
private boolean shouldRemoveFromTabList() {
return entity.getNPC().data().get("removefromtablist", Settings.Setting.DISABLE_TABLIST.asBoolean());
}
/**
* Send skin related packets to all nearby players within the specified block radius.
*
* @param radius
* The radius.
*/
public void updateNearbyViewers(double radius) {
radius *= radius;
org.bukkit.World world = entity.getBukkitEntity().getWorld();
Player from = entity.getBukkitEntity();
Location location = from.getLocation();
for (Player player : world.getPlayers()) {
if (player == null || player.hasMetadata("NPC"))
continue;
player.getLocation(CACHE_LOCATION);
if (!player.canSee(from) || !location.getWorld().equals(CACHE_LOCATION.getWorld()))
continue;
if (location.distanceSquared(CACHE_LOCATION) > radius)
continue;
updateViewer(player);
}
}
/**
* Send skin related packets to a player.
*
* @param player
* The player.
*/
public void updateViewer(final Player player) {
Preconditions.checkNotNull(player);
if (isRemoved || player.hasMetadata("NPC"))
return;
PlayerEntry entry = inProgress.get(player.getUniqueId());
if (entry != null) {
entry.cancel();
} else {
entry = new PlayerEntry(player);
}
TAB_LIST_REMOVER.cancelPackets(player, entity);
inProgress.put(player.getUniqueId(), entry);
skin.apply(entity);
NMS.sendTabListAdd(player, entity.getBukkitEntity());
scheduleRemovePacket(entry, 2);
}
private class PlayerEntry {
Player player;
int removeCount;
BukkitTask removeTask;
PlayerEntry(Player player) {
this.player = player;
}
// cancel previous packet tasks so they do not interfere with
// new tasks
void cancel() {
if (removeTask != null)
removeTask.cancel();
removeCount = 0;
}
}
private static class PlayerListener implements Listener {
@EventHandler
private void onPlayerQuit(PlayerQuitEvent event) {
// this also causes any entries in the "inProgress" field to
// be removed.
TAB_LIST_REMOVER.cancelPackets(event.getPlayer());
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static PlayerListener LISTENER;
private static final int PACKET_DELAY_REMOVE = 1;
private static final TabListRemover TAB_LIST_REMOVER = new TabListRemover();
}

View File

@ -0,0 +1,481 @@
package net.citizensnpcs.npc.skin;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.npc.NPCRegistry;
import net.citizensnpcs.util.Util;
/**
* Tracks skin updates for players.
*
* @see net.citizensnpcs.EventListen
*/
public class SkinUpdateTracker {
private final Map<SkinnableEntity, Void> navigating = new WeakHashMap<SkinnableEntity, Void>(25);
private final NPCRegistry npcRegistry;
private final Map<UUID, PlayerTracker> playerTrackers = new HashMap<UUID, PlayerTracker>(
Bukkit.getMaxPlayers() / 2);
private final Map<String, NPCRegistry> registries;
private final NPCNavigationUpdater updater = new NPCNavigationUpdater();
/**
* Constructor.
*
* @param npcRegistry
* The primary citizens registry.
* @param registries
* Map of other registries.
*/
public SkinUpdateTracker(NPCRegistry npcRegistry, Map<String, NPCRegistry> registries) {
Preconditions.checkNotNull(npcRegistry);
Preconditions.checkNotNull(registries);
this.npcRegistry = npcRegistry;
this.registries = registries;
updater.runTaskTimer(CitizensAPI.getPlugin(), 1, 1);
new NPCNavigationTracker().runTaskTimer(CitizensAPI.getPlugin(), 3, 7);
}
// determines if a player is near a skinnable entity and, if checkFov set, if the
// skinnable entity is within the players field of view.
private boolean canSee(Player player, SkinnableEntity skinnable, boolean checkFov) {
Player entity = skinnable.getBukkitEntity();
if (entity == null)
return false;
if (!player.canSee(entity))
return false;
if (!player.getWorld().equals(entity.getWorld()))
return false;
Location playerLoc = player.getLocation(CACHE_LOCATION);
Location skinLoc = entity.getLocation(NPC_LOCATION);
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
if (playerLoc.distanceSquared(skinLoc) > viewDistance)
return false;
// see if the NPC is within the players field of view
if (checkFov) {
double deltaX = skinLoc.getX() - playerLoc.getX();
double deltaZ = skinLoc.getZ() - playerLoc.getZ();
double angle = Math.atan2(deltaX, deltaZ);
float skinYaw = Util.clampYaw(-(float) Math.toDegrees(angle));
float playerYaw = Util.clampYaw(playerLoc.getYaw());
float upperBound = Util.clampYaw(playerYaw + FIELD_OF_VIEW);
float lowerBound = Util.clampYaw(playerYaw - FIELD_OF_VIEW);
if (upperBound == -180.0 && playerYaw > 0) {
upperBound = 0;
}
boolean hasMoved;
if (playerYaw - 90 < -180 || playerYaw + 90 > 180) {
hasMoved = skinYaw > lowerBound && skinYaw < upperBound;
} else {
hasMoved = skinYaw < lowerBound || skinYaw > upperBound;
}
return hasMoved;
}
return true;
}
private Iterable<NPC> getAllNPCs() {
return Iterables.filter(Iterables.concat(npcRegistry, Iterables.concat(registries.values())),
Predicates.notNull());
}
private List<SkinnableEntity> getNearbyNPCs(Player player, boolean reset, boolean checkFov) {
List<SkinnableEntity> results = new ArrayList<SkinnableEntity>();
PlayerTracker tracker = getTracker(player, reset);
for (NPC npc : getAllNPCs()) {
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
continue;
// if checking field of view, don't add skins that have already been updated for FOV
if (checkFov && tracker.fovVisibleSkins.contains(skinnable))
continue;
if (canSee(player, skinnable, checkFov)) {
results.add(skinnable);
}
}
return results;
}
// get all navigating skinnable NPC's within the players FOV that have not been "seen" yet
private void getNewVisibleNavigating(Player player, Collection<SkinnableEntity> output) {
PlayerTracker tracker = getTracker(player, false);
for (SkinnableEntity skinnable : navigating.keySet()) {
// make sure player hasn't already been updated to prevent excessive tab list flashing
// while NPC's are navigating and to reduce the number of times #canSee is invoked.
if (tracker.fovVisibleSkins.contains(skinnable))
continue;
if (canSee(player, skinnable, true))
output.add(skinnable);
}
}
@Nullable
private SkinnableEntity getSkinnable(NPC npc) {
Entity entity = npc.getEntity();
if (entity == null)
return null;
return entity instanceof SkinnableEntity ? (SkinnableEntity) entity : null;
}
// get a players tracker, create new one if not exists.
private PlayerTracker getTracker(Player player, boolean reset) {
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker == null) {
tracker = new PlayerTracker(player);
playerTrackers.put(player.getUniqueId(), tracker);
} else if (reset) {
tracker.hardReset(player);
}
return tracker;
}
/**
* Invoke when an NPC is despawned.
*
* @param npc
* The despawned NPC.
*/
public void onNPCDespawn(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.remove(skinnable);
for (PlayerTracker tracker : playerTrackers.values()) {
tracker.fovVisibleSkins.remove(skinnable);
}
}
/**
* Invoke when an NPC begins navigating.
*
* @param npc
* The navigating NPC.
*/
public void onNPCNavigationBegin(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.put(skinnable, null);
}
/**
* Invoke when an NPC finishes navigating.
*
* @param npc
* The finished NPC.
*/
public void onNPCNavigationComplete(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
navigating.remove(skinnable);
}
/**
* Invoke when an NPC is spawned.
*
* @param npc
* The spawned NPC.
*/
public void onNPCSpawn(NPC npc) {
Preconditions.checkNotNull(npc);
SkinnableEntity skinnable = getSkinnable(npc);
if (skinnable == null)
return;
// reset nearby players in case they are not looking at the NPC when it spawns.
resetNearbyPlayers(skinnable);
}
/**
* Invoke when a player moves.
*
* @param player
* The player that moved.
*/
public void onPlayerMove(Player player) {
Preconditions.checkNotNull(player);
PlayerTracker updateTracker = playerTrackers.get(player.getUniqueId());
if (updateTracker == null)
return;
if (!updateTracker.shouldUpdate(player))
return;
updatePlayer(player, 10, false);
}
/**
* Remove a player from the tracker.
*
* <p>
* Used when the player logs out.
* </p>
*
* @param playerId
* The ID of the player.
*/
public void removePlayer(UUID playerId) {
Preconditions.checkNotNull(playerId);
playerTrackers.remove(playerId);
}
/**
* Reset all players currently being tracked.
*
* <p>
* Used when Citizens is reloaded.
* </p>
*/
public void reset() {
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasMetadata("NPC"))
continue;
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker == null)
continue;
tracker.hardReset(player);
}
}
// hard reset players near a skinnable NPC
private void resetNearbyPlayers(SkinnableEntity skinnable) {
Entity entity = skinnable.getBukkitEntity();
if (entity == null || !entity.isValid())
return;
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
viewDistance *= viewDistance;
Location location = entity.getLocation(NPC_LOCATION);
List<Player> players = entity.getWorld().getPlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
Location ploc = player.getLocation(CACHE_LOCATION);
if (ploc.getWorld() != location.getWorld())
continue;
double distanceSquared = ploc.distanceSquared(location);
if (distanceSquared > viewDistance)
continue;
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
if (tracker != null) {
tracker.hardReset(player);
}
}
}
/**
* Update a player with skin related packets from nearby skinnable NPC's.
*
* @param player
* The player to update.
* @param delay
* The delay before sending the packets.
* @param reset
* True to hard reset the players tracking info, otherwise false.
*/
public void updatePlayer(final Player player, long delay, final boolean reset) {
if (player.hasMetadata("NPC"))
return;
new BukkitRunnable() {
@Override
public void run() {
List<SkinnableEntity> visible = getNearbyNPCs(player, reset, false);
for (SkinnableEntity skinnable : visible) {
skinnable.getSkinTracker().updateViewer(player);
}
}
}.runTaskLater(CitizensAPI.getPlugin(), delay);
}
// update players when the NPC navigates into their field of view
private class NPCNavigationTracker extends BukkitRunnable {
@Override
public void run() {
if (navigating.isEmpty() || playerTrackers.isEmpty())
return;
List<SkinnableEntity> nearby = new ArrayList<SkinnableEntity>(10);
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
for (Player player : players) {
if (player.hasMetadata("NPC"))
continue;
getNewVisibleNavigating(player, nearby);
for (SkinnableEntity skinnable : nearby) {
PlayerTracker tracker = getTracker(player, false);
tracker.fovVisibleSkins.add(skinnable);
updater.queue.offer(new UpdateInfo(player, skinnable));
}
nearby.clear();
}
}
}
// Updates players. Repeating task used to schedule updates without
// causing excessive scheduling.
private class NPCNavigationUpdater extends BukkitRunnable {
Queue<UpdateInfo> queue = new ArrayDeque<UpdateInfo>(20);
@Override
public void run() {
while (!queue.isEmpty()) {
UpdateInfo info = queue.remove();
info.entity.getSkinTracker().updateViewer(info.player);
}
}
}
// Tracks player location and yaw to determine when the player should be updated
// with nearby skins.
private class PlayerTracker {
final Set<SkinnableEntity> fovVisibleSkins = new HashSet<SkinnableEntity>(20);
boolean hasMoved;
final Location location = new Location(null, 0, 0, 0);
float lowerBound;
int rotationCount;
float startYaw;
float upperBound;
PlayerTracker(Player player) {
hardReset(player);
}
// reset all
void hardReset(Player player) {
this.hasMoved = false;
this.rotationCount = 0;
this.lowerBound = this.upperBound = this.startYaw = 0;
this.fovVisibleSkins.clear();
reset(player);
}
// resets initial yaw and location to the players current location and yaw.
void reset(Player player) {
player.getLocation(this.location);
if (rotationCount < 3) {
float rotationDegrees = Settings.Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
float yaw = Util.clampYaw(this.location.getYaw());
this.startYaw = yaw;
this.upperBound = Util.clampYaw(yaw + rotationDegrees);
this.lowerBound = Util.clampYaw(yaw - rotationDegrees);
if (upperBound == -180.0 && startYaw > 0) {
upperBound = 0;
}
}
}
boolean shouldUpdate(Player player) {
Location currentLoc = player.getLocation(CACHE_LOCATION);
if (!hasMoved) {
hasMoved = true;
return true;
}
if (rotationCount < 3) {
float yaw = Util.clampYaw(currentLoc.getYaw());
boolean hasRotated;
if (startYaw - 90 < -180 || startYaw + 90 > 180) {
hasRotated = yaw > lowerBound && yaw < upperBound;
} else {
hasRotated = yaw < lowerBound || yaw > upperBound;
}
// update the first 3 times the player rotates. helps load skins around player
// after the player logs/teleports.
if (hasRotated) {
rotationCount++;
reset(player);
return true;
}
}
// make sure player is in same world
if (!currentLoc.getWorld().equals(this.location.getWorld())) {
reset(player);
return true;
}
// update every time a player moves a certain distance
double distance = currentLoc.distanceSquared(this.location);
if (distance > MOVEMENT_SKIN_UPDATE_DISTANCE) {
reset(player);
return true;
} else {
return false;
}
}
}
private static class UpdateInfo {
SkinnableEntity entity;
Player player;
UpdateInfo(Player player, SkinnableEntity entity) {
this.player = player;
this.entity = entity;
}
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static final float FIELD_OF_VIEW = 70f;
private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50;
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,59 @@
package net.citizensnpcs.npc.skin;
import org.bukkit.entity.Player;
import com.mojang.authlib.GameProfile;
import net.citizensnpcs.npc.ai.NPCHolder;
/**
* Interface for player entities that are skinnable.
*/
public interface SkinnableEntity extends NPCHolder {
/**
* Get the bukkit entity.
*/
Player getBukkitEntity();
/**
* Get entity game profile.
*/
GameProfile getProfile();
/**
* Get the name of the player whose skin the NPC uses.
*/
String getSkinName();
/**
* Get the entities skin packet tracker.
*/
SkinPacketTracker getSkinTracker();
/**
* Set the bit flags that represent the skin layer parts visibility.
*
* <p>
* Setting the skin flags automatically updates the NPC skin.
* </p>
*
* @param flags
* The bit flags.
*/
void setSkinFlags(byte flags);
/**
* Set the name of the player whose skin the NPC uses.
*
* <p>
* Setting the skin name automatically updates and respawn the NPC.
* </p>
*
* @param name
* The skin name.
*/
void setSkinName(String name);
void setSkinName(String skinName, boolean forceUpdate);
}

View File

@ -0,0 +1,158 @@
package net.citizensnpcs.npc.skin;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import com.google.common.base.Preconditions;
import net.citizensnpcs.Settings;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.util.NMS;
/**
* Sends remove packets in batch per player.
*
* <p>
* Collects entities to remove and sends them all to the player in a single packet.
* </p>
*/
public class TabListRemover {
private final Map<UUID, PlayerEntry> pending = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
TabListRemover() {
Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2);
}
/**
* Cancel packets pending to be sent to the specified player.
*
* @param player
* The player.
*/
public void cancelPackets(Player player) {
Preconditions.checkNotNull(player);
PlayerEntry entry = pending.remove(player.getUniqueId());
if (entry == null)
return;
for (SkinnableEntity entity : entry.toRemove) {
entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
}
}
/**
* Cancel packets pending to be sent to the specified player for the specified skinnable entity.
*
* @param player
* The player.
* @param skinnable
* The skinnable entity.
*/
public void cancelPackets(Player player, SkinnableEntity skinnable) {
Preconditions.checkNotNull(player);
Preconditions.checkNotNull(skinnable);
PlayerEntry entry = pending.get(player.getUniqueId());
if (entry == null)
return;
if (entry.toRemove.remove(skinnable)) {
skinnable.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
}
if (entry.toRemove.isEmpty())
pending.remove(player.getUniqueId());
}
private PlayerEntry getEntry(Player player) {
PlayerEntry entry = pending.get(player.getUniqueId());
if (entry == null) {
entry = new PlayerEntry(player);
pending.put(player.getUniqueId(), entry);
}
return entry;
}
/**
* Send a remove packet to the specified player for the specified skinnable entity.
*
* @param player
* The player to send the packet to.
* @param entity
* The entity to remove.
*/
public void sendPacket(Player player, SkinnableEntity entity) {
Preconditions.checkNotNull(player);
Preconditions.checkNotNull(entity);
PlayerEntry entry = getEntry(player);
entry.toRemove.add(entity);
}
private class PlayerEntry {
Player player;
Set<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(25);
PlayerEntry(Player player) {
this.player = player;
}
}
private class Sender implements Runnable {
@Override
public void run() {
int maxPacketEntries = Settings.Setting.MAX_PACKET_ENTRIES.asInt();
Iterator<Map.Entry<UUID, PlayerEntry>> entryIterator = pending.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<UUID, PlayerEntry> mapEntry = entryIterator.next();
PlayerEntry entry = mapEntry.getValue();
int listSize = Math.min(maxPacketEntries, entry.toRemove.size());
boolean sendAll = listSize == entry.toRemove.size();
List<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
int i = 0;
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
while (skinIterator.hasNext()) {
if (i >= maxPacketEntries)
break;
SkinnableEntity skinnable = skinIterator.next();
skinnableList.add(skinnable);
skinIterator.remove();
i++;
}
if (entry.player.isOnline())
NMS.sendTabListRemove(entry.player, skinnableList);
// notify skin trackers that a remove packet has been sent to a player
for (SkinnableEntity entity : skinnableList) {
entity.getSkinTracker().notifyRemovePacketSent(entry.player.getUniqueId());
}
if (sendAll)
entryIterator.remove();
}
}
}
}

View File

@ -0,0 +1,69 @@
package net.citizensnpcs.trait;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Ageable;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
@TraitName("age")
public class Age extends Trait implements Toggleable {
@Persist
private int age = 0;
private Ageable ageable;
@Persist
private boolean locked = true;
public Age() {
super("age");
}
public void describe(CommandSender sender) {
Messaging.sendTr(sender, Messages.AGE_TRAIT_DESCRIPTION, npc.getName(), age, locked);
}
private boolean isAgeable() {
return ageable != null;
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Ageable) {
Ageable entity = (Ageable) npc.getEntity();
entity.setAge(age);
entity.setAgeLock(locked);
ageable = entity;
} else
ageable = null;
}
@Override
public void run() {
if (!locked && isAgeable()) {
age = ageable.getAge();
}
}
public void setAge(int age) {
this.age = age;
if (isAgeable()) {
ageable.setAge(age);
}
}
@Override
public boolean toggle() {
locked = !locked;
if (isAgeable())
ageable.setAgeLock(locked);
return locked;
}
@Override
public String toString() {
return "Age{age=" + age + ",locked=" + locked + "}";
}
}

View File

@ -0,0 +1,87 @@
package net.citizensnpcs.trait;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.world.WorldLoadEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Anchor;
import net.citizensnpcs.util.Messages;
@TraitName("anchors")
public class Anchors extends Trait {
private final List<Anchor> anchors = new ArrayList<Anchor>();
public Anchors() {
super("anchors");
}
public boolean addAnchor(String name, Location location) {
Anchor newAnchor = new Anchor(name, location);
if (anchors.contains(newAnchor))
return false;
anchors.add(newAnchor);
return true;
}
@EventHandler
public void checkWorld(WorldLoadEvent event) {
for (Anchor anchor : anchors)
if (!anchor.isLoaded())
anchor.load();
}
public Anchor getAnchor(String name) {
for (Anchor anchor : anchors)
if (anchor.getName().equalsIgnoreCase(name))
return anchor;
return null;
}
public List<Anchor> getAnchors() {
return anchors;
}
@Override
public void load(DataKey key) throws NPCLoadException {
for (DataKey sub : key.getRelative("list").getIntegerSubKeys()) {
String[] parts = sub.getString("").split(";");
Location location;
try {
location = new Location(Bukkit.getServer().getWorld(parts[1]), Double.valueOf(parts[2]),
Double.valueOf(parts[3]), Double.valueOf(parts[4]));
anchors.add(new Anchor(parts[0], location));
} catch (NumberFormatException e) {
Messaging.logTr(Messages.SKIPPING_INVALID_ANCHOR, sub.name(), e.getMessage());
} catch (NullPointerException e) {
// Invalid world/location/etc. Still enough data to build an
// unloaded anchor
anchors.add(new Anchor(parts[0], sub.getString("").split(";", 2)[1]));
}
}
}
public boolean removeAnchor(Anchor anchor) {
if (anchors.contains(anchor)) {
anchors.remove(anchor);
return true;
}
return false;
}
@Override
public void save(DataKey key) {
key.removeKey("list");
for (int i = 0; i < anchors.size(); i++)
key.setString("list." + String.valueOf(i), anchors.get(i).stringValue());
}
}

View File

@ -0,0 +1,108 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.ArmorStand;
import org.bukkit.util.EulerAngle;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("armorstandtrait")
public class ArmorStandTrait extends Trait {
@Persist
private EulerAngle body;
@Persist
private boolean gravity = true;
@Persist
private boolean hasarms = true;
@Persist
private boolean hasbaseplate = true;
@Persist
private EulerAngle head;
@Persist
private EulerAngle leftArm;
@Persist
private EulerAngle leftLeg;
@Persist
private boolean marker;
@Persist
private EulerAngle rightArm;
@Persist
private EulerAngle rightLeg;
@Persist
private boolean small;
@Persist
private boolean visible = true;
public ArmorStandTrait() {
super("armorstandtrait");
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof ArmorStand) {
ArmorStand entity = (ArmorStand) npc.getEntity();
if (leftArm != null) {
entity.setLeftArmPose(leftArm);
}
if (leftLeg != null) {
entity.setLeftLegPose(leftLeg);
}
if (rightArm != null) {
entity.setRightArmPose(rightArm);
}
if (rightLeg != null) {
entity.setRightLegPose(rightLeg);
}
if (body != null) {
entity.setBodyPose(body);
}
if (head != null) {
entity.setHeadPose(head);
}
}
}
@Override
public void run() {
if (npc.getEntity() instanceof ArmorStand) {
ArmorStand entity = (ArmorStand) npc.getEntity();
body = entity.getBodyPose();
leftArm = entity.getLeftArmPose();
leftLeg = entity.getLeftLegPose();
rightArm = entity.getRightArmPose();
rightLeg = entity.getRightLegPose();
head = entity.getHeadPose();
entity.setVisible(visible);
entity.setGravity(gravity);
entity.setArms(hasarms);
entity.setBasePlate(hasbaseplate);
entity.setSmall(small);
entity.setMarker(marker);
}
}
public void setGravity(boolean gravity) {
this.gravity = gravity;
}
public void setHasArms(boolean arms) {
this.hasarms = arms;
}
public void setHasBaseplate(boolean baseplate) {
this.hasbaseplate = baseplate;
}
public void setMarker(boolean marker) {
this.marker = marker;
}
public void setSmall(boolean small) {
this.small = small;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
}

View File

@ -0,0 +1,74 @@
package net.citizensnpcs.trait;
import java.util.Collection;
import java.util.List;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarFlag;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import com.google.common.collect.Lists;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.util.NMS;
@TraitName("bossbar")
public class BossBarTrait extends Trait {
@Persist("color")
private BarColor color = null;
@Persist("flags")
private List<BarFlag> flags = Lists.newArrayList();
@Persist("title")
private String title = null;
@Persist("visible")
private boolean visible = true;
public BossBarTrait() {
super("bossbar");
}
private boolean isBoss(Entity entity) {
return entity.getType() == EntityType.ENDER_DRAGON || entity.getType() == EntityType.WITHER
|| entity.getType() == EntityType.GUARDIAN;
}
@Override
public void run() {
if (!npc.isSpawned() || !isBoss(npc.getEntity()))
return;
BossBar bar = NMS.getBossBar(npc.getEntity());
bar.setVisible(visible);
if (color != null) {
bar.setColor(color);
}
if (title != null) {
bar.setTitle(title);
}
for (BarFlag flag : BarFlag.values()) {
bar.removeFlag(flag);
}
for (BarFlag flag : flags) {
bar.addFlag(flag);
}
}
public void setColor(BarColor color) {
this.color = color;
}
public void setFlags(Collection<BarFlag> flags) {
this.flags = Lists.newArrayList(flags);
}
public void setTitle(String title) {
this.title = title;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
}

View File

@ -0,0 +1,367 @@
package net.citizensnpcs.trait;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.entity.EnderDragon;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.util.Vector;
import com.google.common.collect.Maps;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.command.CommandConfigurable;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.trait.trait.Owner;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.util.NMS;
import net.citizensnpcs.util.Util;
@TraitName("controllable")
public class Controllable extends Trait implements Toggleable, CommandConfigurable {
private MovementController controller = new GroundController();
@Persist
private boolean enabled = true;
private EntityType explicitType;
@Persist("owner_required")
private boolean ownerRequired;
public Controllable() {
super("controllable");
}
public Controllable(boolean enabled) {
this();
this.enabled = enabled;
}
@Override
public void configure(CommandContext args) {
if (args.hasFlag('f')) {
explicitType = EntityType.BLAZE;
} else if (args.hasFlag('g')) {
explicitType = EntityType.OCELOT;
} else if (args.hasFlag('o')) {
explicitType = EntityType.UNKNOWN;
} else if (args.hasFlag('r')) {
explicitType = null;
} else if (args.hasValueFlag("explicittype")) {
explicitType = Util.matchEntityType(args.getFlag("explicittype"));
}
if (npc.isSpawned()) {
loadController();
}
}
private void enterOrLeaveVehicle(Player player) {
List<Entity> passengers = NMS.getPassengers(player);
if (passengers.size() > 0) {
if (passengers.contains(player)) {
player.leaveVehicle();
}
return;
}
if (ownerRequired && !npc.getTrait(Owner.class).isOwnedBy(player)) {
return;
}
NMS.mount(npc.getEntity(), player);
}
public boolean isEnabled() {
return enabled;
}
@Override
public void load(DataKey key) throws NPCLoadException {
if (key.keyExists("explicittype")) {
explicitType = Util.matchEntityType(key.getString("explicittype"));
}
}
private void loadController() {
EntityType type = npc.getEntity().getType();
if (explicitType != null)
type = explicitType;
Class<? extends MovementController> clazz = controllerTypes.get(type);
if (clazz == null) {
controller = new GroundController();
return;
}
Constructor<? extends MovementController> innerConstructor = null;
try {
innerConstructor = clazz.getConstructor(Controllable.class);
innerConstructor.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
try {
if (innerConstructor == null) {
controller = clazz.newInstance();
} else
controller = innerConstructor.newInstance(this);
} catch (Exception e) {
e.printStackTrace();
controller = new GroundController();
}
}
public boolean mount(Player toMount) {
Entity passenger = npc.getEntity().getPassenger();
if (passenger != null && passenger != toMount) {
return false;
}
enterOrLeaveVehicle(toMount);
return true;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerInteract(PlayerInteractEvent event) {
if (!npc.isSpawned() || !enabled)
return;
Action performed = event.getAction();
if (NMS.getPassengers(npc.getEntity()).contains(npc.getEntity()))
return;
switch (performed) {
case RIGHT_CLICK_BLOCK:
case RIGHT_CLICK_AIR:
controller.rightClick(event);
break;
case LEFT_CLICK_BLOCK:
case LEFT_CLICK_AIR:
controller.leftClick(event);
break;
default:
break;
}
}
@EventHandler
public void onRightClick(NPCRightClickEvent event) {
if (!enabled || !npc.isSpawned() || !event.getNPC().equals(npc))
return;
controller.rightClickEntity(event);
}
@Override
public void onSpawn() {
loadController();
}
@Override
public void run() {
if (!enabled || !npc.isSpawned())
return;
List<Entity> passengers = NMS.getPassengers(npc.getEntity());
if (passengers.size() == 0 || !(passengers.get(0) instanceof Player))
return;
controller.run((Player) passengers.get(0));
}
@Override
public void save(DataKey key) {
if (explicitType == null) {
key.removeKey("explicittype");
} else {
key.setString("explicittype", explicitType.name());
}
}
public boolean setEnabled(boolean enabled) {
this.enabled = enabled;
return enabled;
}
private void setMountedYaw(Entity entity) {
if (entity instanceof EnderDragon || !Setting.USE_BOAT_CONTROLS.asBoolean())
return; // EnderDragon handles this separately
Location loc = entity.getLocation();
Vector vel = entity.getVelocity();
double tX = loc.getX() + vel.getX();
double tZ = loc.getZ() + vel.getZ();
if (loc.getX() > tZ) {
loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ))) + 180F);
} else if (loc.getZ() < tZ) {
loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ))));
}
entity.teleport(loc);
NMS.setHeadYaw(entity, loc.getYaw());
}
public void setOwnerRequired(boolean ownerRequired) {
this.ownerRequired = ownerRequired;
}
@Override
public boolean toggle() {
enabled = !enabled;
if (!enabled && NMS.getPassengers(npc.getEntity()).size() > 0) {
NMS.getPassengers(npc.getEntity()).get(0).leaveVehicle();
}
return enabled;
}
private double updateHorizontalSpeed(Entity handle, Entity passenger, double speed, float speedMod) {
Vector hvel = handle.getVelocity();
double oldSpeed = Math.sqrt(hvel.getX() * hvel.getX() + hvel.getZ() * hvel.getZ());
double angle = Math.toRadians(passenger.getLocation().getYaw() - NMS.getVerticalMovement(passenger) * 45.0F);
hvel = hvel.add(new Vector(speedMod * -Math.sin(angle) * NMS.getHorizontalMovement(passenger) * 0.05, 0,
speedMod * Math.cos(angle) * NMS.getHorizontalMovement(passenger) * 0.05));
handle.setVelocity(hvel);
double newSpeed = Math.sqrt(hvel.getX() * hvel.getX() + hvel.getZ() * hvel.getZ());
if (newSpeed > oldSpeed && speed < 0.35D) {
return (float) Math.min(0.35D, (speed + ((0.35D - speed) / 35.0D)));
} else {
return (float) Math.max(0.07D, (speed - ((speed - 0.07D) / 35.0D)));
}
}
public class GroundController implements MovementController {
private int jumpTicks = 0;
private double speed = 0.07D;
@Override
public void leftClick(PlayerInteractEvent event) {
}
@Override
public void rightClick(PlayerInteractEvent event) {
}
@Override
public void rightClickEntity(NPCRightClickEvent event) {
enterOrLeaveVehicle(event.getClicker());
}
@Override
public void run(Player rider) {
boolean onGround = NMS.isOnGround(npc.getEntity());
float speedMod = npc.getNavigator().getDefaultParameters()
.modifiedSpeed((onGround ? GROUND_SPEED : AIR_SPEED));
speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, speedMod);
boolean shouldJump = NMS.shouldJump(rider);
if (shouldJump) {
if (onGround && jumpTicks == 0) {
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(JUMP_VELOCITY));
jumpTicks = 10;
}
} else {
jumpTicks = 0;
}
jumpTicks = Math.max(0, jumpTicks - 1);
setMountedYaw(npc.getEntity());
}
private static final float AIR_SPEED = 1.5F;
private static final float GROUND_SPEED = 4F;
private static final float JUMP_VELOCITY = 0.6F;
}
public class LookAirController implements MovementController {
private boolean paused = false;
@Override
public void leftClick(PlayerInteractEvent event) {
paused = !paused;
}
@Override
public void rightClick(PlayerInteractEvent event) {
paused = !paused;
}
@Override
public void rightClickEntity(NPCRightClickEvent event) {
enterOrLeaveVehicle(event.getClicker());
}
@Override
public void run(Player rider) {
if (paused) {
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001));
return;
}
Vector dir = rider.getEyeLocation().getDirection();
dir.multiply(npc.getNavigator().getDefaultParameters().speedModifier());
npc.getEntity().setVelocity(dir);
setMountedYaw(npc.getEntity());
}
}
public static interface MovementController {
void leftClick(PlayerInteractEvent event);
void rightClick(PlayerInteractEvent event);
void rightClickEntity(NPCRightClickEvent event);
void run(Player rider);
}
public class PlayerInputAirController implements MovementController {
private boolean paused = false;
private double speed;
@Override
public void leftClick(PlayerInteractEvent event) {
paused = !paused;
}
@Override
public void rightClick(PlayerInteractEvent event) {
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(-0.3F));
}
@Override
public void rightClickEntity(NPCRightClickEvent event) {
enterOrLeaveVehicle(event.getClicker());
}
@Override
public void run(Player rider) {
if (paused) {
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001F));
return;
}
speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, 1F);
boolean shouldJump = NMS.shouldJump(rider);
if (shouldJump) {
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.3F));
}
npc.getEntity().setVelocity(npc.getEntity().getVelocity().multiply(new Vector(1, 0.98, 1)));
}
}
public static void registerControllerType(EntityType type, Class<? extends MovementController> clazz) {
controllerTypes.put(type, clazz);
}
private static final Map<EntityType, Class<? extends MovementController>> controllerTypes = Maps
.newEnumMap(EntityType.class);
static {
controllerTypes.put(EntityType.BAT, PlayerInputAirController.class);
controllerTypes.put(EntityType.BLAZE, PlayerInputAirController.class);
controllerTypes.put(EntityType.ENDER_DRAGON, PlayerInputAirController.class);
controllerTypes.put(EntityType.GHAST, PlayerInputAirController.class);
controllerTypes.put(EntityType.WITHER, PlayerInputAirController.class);
controllerTypes.put(EntityType.UNKNOWN, LookAirController.class);
}
}

View File

@ -0,0 +1,37 @@
package net.citizensnpcs.trait;
import org.bukkit.Location;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("location")
public class CurrentLocation extends Trait {
@Persist(value = "", required = true)
private Location location = new Location(null, 0, 0, 0);
public CurrentLocation() {
super("location");
}
public Location getLocation() {
return location.getWorld() == null ? null : location;
}
@Override
public void run() {
if (!npc.isSpawned())
return;
location = npc.getEntity().getLocation(location);
}
public void setLocation(Location loc) {
this.location = loc.clone();
}
@Override
public String toString() {
return "CurrentLocation{" + location + "}";
}
}

View File

@ -0,0 +1,41 @@
package net.citizensnpcs.trait;
import org.bukkit.util.Vector;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("gravity")
public class Gravity extends Trait implements Toggleable {
@Persist
private boolean enabled;
public Gravity() {
super("gravity");
}
public void gravitate(boolean gravitate) {
enabled = gravitate;
}
public boolean hasGravity() {
return !enabled;
}
@Override
public void run() {
if (!npc.isSpawned())
return;
if (!enabled || npc.getNavigator().isNavigating())
return;
Vector vector = npc.getEntity().getVelocity();
vector.setY(Math.max(0, vector.getY()));
npc.getEntity().setVelocity(vector);
}
@Override
public boolean toggle() {
return enabled = !enabled;
}
}

View File

@ -0,0 +1,105 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Horse;
import org.bukkit.entity.Horse.Color;
import org.bukkit.entity.Horse.Style;
import org.bukkit.entity.Horse.Variant;
import org.bukkit.inventory.ItemStack;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("horsemodifiers")
public class HorseModifiers extends Trait {
@Persist("armor")
private ItemStack armor = null;
@Persist("carryingChest")
private boolean carryingChest;
@Persist("color")
private Color color = Color.CREAMY;
@Persist("saddle")
private ItemStack saddle = null;
@Persist("style")
private Style style = Style.NONE;
@Persist("type")
private Variant type = Variant.HORSE;
public HorseModifiers() {
super("horsemodifiers");
}
public ItemStack getArmor() {
return armor;
}
public Color getColor() {
return color;
}
public ItemStack getSaddle() {
return saddle;
}
public Style getStyle() {
return style;
}
public Variant getType() {
return type;
}
@Override
public void onSpawn() {
updateModifiers();
}
@Override
public void run() {
if (npc.getEntity() instanceof Horse) {
Horse horse = (Horse) npc.getEntity();
saddle = horse.getInventory().getSaddle();
armor = horse.getInventory().getArmor();
}
}
public void setArmor(ItemStack armor) {
this.armor = armor;
}
public void setCarryingChest(boolean carryingChest) {
this.carryingChest = carryingChest;
updateModifiers();
}
public void setColor(Horse.Color color) {
this.color = color;
updateModifiers();
}
public void setSaddle(ItemStack saddle) {
this.saddle = saddle;
}
public void setStyle(Horse.Style style) {
this.style = style;
updateModifiers();
}
public void setType(Horse.Variant type) {
this.type = type;
updateModifiers();
}
private void updateModifiers() {
if (npc.getEntity() instanceof Horse) {
Horse horse = (Horse) npc.getEntity();
horse.setCarryingChest(carryingChest);
horse.setColor(color);
horse.setStyle(style);
horse.setVariant(type);
horse.getInventory().setArmor(armor);
horse.getInventory().setSaddle(saddle);
}
}
}

View File

@ -0,0 +1,139 @@
package net.citizensnpcs.trait;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.command.CommandConfigurable;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.util.Util;
@TraitName("lookclose")
public class LookClose extends Trait implements Toggleable, CommandConfigurable {
private boolean enabled = Setting.DEFAULT_LOOK_CLOSE.asBoolean();
private Player lookingAt;
private double range = Setting.DEFAULT_LOOK_CLOSE_RANGE.asDouble();
private boolean realisticLooking = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
public LookClose() {
super("lookclose");
}
private boolean canSeeTarget() {
return realisticLooking && npc.getEntity() instanceof LivingEntity
? ((LivingEntity) npc.getEntity()).hasLineOfSight(lookingAt) : true;
}
@Override
public void configure(CommandContext args) {
range = args.getFlagDouble("range", range);
range = args.getFlagDouble("r", range);
realisticLooking = args.hasFlag('r');
}
private void findNewTarget() {
List<Entity> nearby = npc.getEntity().getNearbyEntities(range, range, range);
Collections.sort(nearby, new Comparator<Entity>() {
@Override
public int compare(Entity o1, Entity o2) {
Location l1 = o1.getLocation(CACHE_LOCATION);
Location l2 = o2.getLocation(CACHE_LOCATION2);
if (!NPC_LOCATION.getWorld().equals(l1.getWorld()) || !NPC_LOCATION.getWorld().equals(l2.getWorld())) {
return -1;
}
return Double.compare(l1.distanceSquared(NPC_LOCATION), l2.distanceSquared(NPC_LOCATION));
}
});
for (Entity entity : nearby) {
if (entity.getType() != EntityType.PLAYER || ((Player) entity).getGameMode() == GameMode.SPECTATOR
|| entity.getLocation(CACHE_LOCATION).getWorld() != NPC_LOCATION.getWorld()
|| CitizensAPI.getNPCRegistry().getNPC(entity) != null)
continue;
lookingAt = (Player) entity;
return;
}
}
private boolean hasInvalidTarget() {
if (lookingAt == null)
return true;
if (!lookingAt.isOnline() || lookingAt.getWorld() != npc.getEntity().getWorld()
|| lookingAt.getLocation(PLAYER_LOCATION).distanceSquared(NPC_LOCATION) > range) {
lookingAt = null;
}
return lookingAt == null;
}
@Override
public void load(DataKey key) throws NPCLoadException {
enabled = key.getBoolean("enabled", true);
range = key.getDouble("range", range);
realisticLooking = key.getBoolean("realisticlooking", key.getBoolean("realistic-looking"));
}
public void lookClose(boolean lookClose) {
enabled = lookClose;
}
@Override
public void onDespawn() {
lookingAt = null;
}
@Override
public void run() {
if (!enabled || !npc.isSpawned() || npc.getNavigator().isNavigating())
return;
npc.getEntity().getLocation(NPC_LOCATION);
if (hasInvalidTarget()) {
findNewTarget();
}
if (lookingAt != null && canSeeTarget()) {
Util.faceEntity(npc.getEntity(), lookingAt);
}
}
@Override
public void save(DataKey key) {
key.setBoolean("enabled", enabled);
key.setDouble("range", range);
key.setBoolean("realisticlooking", realisticLooking);
}
public void setRange(int range) {
this.range = range;
}
public void setRealisticLooking(boolean realistic) {
this.realisticLooking = realistic;
}
@Override
public boolean toggle() {
enabled = !enabled;
return enabled;
}
@Override
public String toString() {
return "LookClose{" + enabled + "}";
}
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
private static final Location CACHE_LOCATION2 = new Location(null, 0, 0, 0);
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
private static final Location PLAYER_LOCATION = new Location(null, 0, 0, 0);
}

View File

@ -0,0 +1,37 @@
package net.citizensnpcs.trait;
import java.util.UUID;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.npc.ai.NPCHolder;
import net.citizensnpcs.util.NMS;
@TraitName("mounttrait")
public class MountTrait extends Trait {
@Persist("mountedon")
private UUID mountedOn;
public MountTrait() {
super("mounttrait");
}
@Override
public void run() {
if (!npc.isSpawned())
return;
if (mountedOn != null) {
NPC other = CitizensAPI.getNPCRegistry().getByUniqueId(mountedOn);
if (other != null && other.isSpawned()) {
NMS.mount(other.getEntity(), npc.getEntity());
}
}
if (NMS.getVehicle(npc.getEntity()) instanceof NPCHolder) {
mountedOn = ((NPCHolder) NMS.getVehicle(npc.getEntity())).getNPC().getUniqueId();
}
}
}

View File

@ -0,0 +1,34 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Skeleton;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("skeletontype")
public class NPCSkeletonType extends Trait {
private Skeleton skeleton;
@Persist
private org.bukkit.entity.Skeleton.SkeletonType type = org.bukkit.entity.Skeleton.SkeletonType.NORMAL;
public NPCSkeletonType() {
super("skeletontype");
}
@Override
public void onSpawn() {
skeleton = npc.getEntity() instanceof Skeleton ? (Skeleton) npc.getEntity() : null;
}
@Override
public void run() {
if (skeleton != null) {
skeleton.setSkeletonType(type);
}
}
public void setType(org.bukkit.entity.Skeleton.SkeletonType type) {
this.type = type;
}
}

View File

@ -0,0 +1,43 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Ocelot;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.util.NMS;
@TraitName("ocelotmodifiers")
public class OcelotModifiers extends Trait {
@Persist("sitting")
private boolean sitting;
@Persist("type")
private Ocelot.Type type = Ocelot.Type.WILD_OCELOT;
public OcelotModifiers() {
super("ocelotmodifiers");
}
@Override
public void onSpawn() {
updateModifiers();
}
public void setSitting(boolean sit) {
this.sitting = sit;
updateModifiers();
}
public void setType(Ocelot.Type type) {
this.type = type;
updateModifiers();
}
private void updateModifiers() {
if (npc.getEntity() instanceof Ocelot) {
Ocelot ocelot = (Ocelot) npc.getEntity();
ocelot.setCatType(type);
NMS.setSitting(ocelot, sitting);
}
}
}

View File

@ -0,0 +1,104 @@
package net.citizensnpcs.trait;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import com.google.common.collect.Maps;
import net.citizensnpcs.api.command.exception.CommandException;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.api.util.Paginator;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Pose;
import net.citizensnpcs.util.Util;
@TraitName("poses")
public class Poses extends Trait {
private final Map<String, Pose> poses = Maps.newHashMap();
public Poses() {
super("poses");
}
public boolean addPose(String name, Location location) {
name = name.toLowerCase();
Pose newPose = new Pose(name, location.getPitch(), location.getYaw());
if (poses.containsValue(newPose) || poses.containsKey(name))
return false;
poses.put(name, newPose);
return true;
}
private void assumePose(float yaw, float pitch) {
if (!npc.isSpawned())
npc.spawn(npc.getTrait(CurrentLocation.class).getLocation());
Util.assumePose(npc.getEntity(), yaw, pitch);
}
public void assumePose(Location location) {
assumePose(location.getYaw(), location.getPitch());
}
public void assumePose(String flag) {
Pose pose = poses.get(flag.toLowerCase());
assumePose(pose.getYaw(), pose.getPitch());
}
public void describe(CommandSender sender, int page) throws CommandException {
Paginator paginator = new Paginator().header("Pose");
paginator.addLine("<e>Key: <a>ID <b>Name <c>Pitch/Yaw");
int i = 0;
for (Pose pose : poses.values()) {
String line = "<a>" + i + "<b> " + pose.getName() + "<c> " + pose.getPitch() + "/" + pose.getYaw();
paginator.addLine(line);
i++;
}
if (!paginator.sendPage(sender, page))
throw new CommandException(Messages.COMMAND_PAGE_MISSING);
}
public Pose getPose(String name) {
for (Pose pose : poses.values())
if (pose.getName().equalsIgnoreCase(name))
return pose;
return null;
}
public boolean hasPose(String pose) {
return poses.containsKey(pose.toLowerCase());
}
@Override
public void load(DataKey key) throws NPCLoadException {
poses.clear();
for (DataKey sub : key.getRelative("list").getIntegerSubKeys())
try {
String[] parts = sub.getString("").split(";");
poses.put(parts[0], new Pose(parts[0], Float.valueOf(parts[1]), Float.valueOf(parts[2])));
} catch (NumberFormatException e) {
Messaging.logTr(Messages.SKIPPING_INVALID_POSE, sub.name(), e.getMessage());
}
}
public boolean removePose(String pose) {
return poses.remove(pose.toLowerCase()) != null;
}
@Override
public void save(DataKey key) {
key.removeKey("list");
int i = 0;
for (Pose pose : poses.values()) {
key.setString("list." + i, pose.stringValue());
i++;
}
}
}

View File

@ -0,0 +1,36 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Creeper;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("powered")
public class Powered extends Trait implements Toggleable {
@Persist("")
private boolean powered;
public Powered() {
super("powered");
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Creeper)
((Creeper) npc.getEntity()).setPowered(powered);
}
@Override
public boolean toggle() {
powered = !powered;
if (npc.getEntity() instanceof Creeper)
((Creeper) npc.getEntity()).setPowered(powered);
return powered;
}
@Override
public String toString() {
return "Powered{" + powered + "}";
}
}

View File

@ -0,0 +1,31 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Rabbit;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("rabbittype")
public class RabbitType extends Trait {
private Rabbit rabbit;
@Persist
private Rabbit.Type type = Rabbit.Type.BROWN;
public RabbitType() {
super("rabbittype");
}
@Override
public void onSpawn() {
rabbit = npc.getEntity() instanceof Rabbit ? (Rabbit) npc.getEntity() : null;
setType(type);
}
public void setType(Rabbit.Type type) {
this.type = type;
if (rabbit != null && rabbit.isValid()) {
rabbit.setRabbitType(type);
}
}
}

View File

@ -0,0 +1,49 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Pig;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("saddle")
public class Saddle extends Trait implements Toggleable {
private boolean pig;
@Persist("")
private boolean saddle;
public Saddle() {
super("saddle");
}
@EventHandler
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (pig && npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getRightClicked())))
event.setCancelled(true);
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Pig) {
((Pig) npc.getEntity()).setSaddle(saddle);
pig = true;
} else
pig = false;
}
@Override
public boolean toggle() {
saddle = !saddle;
if (pig)
((Pig) npc.getEntity()).setSaddle(saddle);
return saddle;
}
@Override
public String toString() {
return "Saddle{" + saddle + "}";
}
}

View File

@ -0,0 +1,119 @@
package net.citizensnpcs.trait;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import net.citizensnpcs.Citizens;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.scripting.CompileCallback;
import net.citizensnpcs.api.scripting.Script;
import net.citizensnpcs.api.scripting.ScriptFactory;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
@TraitName("scripttrait")
public class ScriptTrait extends Trait {
@Persist
public List<String> files = new ArrayList<String>();
private final List<RunnableScript> runnableScripts = new ArrayList<RunnableScript>();
public ScriptTrait() {
super("scripttrait");
}
public void addScripts(List<String> scripts) {
for (String f : scripts) {
if (!files.contains(f) && validateFile(f)) {
loadScript(f);
files.add(f);
}
}
}
public List<String> getScripts() {
return files;
}
@Override
public void load(DataKey key) {
for (String file : files) {
if (validateFile(file)) {
loadScript(file);
}
}
}
public void loadScript(final String file) {
File f = new File(JavaPlugin.getPlugin(Citizens.class).getScriptFolder(), file);
CitizensAPI.getScriptCompiler().compile(f).cache(true).withCallback(new CompileCallback() {
@Override
public void onScriptCompiled(String sourceDescriptor, ScriptFactory compiled) {
final Script newInstance = compiled.newInstance();
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
try {
newInstance.invoke("onLoad", npc);
} catch (RuntimeException e) {
if (!(e.getCause() instanceof NoSuchMethodException)) {
throw e;
}
}
runnableScripts.add(new RunnableScript(newInstance, file));
}
});
}
}).beginWithFuture();
}
public void removeScripts(List<String> scripts) {
files.removeAll(scripts);
Iterator<RunnableScript> itr = runnableScripts.iterator();
while (itr.hasNext()) {
if (scripts.remove(itr.next().file)) {
itr.remove();
}
}
}
@Override
public void run() {
Iterator<RunnableScript> itr = runnableScripts.iterator();
while (itr.hasNext()) {
try {
itr.next().script.invoke("run", npc);
} catch (RuntimeException e) {
if (e.getCause() instanceof NoSuchMethodException) {
itr.remove();
} else {
throw e;
}
}
}
}
public boolean validateFile(String file) {
File f = new File(JavaPlugin.getPlugin(Citizens.class).getScriptFolder(), file);
if (!f.exists() || !f.getParentFile().equals(JavaPlugin.getPlugin(Citizens.class).getScriptFolder())) {
return false;
}
return CitizensAPI.getScriptCompiler().canCompile(f);
}
private static class RunnableScript {
String file;
Script script;
public RunnableScript(Script script, String file) {
this.script = script;
this.file = file;
}
}
}

View File

@ -0,0 +1,55 @@
package net.citizensnpcs.trait;
import org.bukkit.DyeColor;
import org.bukkit.entity.Sheep;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerShearEntityEvent;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("sheeptrait")
public class SheepTrait extends Trait {
@Persist("color")
private DyeColor color = DyeColor.WHITE;
@Persist("sheared")
private boolean sheared = false;
public SheepTrait() {
super("sheeptrait");
}
@EventHandler
public void onPlayerShearEntityEvent(PlayerShearEntityEvent event) {
if (npc != null && npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getEntity()))) {
event.setCancelled(true);
}
}
@Override
public void onSpawn() {
}
@Override
public void run() {
if (npc.getEntity() instanceof Sheep) {
Sheep sheep = (Sheep) npc.getEntity();
sheep.setSheared(sheared);
sheep.setColor(color);
}
}
public void setColor(DyeColor color) {
this.color = color;
}
public void setSheared(boolean sheared) {
this.sheared = sheared;
}
public boolean toggleSheared() {
return sheared = !sheared;
}
}

View File

@ -0,0 +1,263 @@
package net.citizensnpcs.trait;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.npc.skin.SkinnableEntity;
@TraitName("skinlayers")
public class SkinLayers extends Trait {
@Persist("cape")
private boolean cape = true;
@Persist("hat")
private boolean hat = true;
@Persist("jacket")
private boolean jacket = true;
@Persist("left-pants")
private boolean leftPants = true;
@Persist("left-sleeve")
private boolean leftSleeve = true;
@Persist("right-pants")
private boolean rightPants = true;
@Persist("right-sleeve")
private boolean rightSleeve = true;
public SkinLayers() {
super("skinlayers");
}
public SkinLayers hide() {
cape = false;
hat = false;
jacket = false;
leftSleeve = false;
rightSleeve = false;
leftPants = false;
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideCape() {
cape = false;
setFlags();
return this;
}
public SkinLayers hideHat() {
hat = false;
setFlags();
return this;
}
public SkinLayers hideJacket() {
jacket = false;
setFlags();
return this;
}
public SkinLayers hideLeftPants() {
leftPants = false;
setFlags();
return this;
}
public SkinLayers hideLeftSleeve() {
leftSleeve = false;
setFlags();
return this;
}
public SkinLayers hidePants() {
leftPants = false;
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideRightPants() {
rightPants = false;
setFlags();
return this;
}
public SkinLayers hideRightSleeve() {
rightSleeve = false;
setFlags();
return this;
}
public SkinLayers hideSleeves() {
leftSleeve = false;
rightSleeve = false;
setFlags();
return this;
}
public boolean isVisible(Layer layer) {
switch (layer) {
case CAPE:
return cape;
case JACKET:
return jacket;
case LEFT_SLEEVE:
return leftSleeve;
case RIGHT_SLEEVE:
return rightSleeve;
case LEFT_PANTS:
return leftPants;
case RIGHT_PANTS:
return rightPants;
case HAT:
return hat;
default:
return false;
}
}
@Override
public void onAttach() {
setFlags();
}
@Override
public void onSpawn() {
setFlags();
}
private void setFlags() {
if (!npc.isSpawned())
return;
SkinnableEntity skinnable = npc.getEntity() instanceof SkinnableEntity ? (SkinnableEntity) npc.getEntity()
: null;
if (skinnable == null)
return;
int flags = 0xFF;
for (Layer layer : Layer.values()) {
if (!isVisible(layer)) {
flags &= ~layer.flag;
}
}
skinnable.setSkinFlags((byte) flags);
}
public SkinLayers setVisible(Layer layer, boolean isVisible) {
switch (layer) {
case CAPE:
cape = isVisible;
break;
case JACKET:
jacket = isVisible;
break;
case LEFT_SLEEVE:
leftSleeve = isVisible;
break;
case RIGHT_SLEEVE:
rightSleeve = isVisible;
break;
case LEFT_PANTS:
leftPants = isVisible;
break;
case RIGHT_PANTS:
rightPants = isVisible;
break;
case HAT:
hat = isVisible;
break;
}
setFlags();
return this;
}
public SkinLayers show() {
cape = true;
hat = true;
jacket = true;
leftSleeve = true;
rightSleeve = true;
leftPants = true;
rightPants = true;
setFlags();
return this;
}
public SkinLayers showCape() {
cape = true;
setFlags();
return this;
}
public SkinLayers showHat() {
hat = true;
setFlags();
return this;
}
public SkinLayers showJacket() {
jacket = true;
setFlags();
return this;
}
public SkinLayers showLeftPants() {
leftPants = true;
setFlags();
return this;
}
public SkinLayers showLeftSleeve() {
leftSleeve = true;
setFlags();
return this;
}
public SkinLayers showPants() {
leftPants = true;
rightPants = true;
setFlags();
return this;
}
public SkinLayers showRightPants() {
rightPants = true;
setFlags();
return this;
}
public SkinLayers showRightSleeve() {
rightSleeve = true;
setFlags();
return this;
}
public SkinLayers showSleeves() {
leftSleeve = true;
rightSleeve = true;
setFlags();
return this;
}
@Override
public String toString() {
return "SkinLayers{cape:" + cape + ", hat:" + hat + ", jacket:" + jacket + ", leftSleeve:" + leftSleeve
+ ", rightSleeve:" + rightSleeve + ", leftPants:" + leftPants + ", rightPants:" + rightPants + "}";
}
public enum Layer {
CAPE(0),
HAT(6),
JACKET(1),
LEFT_PANTS(4),
LEFT_SLEEVE(2),
RIGHT_PANTS(5),
RIGHT_SLEEVE(3);
final int flag;
Layer(int offset) {
this.flag = 1 << offset;
}
}
}

View File

@ -0,0 +1,41 @@
package net.citizensnpcs.trait;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Slime;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
@TraitName("slimesize")
public class SlimeSize extends Trait {
@Persist
private int size = 3;
private boolean slime;
public SlimeSize() {
super("slimesize");
}
public void describe(CommandSender sender) {
Messaging.sendTr(sender, Messages.SIZE_DESCRIPTION, npc.getName(), size);
}
@Override
public void onSpawn() {
if (!(npc.getEntity() instanceof Slime)) {
slime = false;
return;
}
((Slime) npc.getEntity()).setSize(size);
slime = true;
}
public void setSize(int size) {
this.size = size;
if (slime)
((Slime) npc.getEntity()).setSize(size);
}
}

View File

@ -0,0 +1,5 @@
package net.citizensnpcs.trait;
public interface Toggleable {
public boolean toggle();
}

View File

@ -0,0 +1,57 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Villager;
import org.bukkit.entity.Villager.Profession;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
@TraitName("profession")
public class VillagerProfession extends Trait {
private Profession profession = Profession.FARMER;
public VillagerProfession() {
super("profession");
}
@Override
public void load(DataKey key) throws NPCLoadException {
try {
profession = Profession.valueOf(key.getString(""));
if (profession == Profession.NORMAL) {
profession = Profession.FARMER;
}
} catch (IllegalArgumentException ex) {
throw new NPCLoadException("Invalid profession.");
}
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Villager) {
((Villager) npc.getEntity()).setProfession(profession);
}
}
@Override
public void save(DataKey key) {
key.setString("", profession.name());
}
public void setProfession(Profession profession) {
if (profession == Profession.NORMAL) {
profession = Profession.FARMER;
}
this.profession = profession;
if (npc.getEntity() instanceof Villager) {
((Villager) npc.getEntity()).setProfession(profession);
}
}
@Override
public String toString() {
return "Profession{" + profession + "}";
}
}

View File

@ -0,0 +1,38 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Wither;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.util.NMS;
@TraitName("withertrait")
public class WitherTrait extends Trait {
@Persist("charged")
private boolean charged = false;
public WitherTrait() {
super("withertrait");
}
public boolean isCharged() {
return charged;
}
@Override
public void onSpawn() {
}
@Override
public void run() {
if (npc.getEntity() instanceof Wither) {
Wither wither = (Wither) npc.getEntity();
NMS.setWitherCharged(wither, charged);
}
}
public void setCharged(boolean charged) {
this.charged = charged;
}
}

View File

@ -0,0 +1,66 @@
package net.citizensnpcs.trait;
import org.bukkit.DyeColor;
import org.bukkit.entity.Wolf;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("wolfmodifiers")
public class WolfModifiers extends Trait {
@Persist("angry")
private boolean angry;
@Persist("collarColor")
private DyeColor collarColor = DyeColor.RED;
@Persist("sitting")
private boolean sitting;
@Persist("tamed")
private boolean tamed;
public WolfModifiers() {
super("wolfmodifiers");
}
public DyeColor getCollarColor() {
return collarColor;
}
@Override
public void onSpawn() {
updateModifiers();
}
public void setAngry(boolean angry) {
this.angry = angry;
updateModifiers();
}
public void setCollarColor(DyeColor color) {
this.collarColor = color;
updateModifiers();
}
public void setSitting(boolean sitting) {
this.sitting = sitting;
updateModifiers();
}
public void setTamed(boolean tamed) {
this.tamed = tamed;
updateModifiers();
}
private void updateModifiers() {
if (npc.getEntity() instanceof Wolf) {
Wolf wolf = (Wolf) npc.getEntity();
wolf.setCollarColor(collarColor);
wolf.setSitting(sitting);
wolf.setAngry(angry);
if (angry) {
wolf.setTarget(wolf);
}
wolf.setTamed(tamed);
}
}
}

View File

@ -0,0 +1,64 @@
package net.citizensnpcs.trait;
import org.bukkit.DyeColor;
import org.bukkit.entity.Sheep;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.SheepDyeWoolEvent;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
@TraitName("woolcolor")
public class WoolColor extends Trait {
private DyeColor color = DyeColor.WHITE;
boolean sheep = false;
public WoolColor() {
super("woolcolor");
}
@Override
public void load(DataKey key) throws NPCLoadException {
try {
color = DyeColor.valueOf(key.getString(""));
} catch (Exception ex) {
color = DyeColor.WHITE;
}
}
@EventHandler
public void onSheepDyeWool(SheepDyeWoolEvent event) {
if (npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getEntity())))
event.setCancelled(true);
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Sheep) {
((Sheep) npc.getEntity()).setColor(color);
sheep = true;
} else {
sheep = false;
}
}
@Override
public void save(DataKey key) {
key.setString("", color.name());
}
public void setColor(DyeColor color) {
this.color = color;
if (sheep) {
((Sheep) npc.getEntity()).setColor(color);
}
}
@Override
public String toString() {
return "WoolColor{" + color.name() + "}";
}
}

View File

@ -0,0 +1,58 @@
package net.citizensnpcs.trait;
import org.bukkit.entity.Villager.Profession;
import org.bukkit.entity.Zombie;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
@TraitName("zombiemodifier")
public class ZombieModifier extends Trait {
@Persist
private boolean baby;
@Persist
private Profession profession;
@Persist
private boolean villager;
private boolean zombie;
public ZombieModifier() {
super("zombiemodifier");
}
@Override
public void onSpawn() {
if (npc.getEntity() instanceof Zombie) {
((Zombie) npc.getEntity()).setVillager(villager);
((Zombie) npc.getEntity()).setBaby(baby);
((Zombie) npc.getEntity()).setVillagerProfession(profession);
zombie = true;
} else {
zombie = false;
}
}
public void setProfession(Profession profession) {
this.profession = profession;
if (zombie) {
((Zombie) npc.getEntity()).setVillagerProfession(profession);
}
}
public boolean toggleBaby() {
baby = !baby;
if (zombie) {
((Zombie) npc.getEntity()).setBaby(baby);
}
return baby;
}
public boolean toggleVillager() {
villager = !villager;
if (zombie) {
((Zombie) npc.getEntity()).setVillager(villager);
}
return villager;
}
}

View File

@ -0,0 +1,38 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.ChatColor;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.NumericPrompt;
import org.bukkit.conversations.Prompt;
import org.bukkit.entity.Player;
public class PageChangePrompt extends NumericPrompt {
private final Text text;
public PageChangePrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptValidatedInput(ConversationContext context, Number input) {
Player player = (Player) context.getForWhom();
if (!text.sendPage(player, input.intValue())) {
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_PAGE);
return new TextStartPrompt(text);
}
return (Prompt) context.getSessionData("previous");
}
@Override
public String getFailedValidationText(ConversationContext context, String input) {
return ChatColor.RED + Messaging.tr(Messages.TEXT_EDITOR_INVALID_PAGE);
}
@Override
public String getPromptText(ConversationContext context) {
return Messaging.tr(Messages.TEXT_EDITOR_PAGE_PROMPT);
}
}

View File

@ -0,0 +1,241 @@
package net.citizensnpcs.trait.text;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ConversationAbandonedListener;
import org.bukkit.conversations.ConversationFactory;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import com.google.common.collect.Maps;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.speech.SpeechContext;
import net.citizensnpcs.api.event.NPCRightClickEvent;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.api.util.Paginator;
import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.trait.Toggleable;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Util;
@TraitName("text")
public class Text extends Trait implements Runnable, Toggleable, Listener, ConversationAbandonedListener {
private final Map<UUID, Date> cooldowns = Maps.newHashMap();
private int currentIndex;
private String itemInHandPattern = "default";
private final Plugin plugin;
private boolean randomTalker = Setting.DEFAULT_RANDOM_TALKER.asBoolean();
private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble();
private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean();
private final List<String> text = new ArrayList<String>();
public Text() {
super("text");
this.plugin = CitizensAPI.getPlugin();
}
void add(String string) {
text.add(string);
}
@Override
public void conversationAbandoned(ConversationAbandonedEvent event) {
Bukkit.dispatchCommand((Player) event.getContext().getForWhom(), "npc text");
}
void edit(int index, String newText) {
text.set(index, newText);
}
public Editor getEditor(final Player player) {
final Conversation conversation = new ConversationFactory(plugin).addConversationAbandonedListener(this)
.withLocalEcho(false).withEscapeSequence("/npc text").withEscapeSequence("exit").withModality(false)
.withFirstPrompt(new TextStartPrompt(this)).buildConversation(player);
return new Editor() {
@Override
public void begin() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_BEGIN);
conversation.begin();
}
@Override
public void end() {
Messaging.sendTr(player, Messages.TEXT_EDITOR_END);
conversation.abandon();
}
};
}
boolean hasIndex(int index) {
return index >= 0 && text.size() > index;
}
@Override
public void load(DataKey key) throws NPCLoadException {
text.clear();
// TODO: legacy, remove later
for (DataKey sub : key.getIntegerSubKeys()) {
text.add(sub.getString(""));
}
for (DataKey sub : key.getRelative("text").getIntegerSubKeys()) {
text.add(sub.getString(""));
}
if (text.isEmpty()) {
populateDefaultText();
}
talkClose = key.getBoolean("talk-close", talkClose);
realisticLooker = key.getBoolean("realistic-looking", realisticLooker);
randomTalker = key.getBoolean("random-talker", randomTalker);
range = key.getDouble("range", range);
itemInHandPattern = key.getString("talkitem", itemInHandPattern);
}
@EventHandler
public void onRightClick(NPCRightClickEvent event) {
if (!event.getNPC().equals(npc))
return;
String localPattern = itemInHandPattern.equals("default") ? Setting.TALK_ITEM.asString() : itemInHandPattern;
if (Util.matchesItemInHand(event.getClicker(), localPattern) && !shouldTalkClose()) {
sendText(event.getClicker());
}
}
private void populateDefaultText() {
text.addAll(Setting.DEFAULT_TEXT.asList());
}
void remove(int index) {
text.remove(index);
}
@Override
public void run() {
if (!talkClose || !npc.isSpawned())
return;
List<Entity> nearby = npc.getEntity().getNearbyEntities(range, range, range);
for (Entity search : nearby) {
if (!(search instanceof Player) || ((Player) search).getGameMode() == GameMode.SPECTATOR)
continue;
Player player = (Player) search;
// If the cooldown is not expired, do not send text
Date cooldown = cooldowns.get(player.getUniqueId());
if (cooldown != null) {
if (!new Date().after(cooldown)) {
return;
}
cooldowns.remove(player.getUniqueId());
}
if (!sendText(player))
return;
// Add a cooldown if the text was successfully sent
Date wait = new Date();
int secondsDelta = RANDOM.nextInt(Setting.TALK_CLOSE_MAXIMUM_COOLDOWN.asInt())
+ Setting.TALK_CLOSE_MINIMUM_COOLDOWN.asInt();
if (secondsDelta <= 0)
return;
long millisecondsDelta = TimeUnit.MILLISECONDS.convert(secondsDelta, TimeUnit.SECONDS);
wait.setTime(wait.getTime() + millisecondsDelta);
cooldowns.put(player.getUniqueId(), wait);
}
}
@Override
public void save(DataKey key) {
key.setBoolean("talk-close", talkClose);
key.setBoolean("random-talker", randomTalker);
key.setBoolean("realistic-looking", realisticLooker);
key.setDouble("range", range);
key.setString("talkitem", itemInHandPattern);
// TODO: legacy, remove later
for (int i = 0; i < 100; i++)
key.removeKey(String.valueOf(i));
key.removeKey("text");
for (int i = 0; i < text.size(); i++)
key.setString("text." + String.valueOf(i), text.get(i));
}
boolean sendPage(Player player, int page) {
Paginator paginator = new Paginator().header(npc.getName() + "'s Text Entries");
for (int i = 0; i < text.size(); i++)
paginator.addLine("<a>" + i + " <7>- <e>" + text.get(i));
return paginator.sendPage(player, page);
}
private boolean sendText(Player player) {
if (!player.hasPermission("citizens.admin") && !player.hasPermission("citizens.npc.talk"))
return false;
if (text.size() == 0)
return false;
int index = 0;
if (randomTalker)
index = RANDOM.nextInt(text.size());
else {
if (currentIndex > text.size() - 1)
currentIndex = 0;
index = currentIndex++;
}
npc.getDefaultSpeechController().speak(new SpeechContext(text.get(index), player));
return true;
}
void setItemInHandPattern(String pattern) {
itemInHandPattern = pattern;
}
void setRange(double range) {
this.range = range;
}
boolean shouldTalkClose() {
return talkClose;
}
@Override
public boolean toggle() {
return (talkClose = !talkClose);
}
boolean toggleRandomTalker() {
return (randomTalker = !randomTalker);
}
boolean toggleRealisticLooking() {
return (realisticLooker = !realisticLooker);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Text{talk-close=" + talkClose + ",text=");
for (String line : text)
builder.append(line + ",");
builder.append("}");
return builder.toString();
}
private static Random RANDOM = Util.getFastRandom();
}

View File

@ -0,0 +1,30 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.ChatColor;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import org.bukkit.entity.Player;
public class TextAddPrompt extends StringPrompt {
private final Text text;
public TextAddPrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptInput(ConversationContext context, String input) {
text.add(input);
Messaging.sendTr((Player) context.getForWhom(), Messages.TEXT_EDITOR_ADDED_ENTRY, input);
return new TextStartPrompt(text);
}
@Override
public String getPromptText(ConversationContext context) {
return ChatColor.GREEN + Messaging.tr(Messages.TEXT_EDITOR_ADD_PROMPT);
}
}

View File

@ -0,0 +1,31 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
public class TextEditPrompt extends StringPrompt {
private final Text text;
public TextEditPrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptInput(ConversationContext context, String input) {
int index = (Integer) context.getSessionData("index");
text.edit(index, input);
Messaging.sendTr((CommandSender) context.getForWhom(), Messages.TEXT_EDITOR_EDITED_TEXT, index, input);
return new TextStartPrompt(text);
}
@Override
public String getPromptText(ConversationContext context) {
return ChatColor.GREEN + Messaging.tr(Messages.TEXT_EDITOR_EDIT_PROMPT);
}
}

View File

@ -0,0 +1,44 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import org.bukkit.entity.Player;
public class TextEditStartPrompt extends StringPrompt {
private final Text text;
public TextEditStartPrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptInput(ConversationContext context, String input) {
Player player = (Player) context.getForWhom();
try {
int index = Integer.parseInt(input);
if (!text.hasIndex(index)) {
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INDEX, index);
return new TextStartPrompt(text);
}
context.setSessionData("index", index);
return new TextEditPrompt(text);
} catch (NumberFormatException ex) {
if (input.equalsIgnoreCase("page")) {
context.setSessionData("previous", this);
return new PageChangePrompt(text);
}
}
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INPUT);
return new TextStartPrompt(text);
}
@Override
public String getPromptText(ConversationContext context) {
text.sendPage(((Player) context.getForWhom()), 1);
return Messaging.tr(Messages.TEXT_EDITOR_EDIT_BEGIN_PROMPT);
}
}

View File

@ -0,0 +1,45 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import org.bukkit.entity.Player;
public class TextRemovePrompt extends StringPrompt {
private final Text text;
public TextRemovePrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptInput(ConversationContext context, String input) {
Player player = (Player) context.getForWhom();
try {
int index = Integer.parseInt(input);
if (!text.hasIndex(index)) {
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INDEX, index);
return new TextStartPrompt(text);
}
text.remove(index);
Messaging.sendTr(player, Messages.TEXT_EDITOR_REMOVED_ENTRY, index);
return new TextStartPrompt(text);
} catch (NumberFormatException ex) {
if (input.equalsIgnoreCase("page")) {
context.setSessionData("previous", this);
return new PageChangePrompt(text);
}
}
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INPUT);
return new TextStartPrompt(text);
}
@Override
public String getPromptText(ConversationContext context) {
text.sendPage(((Player) context.getForWhom()), 1);
return Messaging.tr(Messages.TEXT_EDITOR_REMOVE_PROMPT);
}
}

View File

@ -0,0 +1,69 @@
package net.citizensnpcs.trait.text;
import net.citizensnpcs.Settings.Setting;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
public class TextStartPrompt extends StringPrompt {
private final Text text;
public TextStartPrompt(Text text) {
this.text = text;
}
@Override
public Prompt acceptInput(ConversationContext context, String original) {
String[] parts = ChatColor.stripColor(original.trim()).split(" ");
String input = parts[0];
CommandSender sender = (CommandSender) context.getForWhom();
if (input.equalsIgnoreCase("add"))
return new TextAddPrompt(text);
else if (input.equalsIgnoreCase("edit"))
return new TextEditStartPrompt(text);
else if (input.equalsIgnoreCase("remove"))
return new TextRemovePrompt(text);
else if (input.equalsIgnoreCase("random"))
Messaging.sendTr(sender, Messages.TEXT_EDITOR_RANDOM_TALKER_SET, text.toggleRandomTalker());
else if (input.equalsIgnoreCase("realistic looking"))
Messaging.sendTr(sender, Messages.TEXT_EDITOR_REALISTIC_LOOKING_SET, text.toggleRealisticLooking());
else if (input.equalsIgnoreCase("close") || input.equalsIgnoreCase("talk-close"))
Messaging.sendTr(sender, Messages.TEXT_EDITOR_CLOSE_TALKER_SET, text.toggle());
else if (input.equalsIgnoreCase("range")) {
try {
double range = Math.min(Math.max(0, Double.parseDouble(parts[1])), Setting.MAX_TEXT_RANGE.asDouble());
text.setRange(range);
Messaging.sendTr(sender, Messages.TEXT_EDITOR_RANGE_SET, range);
} catch (NumberFormatException e) {
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_RANGE);
} catch (ArrayIndexOutOfBoundsException e) {
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_RANGE);
}
} else if (input.equalsIgnoreCase("item")) {
if (parts.length > 1) {
text.setItemInHandPattern(parts[1]);
Messaging.sendTr(sender, Messages.TEXT_EDITOR_SET_ITEM, parts[1]);
}
} else if (input.equalsIgnoreCase("help")) {
context.setSessionData("said-text", false);
Messaging.send(sender, getPromptText(context));
} else
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_EDIT_TYPE);
return new TextStartPrompt(text);
}
@Override
public String getPromptText(ConversationContext context) {
if (context.getSessionData("said-text") == Boolean.TRUE)
return "";
String text = Messaging.tr(Messages.TEXT_EDITOR_START_PROMPT);
context.setSessionData("said-text", Boolean.TRUE);
return text;
}
}

View File

@ -0,0 +1,393 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Iterator;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.util.Vector;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.Goal;
import net.citizensnpcs.api.ai.GoalSelector;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.ai.event.NavigatorCallback;
import net.citizensnpcs.api.astar.AStarGoal;
import net.citizensnpcs.api.astar.AStarMachine;
import net.citizensnpcs.api.astar.AStarNode;
import net.citizensnpcs.api.astar.Agent;
import net.citizensnpcs.api.astar.Plan;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.PersistenceLoader;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.api.util.prtree.DistanceResult;
import net.citizensnpcs.api.util.prtree.PRTree;
import net.citizensnpcs.api.util.prtree.Region3D;
import net.citizensnpcs.api.util.prtree.SimplePointND;
import net.citizensnpcs.trait.waypoint.WaypointProvider.EnumerableWaypointProvider;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Util;
public class GuidedWaypointProvider implements EnumerableWaypointProvider {
private final List<Waypoint> available = Lists.newArrayList();
private GuidedAIGoal currentGoal;
private final List<Waypoint> helpers = Lists.newArrayList();
private NPC npc;
private boolean paused;
private PRTree<Region3D<Waypoint>> tree = PRTree.create(new Region3D.Converter<Waypoint>(), 30);
@Override
public WaypointEditor createEditor(final CommandSender sender, CommandContext args) {
if (!(sender instanceof Player)) {
Messaging.sendErrorTr(sender, Messages.COMMAND_MUST_BE_INGAME);
return null;
}
final Player player = (Player) sender;
return new WaypointEditor() {
private final WaypointMarkers markers = new WaypointMarkers(player.getWorld());
private boolean showPath;
@Override
public void begin() {
showPath();
Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_BEGIN);
}
private void createWaypointMarkers() {
for (Waypoint waypoint : Iterables.concat(available, helpers)) {
markers.createWaypointMarker(waypoint);
}
}
private void createWaypointMarkerWithData(Waypoint element) {
Entity entity = markers.createWaypointMarker(element);
if (entity == null)
return;
entity.setMetadata("citizens.waypointhashcode",
new FixedMetadataValue(CitizensAPI.getPlugin(), element.hashCode()));
}
@Override
public void end() {
Messaging.sendTr(player, Messages.GUIDED_WAYPOINT_EDITOR_END);
markers.destroyWaypointMarkers();
}
@EventHandler(ignoreCancelled = true)
public void onPlayerChat(AsyncPlayerChatEvent event) {
if (event.getMessage().equalsIgnoreCase("toggle path")) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
togglePath();
}
});
} else if (event.getMessage().equalsIgnoreCase("clear")) {
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
available.clear();
helpers.clear();
if (showPath)
markers.destroyWaypointMarkers();
}
});
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (!event.getPlayer().equals(player) || event.getAction() == Action.PHYSICAL
|| event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK
|| event.getClickedBlock() == null || event.getHand() == EquipmentSlot.OFF_HAND)
return;
if (event.getPlayer().getWorld() != npc.getEntity().getWorld())
return;
event.setCancelled(true);
Location at = event.getClickedBlock().getLocation();
Waypoint element = new Waypoint(at);
if (player.isSneaking()) {
available.add(element);
Messaging.send(player, Messages.GUIDED_WAYPOINT_EDITOR_ADDED_AVAILABLE);
} else {
helpers.add(element);
Messaging.send(player, Messages.GUIDED_WAYPOINT_EDITOR_ADDED_GUIDE);
}
createWaypointMarkerWithData(element);
rebuildTree();
}
@EventHandler(ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!event.getRightClicked().hasMetadata("citizens.waypointhashcode")
|| event.getHand() == EquipmentSlot.OFF_HAND)
return;
int hashcode = event.getRightClicked().getMetadata("citizens.waypointhashcode").get(0).asInt();
Iterator<Waypoint> itr = Iterables.concat(available, helpers).iterator();
while (itr.hasNext()) {
if (itr.next().hashCode() == hashcode) {
itr.remove();
break;
}
}
}
private void showPath() {
for (Waypoint element : Iterables.concat(available, helpers)) {
createWaypointMarkerWithData(element);
}
}
private void togglePath() {
showPath = !showPath;
if (showPath) {
createWaypointMarkers();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_SHOWING_MARKERS);
} else {
markers.destroyWaypointMarkers();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_NOT_SHOWING_MARKERS);
}
}
};
}
@Override
public boolean isPaused() {
return paused;
}
@Override
public void load(DataKey key) {
for (DataKey root : key.getRelative("availablewaypoints").getIntegerSubKeys()) {
Waypoint waypoint = PersistenceLoader.load(Waypoint.class, root);
if (waypoint == null)
continue;
available.add(waypoint);
}
for (DataKey root : key.getRelative("helperwaypoints").getIntegerSubKeys()) {
Waypoint waypoint = PersistenceLoader.load(Waypoint.class, root);
if (waypoint == null)
continue;
helpers.add(waypoint);
}
rebuildTree();
}
@Override
public void onRemove() {
npc.getDefaultGoalController().removeGoal(currentGoal);
}
@Override
public void onSpawn(NPC npc) {
this.npc = npc;
if (currentGoal == null) {
currentGoal = new GuidedAIGoal();
npc.getDefaultGoalController().addGoal(currentGoal, 1);
}
}
private void rebuildTree() {
tree = PRTree.create(new Region3D.Converter<Waypoint>(), 30);
tree.load(Lists.newArrayList(Iterables.transform(Iterables.<Waypoint> concat(available, helpers),
new Function<Waypoint, Region3D<Waypoint>>() {
@Override
public Region3D<Waypoint> apply(Waypoint arg0) {
Location loc = arg0.getLocation();
Vector root = new Vector(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
return new Region3D<Waypoint>(root, root, arg0);
}
})));
}
@Override
public void save(DataKey key) {
key.removeKey("availablewaypoints");
DataKey root = key.getRelative("availablewaypoints");
for (int i = 0; i < available.size(); ++i) {
PersistenceLoader.save(available.get(i), root.getRelative(i));
}
key.removeKey("helperwaypoints");
root = key.getRelative("helperwaypoints");
for (int i = 0; i < helpers.size(); ++i) {
PersistenceLoader.save(helpers.get(i), root.getRelative(i));
}
}
@Override
public void setPaused(boolean paused) {
this.paused = paused;
}
@Override
public Iterable<Waypoint> waypoints() {
return Iterables.concat(available, helpers);
}
private class GuidedAIGoal implements Goal {
private GuidedPlan plan;
@Override
public void reset() {
plan = null;
}
@Override
public void run(GoalSelector selector) {
if (plan.isComplete()) {
selector.finish();
return;
}
if (npc.getNavigator().isNavigating()) {
return;
}
Waypoint current = plan.getCurrentWaypoint();
npc.getNavigator().setTarget(current.getLocation());
npc.getNavigator().getLocalParameters().addSingleUseCallback(new NavigatorCallback() {
@Override
public void onCompletion(CancelReason cancelReason) {
plan.update(npc);
}
});
}
@Override
public boolean shouldExecute(GoalSelector selector) {
if (paused || available.size() == 0 || !npc.isSpawned() || npc.getNavigator().isNavigating()) {
return false;
}
Waypoint target = available.get(Util.getFastRandom().nextInt(available.size()));
plan = ASTAR.runFully(new GuidedGoal(target), new GuidedNode(new Waypoint(npc.getStoredLocation())));
return plan != null;
}
}
private static class GuidedGoal implements AStarGoal<GuidedNode> {
private final Waypoint dest;
public GuidedGoal(Waypoint dest) {
this.dest = dest;
}
@Override
public float g(GuidedNode from, GuidedNode to) {
return (float) from.distance(to.waypoint);
}
@Override
public float getInitialCost(GuidedNode node) {
return h(node);
}
@Override
public float h(GuidedNode from) {
return (float) from.distance(dest);
}
@Override
public boolean isFinished(GuidedNode node) {
return node.waypoint.equals(dest);
}
}
private class GuidedNode extends AStarNode {
private final Waypoint waypoint;
public GuidedNode(Waypoint waypoint) {
this.waypoint = waypoint;
}
@Override
public Plan buildPlan() {
return new GuidedPlan(this.<GuidedNode> getParents());
}
public double distance(Waypoint dest) {
return waypoint.distance(dest);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
GuidedNode other = (GuidedNode) obj;
if (waypoint == null) {
if (other.waypoint != null) {
return false;
}
} else if (!waypoint.equals(other.waypoint)) {
return false;
}
return true;
}
@Override
public Iterable<AStarNode> getNeighbours() {
List<DistanceResult<Region3D<Waypoint>>> res = tree.nearestNeighbour(
Region3D.<Waypoint> distanceCalculator(), Region3D.<Waypoint> alwaysAcceptNodeFilter(), 15,
new SimplePointND(waypoint.getLocation().getBlockX(), waypoint.getLocation().getBlockY(),
waypoint.getLocation().getBlockZ()));
return Iterables.transform(res, new Function<DistanceResult<Region3D<Waypoint>>, AStarNode>() {
@Override
public AStarNode apply(DistanceResult<Region3D<Waypoint>> arg0) {
return new GuidedNode(arg0.get().getData());
}
});
}
@Override
public int hashCode() {
return 31 + ((waypoint == null) ? 0 : waypoint.hashCode());
}
}
private static class GuidedPlan implements Plan {
private int index = 0;
private final Waypoint[] path;
public GuidedPlan(Iterable<GuidedNode> path) {
this.path = Iterables.toArray(Iterables.transform(path, new Function<GuidedNode, Waypoint>() {
@Override
public Waypoint apply(GuidedNode to) {
return to.waypoint;
}
}), Waypoint.class);
}
public Waypoint getCurrentWaypoint() {
return path[index];
}
@Override
public boolean isComplete() {
return index >= path.length;
}
@Override
public void update(Agent agent) {
index++;
}
}
private static final AStarMachine<GuidedNode, GuidedPlan> ASTAR = AStarMachine.createWithDefaultStorage();
}

View File

@ -0,0 +1,475 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.conversations.Conversation;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.inventory.EquipmentSlot;
import com.google.common.collect.Lists;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.ai.Goal;
import net.citizensnpcs.api.ai.GoalSelector;
import net.citizensnpcs.api.ai.Navigator;
import net.citizensnpcs.api.ai.event.CancelReason;
import net.citizensnpcs.api.ai.event.NavigatorCallback;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.command.exception.CommandException;
import net.citizensnpcs.api.event.NPCDespawnEvent;
import net.citizensnpcs.api.event.NPCRemoveEvent;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.PersistenceLoader;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.trait.waypoint.WaypointProvider.EnumerableWaypointProvider;
import net.citizensnpcs.trait.waypoint.triggers.TriggerEditPrompt;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.Util;
public class LinearWaypointProvider implements EnumerableWaypointProvider {
private LinearWaypointGoal currentGoal;
private NPC npc;
private final List<Waypoint> waypoints = Lists.newArrayList();
@Override
public WaypointEditor createEditor(CommandSender sender, CommandContext args) {
if (args.hasFlag('h')) {
try {
if (args.getSenderLocation() != null) {
waypoints.add(new Waypoint(args.getSenderLocation()));
}
} catch (CommandException e) {
Messaging.sendError(sender, e.getMessage());
}
return null;
} else if (args.hasValueFlag("at")) {
try {
Location location = CommandContext.parseLocation(args.getSenderLocation(), args.getFlag("at"));
if (location != null) {
waypoints.add(new Waypoint(location));
}
} catch (CommandException e) {
Messaging.sendError(sender, e.getMessage());
}
return null;
} else if (args.hasFlag('c')) {
waypoints.clear();
return null;
} else if (args.hasFlag('l')) {
if (waypoints.size() > 0) {
waypoints.remove(waypoints.size() - 1);
}
return null;
} else if (args.hasFlag('p')) {
setPaused(!isPaused());
return null;
} else if (!(sender instanceof Player)) {
Messaging.sendErrorTr(sender, Messages.COMMAND_MUST_BE_INGAME);
return null;
}
return new LinearWaypointEditor((Player) sender);
}
public Waypoint getCurrentWaypoint() {
if (currentGoal != null && currentGoal.currentDestination != null) {
return currentGoal.currentDestination;
}
return null;
}
@Override
public boolean isPaused() {
return currentGoal.isPaused();
}
@Override
public void load(DataKey key) {
for (DataKey root : key.getRelative("points").getIntegerSubKeys()) {
Waypoint waypoint = PersistenceLoader.load(Waypoint.class, root);
if (waypoint == null)
continue;
waypoints.add(waypoint);
}
}
@Override
public void onRemove() {
npc.getDefaultGoalController().removeGoal(currentGoal);
}
@Override
public void onSpawn(NPC npc) {
this.npc = npc;
if (currentGoal == null) {
currentGoal = new LinearWaypointGoal();
npc.getDefaultGoalController().addGoal(currentGoal, 1);
}
}
@Override
public void save(DataKey key) {
key.removeKey("points");
key = key.getRelative("points");
for (int i = 0; i < waypoints.size(); ++i) {
PersistenceLoader.save(waypoints.get(i), key.getRelative(i));
}
}
@Override
public void setPaused(boolean paused) {
currentGoal.setPaused(paused);
}
@Override
public Iterable<Waypoint> waypoints() {
return waypoints;
}
private final class LinearWaypointEditor extends WaypointEditor {
Conversation conversation;
boolean editing = true;
int editingSlot = waypoints.size() - 1;
WaypointMarkers markers;
private final Player player;
private boolean showPath;
private LinearWaypointEditor(Player player) {
this.player = player;
this.markers = new WaypointMarkers(player.getWorld());
}
@Override
public void begin() {
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_BEGIN);
}
private void clearWaypoints() {
editingSlot = 0;
waypoints.clear();
onWaypointsModified();
markers.destroyWaypointMarkers();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_WAYPOINTS_CLEARED);
}
private void createWaypointMarkers() {
for (int i = 0; i < waypoints.size(); i++) {
markers.createWaypointMarker(waypoints.get(i));
}
}
@Override
public void end() {
if (!editing)
return;
if (conversation != null)
conversation.abandon();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_END);
editing = false;
if (!showPath)
return;
markers.destroyWaypointMarkers();
}
private String formatLoc(Location location) {
return String.format("[[%d]], [[%d]], [[%d]]", location.getBlockX(), location.getBlockY(),
location.getBlockZ());
}
@Override
public Waypoint getCurrentWaypoint() {
if (waypoints.size() == 0 || !editing) {
return null;
}
normaliseEditingSlot();
return waypoints.get(editingSlot);
}
private Location getPreviousWaypoint(int fromSlot) {
if (waypoints.size() <= 1)
return null;
if (--fromSlot < 0)
fromSlot = waypoints.size() - 1;
return waypoints.get(fromSlot).getLocation();
}
private void normaliseEditingSlot() {
editingSlot = Math.max(0, Math.min(waypoints.size() - 1, editingSlot));
}
@EventHandler
public void onNPCDespawn(NPCDespawnEvent event) {
if (event.getNPC().equals(npc)) {
Editor.leave(player);
}
}
@EventHandler
public void onNPCRemove(NPCRemoveEvent event) {
if (event.getNPC().equals(npc)) {
Editor.leave(player);
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerChat(AsyncPlayerChatEvent event) {
if (!event.getPlayer().equals(player))
return;
String message = event.getMessage();
if (message.equalsIgnoreCase("triggers")) {
event.setCancelled(true);
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
conversation = TriggerEditPrompt.start(player, LinearWaypointEditor.this);
}
});
} else if (message.equalsIgnoreCase("clear")) {
event.setCancelled(true);
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
clearWaypoints();
}
});
} else if (message.equalsIgnoreCase("toggle path")) {
event.setCancelled(true);
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
// we need to spawn entities on the main thread.
togglePath();
}
});
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (!event.getPlayer().equals(player) || event.getAction() == Action.PHYSICAL || !npc.isSpawned()
|| event.getPlayer().getWorld() != npc.getEntity().getWorld()
|| event.getHand() == EquipmentSlot.OFF_HAND)
return;
if (event.getAction() == Action.LEFT_CLICK_BLOCK || event.getAction() == Action.LEFT_CLICK_AIR) {
if (event.getClickedBlock() == null)
return;
event.setCancelled(true);
Location at = event.getClickedBlock().getLocation();
Location prev = getPreviousWaypoint(editingSlot);
if (prev != null) {
double distance = at.distanceSquared(prev);
double maxDistance = Math.pow(npc.getNavigator().getDefaultParameters().range(), 2);
if (distance > maxDistance) {
Messaging.sendErrorTr(player, Messages.LINEAR_WAYPOINT_EDITOR_RANGE_EXCEEDED,
Math.sqrt(distance), Math.sqrt(maxDistance), ChatColor.RED);
return;
}
}
Waypoint element = new Waypoint(at);
normaliseEditingSlot();
waypoints.add(editingSlot, element);
if (showPath) {
markers.createWaypointMarker(element);
}
editingSlot = Math.min(editingSlot + 1, waypoints.size());
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_ADDED_WAYPOINT, formatLoc(at), editingSlot + 1,
waypoints.size());
} else if (waypoints.size() > 0) {
event.setCancelled(true);
normaliseEditingSlot();
Waypoint waypoint = waypoints.remove(editingSlot);
if (showPath) {
markers.removeWaypointMarker(waypoint);
}
editingSlot = Math.max(0, editingSlot - 1);
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_REMOVED_WAYPOINT, waypoints.size(),
editingSlot + 1);
}
onWaypointsModified();
}
@EventHandler(ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!player.equals(event.getPlayer()) || !showPath || event.getHand() == EquipmentSlot.OFF_HAND)
return;
if (!event.getRightClicked().hasMetadata("waypointindex"))
return;
editingSlot = event.getRightClicked().getMetadata("waypointindex").get(0).asInt();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_EDIT_SLOT_SET, editingSlot,
formatLoc(waypoints.get(editingSlot).getLocation()));
}
@EventHandler
public void onPlayerItemHeldChange(PlayerItemHeldEvent event) {
if (!event.getPlayer().equals(player) || waypoints.size() == 0)
return;
int previousSlot = event.getPreviousSlot(), newSlot = event.getNewSlot();
// handle wrap-arounds
if (previousSlot == 0 && newSlot == LARGEST_SLOT) {
editingSlot--;
} else if (previousSlot == LARGEST_SLOT && newSlot == 0) {
editingSlot++;
} else {
int diff = newSlot - previousSlot;
if (Math.abs(diff) != 1)
return; // the player isn't scrolling
editingSlot += diff > 0 ? 1 : -1;
}
normaliseEditingSlot();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_EDIT_SLOT_SET, editingSlot,
formatLoc(waypoints.get(editingSlot).getLocation()));
}
private void onWaypointsModified() {
if (currentGoal != null) {
currentGoal.onProviderChanged();
}
}
private void togglePath() {
showPath = !showPath;
if (showPath) {
createWaypointMarkers();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_SHOWING_MARKERS);
} else {
markers.destroyWaypointMarkers();
Messaging.sendTr(player, Messages.LINEAR_WAYPOINT_EDITOR_NOT_SHOWING_MARKERS);
}
}
private static final int LARGEST_SLOT = 8;
}
private class LinearWaypointGoal implements Goal {
private final Location cachedLocation = new Location(null, 0, 0, 0);
private Waypoint currentDestination;
private Iterator<Waypoint> itr;
private boolean paused;
private GoalSelector selector;
private void ensureItr() {
if (itr == null) {
itr = getUnsafeIterator();
} else if (!itr.hasNext()) {
itr = getNewIterator();
}
}
private Navigator getNavigator() {
return npc.getNavigator();
}
private Iterator<Waypoint> getNewIterator() {
LinearWaypointsCompleteEvent event = new LinearWaypointsCompleteEvent(LinearWaypointProvider.this,
getUnsafeIterator());
Bukkit.getPluginManager().callEvent(event);
Iterator<Waypoint> next = event.getNextWaypoints();
return next;
}
private Iterator<Waypoint> getUnsafeIterator() {
return new Iterator<Waypoint>() {
int idx = 0;
@Override
public boolean hasNext() {
return idx < waypoints.size();
}
@Override
public Waypoint next() {
return waypoints.get(idx++);
}
@Override
public void remove() {
waypoints.remove(Math.max(0, idx - 1));
}
};
}
public boolean isPaused() {
return paused;
}
public void onProviderChanged() {
itr = getUnsafeIterator();
if (currentDestination != null) {
if (selector != null) {
selector.finish();
}
if (npc != null && npc.getNavigator().isNavigating()) {
npc.getNavigator().cancelNavigation();
}
}
}
@Override
public void reset() {
currentDestination = null;
selector = null;
}
@Override
public void run(GoalSelector selector) {
if (!getNavigator().isNavigating()) {
selector.finish();
}
}
public void setPaused(boolean pause) {
if (pause && currentDestination != null) {
selector.finish();
}
paused = pause;
}
@Override
public boolean shouldExecute(final GoalSelector selector) {
if (paused || currentDestination != null || !npc.isSpawned() || getNavigator().isNavigating()) {
return false;
}
ensureItr();
boolean shouldExecute = itr.hasNext();
if (!shouldExecute) {
return false;
}
this.selector = selector;
Waypoint next = itr.next();
Location npcLoc = npc.getEntity().getLocation(cachedLocation);
if (npcLoc.getWorld() != next.getLocation().getWorld() || npcLoc.distanceSquared(next.getLocation()) < npc
.getNavigator().getLocalParameters().distanceMargin()) {
return false;
}
currentDestination = next;
getNavigator().setTarget(currentDestination.getLocation());
getNavigator().getLocalParameters().addSingleUseCallback(new NavigatorCallback() {
@Override
public void onCompletion(@Nullable CancelReason cancelReason) {
if (npc.isSpawned() && currentDestination != null && Util
.locationWithinRange(npc.getEntity().getLocation(), currentDestination.getLocation(), 4)) {
currentDestination.onReach(npc);
}
selector.finish();
}
});
return true;
}
}
}

View File

@ -0,0 +1,40 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Iterator;
import net.citizensnpcs.api.event.CitizensEvent;
import org.bukkit.event.HandlerList;
public class LinearWaypointsCompleteEvent extends CitizensEvent {
private Iterator<Waypoint> next;
private final WaypointProvider provider;
public LinearWaypointsCompleteEvent(WaypointProvider provider, Iterator<Waypoint> next) {
this.next = next;
this.provider = provider;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public Iterator<Waypoint> getNextWaypoints() {
return next;
}
public WaypointProvider getWaypointProvider() {
return provider;
}
public void setNextWaypoints(Iterator<Waypoint> waypoints) {
this.next = waypoints;
}
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,71 @@
package net.citizensnpcs.trait.waypoint;
import org.bukkit.command.CommandSender;
import net.citizensnpcs.api.ai.Goal;
import net.citizensnpcs.api.ai.goals.WanderGoal;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.util.DataKey;
public class WanderWaypointProvider implements WaypointProvider {
private Goal currentGoal;
private NPC npc;
private volatile boolean paused;
@Persist
private final int xrange = DEFAULT_XRANGE;
@Persist
private final int yrange = DEFAULT_YRANGE;
@Override
public WaypointEditor createEditor(CommandSender sender, CommandContext args) {
return new WaypointEditor() {
@Override
public void begin() {
// TODO Auto-generated method stub
}
@Override
public void end() {
// TODO Auto-generated method stub
}
};
}
@Override
public boolean isPaused() {
return paused;
}
@Override
public void load(DataKey key) {
}
@Override
public void onRemove() {
npc.getDefaultGoalController().removeGoal(currentGoal);
}
@Override
public void onSpawn(NPC npc) {
this.npc = npc;
if (currentGoal == null) {
currentGoal = WanderGoal.createWithNPCAndRange(npc, xrange, yrange);
}
npc.getDefaultGoalController().addGoal(currentGoal, 1);
}
@Override
public void save(DataKey key) {
}
@Override
public void setPaused(boolean paused) {
this.paused = paused;
}
private static final int DEFAULT_XRANGE = 3;
private static final int DEFAULT_YRANGE = 25;
}

View File

@ -0,0 +1,113 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Collections;
import java.util.List;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.persistence.PersistenceLoader;
import net.citizensnpcs.trait.waypoint.triggers.DelayTrigger;
import net.citizensnpcs.trait.waypoint.triggers.WaypointTrigger;
import net.citizensnpcs.trait.waypoint.triggers.WaypointTriggerRegistry;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import com.google.common.collect.Lists;
public class Waypoint {
@Persist(required = true)
private Location location;
@Persist
private List<WaypointTrigger> triggers;
public Waypoint() {
}
public Waypoint(Location at) {
location = at;
}
public void addTrigger(WaypointTrigger trigger) {
if (triggers == null)
triggers = Lists.newArrayList();
triggers.add(trigger);
}
public double distance(Waypoint dest) {
return location.distance(dest.location);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Waypoint other = (Waypoint) obj;
if (location == null) {
if (other.location != null) {
return false;
}
} else if (!location.equals(other.location)) {
return false;
}
if (triggers == null) {
if (other.triggers != null) {
return false;
}
} else if (!triggers.equals(other.triggers)) {
return false;
}
return true;
}
public Location getLocation() {
return location;
}
@SuppressWarnings("unchecked")
public List<WaypointTrigger> getTriggers() {
return triggers == null ? Collections.EMPTY_LIST : triggers;
}
@Override
public int hashCode() {
final int prime = 31;
int result = prime + ((location == null) ? 0 : location.hashCode());
return prime * result + ((triggers == null) ? 0 : triggers.hashCode());
}
public void onReach(NPC npc) {
if (triggers == null)
return;
runTriggers(npc, 0);
}
private void runTriggers(final NPC npc, int start) {
for (int i = start; i < triggers.size(); i++) {
WaypointTrigger trigger = triggers.get(i);
trigger.onWaypointReached(npc, location);
if (!(trigger instanceof DelayTrigger))
continue;
int delay = ((DelayTrigger) trigger).getDelay();
if (delay <= 0)
continue;
final int newStart = i;
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
runTriggers(npc, newStart);
}
}, delay);
break;
}
}
static {
PersistenceLoader.registerPersistDelegate(WaypointTrigger.class, WaypointTriggerRegistry.class);
}
}

View File

@ -0,0 +1,9 @@
package net.citizensnpcs.trait.waypoint;
import net.citizensnpcs.editor.Editor;
public abstract class WaypointEditor extends Editor {
public Waypoint getCurrentWaypoint() {
return null;
}
}

View File

@ -0,0 +1,52 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import com.google.common.collect.Maps;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.MemoryNPCDataStore;
import net.citizensnpcs.api.npc.NPC;
public class WaypointMarkers {
private final Map<Waypoint, Entity> waypointMarkers = Maps.newHashMap();
private final World world;
public WaypointMarkers(World world) {
this.world = world;
}
public Entity createWaypointMarker(Waypoint waypoint) {
Entity entity = spawnMarker(world, waypoint.getLocation().clone().add(0, 1, 0));
if (entity == null)
return null;
waypointMarkers.put(waypoint, entity);
return entity;
}
public void destroyWaypointMarkers() {
for (Entity entity : waypointMarkers.values()) {
entity.remove();
}
waypointMarkers.clear();
}
public void removeWaypointMarker(Waypoint waypoint) {
Entity entity = waypointMarkers.remove(waypoint);
if (entity != null) {
entity.remove();
}
}
public Entity spawnMarker(World world, Location at) {
NPC npc = CitizensAPI.createAnonymousNPCRegistry(new MemoryNPCDataStore()).createNPC(EntityType.ENDER_SIGNAL,
"");
npc.spawn(at);
return npc.getEntity();
}
}

View File

@ -0,0 +1,51 @@
package net.citizensnpcs.trait.waypoint;
import org.bukkit.command.CommandSender;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persistable;
public interface WaypointProvider extends Persistable {
/**
* Creates an {@link WaypointEditor} with the given {@link CommandSender}.
*
* @param sender
* The player to link the editor with
* @param args
* @return The editor
*/
public WaypointEditor createEditor(CommandSender sender, CommandContext args);
/**
* Returns whether this provider has paused execution of waypoints.
*
* @return Whether the provider is paused.
*/
public boolean isPaused();
/**
* Called when the provider is removed from the NPC.
*/
public void onRemove();
/**
* Called when the {@link NPC} attached to this provider is spawned.
*
* @param npc
* The attached NPC
*/
public void onSpawn(NPC npc);
/**
* Pauses waypoint execution.
*
* @param paused
* Whether to pause waypoint execution.
*/
public void setPaused(boolean paused);
public static interface EnumerableWaypointProvider extends WaypointProvider {
public Iterable<Waypoint> waypoints();
}
}

View File

@ -0,0 +1,137 @@
package net.citizensnpcs.trait.waypoint;
import java.util.Map;
import java.util.Map.Entry;
import org.bukkit.command.CommandSender;
import com.google.common.collect.Maps;
import net.citizensnpcs.api.command.CommandContext;
import net.citizensnpcs.api.exception.NPCLoadException;
import net.citizensnpcs.api.persistence.PersistenceLoader;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.trait.TraitName;
import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.editor.Editor;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.StringHelper;
@TraitName("waypoints")
public class Waypoints extends Trait {
private WaypointProvider provider = new LinearWaypointProvider();
private String providerName = "linear";
public Waypoints() {
super("waypoints");
}
private WaypointProvider create(Class<? extends WaypointProvider> clazz) {
try {
return clazz.newInstance();
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
public void describeProviders(CommandSender sender) {
Messaging.sendTr(sender, Messages.AVAILABLE_WAYPOINT_PROVIDERS);
for (String name : providers.keySet()) {
Messaging.send(sender, " - " + StringHelper.wrap(name));
}
}
/**
* Returns the current {@link WaypointProvider}. May be null during initialisation.
*
* @return The current provider
*/
public WaypointProvider getCurrentProvider() {
return provider;
}
/**
* @return The current provider name
*/
public String getCurrentProviderName() {
return providerName;
}
public Editor getEditor(CommandSender player, CommandContext args) {
return provider.createEditor(player, args);
}
@Override
public void load(DataKey key) throws NPCLoadException {
provider = null;
providerName = key.getString("provider", "linear");
for (Entry<String, Class<? extends WaypointProvider>> entry : providers.entrySet()) {
if (entry.getKey().equals(providerName)) {
provider = create(entry.getValue());
break;
}
}
if (provider == null)
return;
PersistenceLoader.load(provider, key.getRelative(providerName));
}
@Override
public void onSpawn() {
if (provider != null) {
provider.onSpawn(getNPC());
}
}
@Override
public void save(DataKey key) {
if (provider == null)
return;
PersistenceLoader.save(provider, key.getRelative(providerName));
key.setString("provider", providerName);
}
/**
* Sets the current {@link WaypointProvider} using the given name.
*
* @param name
* The name of the waypoint provider, registered using {@link #registerWaypointProvider(Class, String)}
* @return Whether the operation succeeded
*/
public boolean setWaypointProvider(String name) {
name = name.toLowerCase();
Class<? extends WaypointProvider> clazz = providers.get(name);
if (provider != null) {
provider.onRemove();
}
if (clazz == null || (provider = create(clazz)) == null)
return false;
providerName = name;
if (npc != null && npc.isSpawned()) {
provider.onSpawn(npc);
}
return true;
}
/**
* Registers a {@link WaypointProvider}, which can be subsequently used by NPCs.
*
* @param clazz
* The class of the waypoint provider
* @param name
* The name of the waypoint provider
*/
public static void registerWaypointProvider(Class<? extends WaypointProvider> clazz, String name) {
providers.put(name, clazz);
}
private static final Map<String, Class<? extends WaypointProvider>> providers = Maps.newHashMap();
static {
providers.put("linear", LinearWaypointProvider.class);
providers.put("wander", WanderWaypointProvider.class);
providers.put("guided", GuidedWaypointProvider.class);
}
}

View File

@ -0,0 +1,42 @@
package net.citizensnpcs.trait.waypoint.triggers;
import java.util.Collection;
import java.util.List;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.util.PlayerAnimation;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
public class AnimationTrigger implements WaypointTrigger {
@Persist(required = true)
private List<PlayerAnimation> animations;
public AnimationTrigger() {
}
public AnimationTrigger(Collection<PlayerAnimation> collection) {
animations = Lists.newArrayList(collection);
}
@Override
public String description() {
return String.format("Animation Trigger [animating %s]", Joiner.on(", ").join(animations));
}
@Override
public void onWaypointReached(NPC npc, Location waypoint) {
if (npc.getEntity().getType() != EntityType.PLAYER)
return;
Player player = (Player) npc.getEntity();
for (PlayerAnimation animation : animations) {
animation.play(player);
}
}
}

View File

@ -0,0 +1,49 @@
package net.citizensnpcs.trait.waypoint.triggers;
import java.util.List;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import net.citizensnpcs.util.PlayerAnimation;
import net.citizensnpcs.util.Util;
import org.bukkit.command.CommandSender;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
public class AnimationTriggerPrompt extends StringPrompt implements WaypointTriggerPrompt {
private final List<PlayerAnimation> animations = Lists.newArrayList();
@Override
public Prompt acceptInput(ConversationContext context, String input) {
if (input.equalsIgnoreCase("back")) {
return (Prompt) context.getSessionData("previous");
}
if (input.equalsIgnoreCase("finish")) {
context.setSessionData(WaypointTriggerPrompt.CREATED_TRIGGER_KEY, new AnimationTrigger(animations));
return (Prompt) context.getSessionData(WaypointTriggerPrompt.RETURN_PROMPT_KEY);
}
PlayerAnimation animation = Util.matchEnum(PlayerAnimation.values(), input);
if (animation == null) {
Messaging.sendErrorTr((CommandSender) context.getForWhom(), Messages.INVALID_ANIMATION, input,
getValidAnimations());
}
animations.add(animation);
Messaging.sendTr((CommandSender) context.getForWhom(), Messages.ANIMATION_ADDED, input);
return this;
}
@Override
public String getPromptText(ConversationContext context) {
Messaging.sendTr((CommandSender) context.getForWhom(), Messages.ANIMATION_TRIGGER_PROMPT, getValidAnimations());
return "";
}
private String getValidAnimations() {
return Joiner.on(", ").join(PlayerAnimation.values());
}
}

View File

@ -0,0 +1,52 @@
package net.citizensnpcs.trait.waypoint.triggers;
import java.util.Collection;
import java.util.List;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.api.util.Messaging;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
public class ChatTrigger implements WaypointTrigger {
@Persist(required = true)
private List<String> lines;
@Persist
private double radius = -1;
public ChatTrigger() {
}
public ChatTrigger(double radius, Collection<String> chatLines) {
this.radius = radius;
lines = Lists.newArrayList(chatLines);
}
@Override
public String description() {
return String.format("Chat Trigger [radius %d, %s]", radius, Joiner.on(", ").join(lines));
}
@Override
public void onWaypointReached(NPC npc, Location waypoint) {
if (radius < 0) {
for (Player player : npc.getEntity().getWorld().getPlayers()) {
for (String line : lines)
Messaging.send(player, line);
}
} else {
for (Entity entity : npc.getEntity().getNearbyEntities(radius, radius, radius)) {
if (!(entity instanceof Player))
continue;
for (String line : lines)
Messaging.send((Player) entity, line);
}
}
}
}

View File

@ -0,0 +1,47 @@
package net.citizensnpcs.trait.waypoint.triggers;
import java.util.List;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.command.CommandSender;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.Prompt;
import org.bukkit.conversations.StringPrompt;
import com.google.common.collect.Lists;
public class ChatTriggerPrompt extends StringPrompt implements WaypointTriggerPrompt {
private final List<String> lines = Lists.newArrayList();
private double radius = -1;
@Override
public Prompt acceptInput(ConversationContext context, String input) {
if (input.equalsIgnoreCase("back"))
return (Prompt) context.getSessionData("previous");
if (input.startsWith("radius")) {
try {
radius = Double.parseDouble(input.split(" ")[1]);
} catch (NumberFormatException e) {
Messaging.sendErrorTr((CommandSender) context.getForWhom(),
Messages.WAYPOINT_TRIGGER_CHAT_INVALID_RADIUS);
} catch (IndexOutOfBoundsException e) {
Messaging.sendErrorTr((CommandSender) context.getForWhom(), Messages.WAYPOINT_TRIGGER_CHAT_NO_RADIUS);
}
return this;
}
if (input.equalsIgnoreCase("finish")) {
context.setSessionData(WaypointTriggerPrompt.CREATED_TRIGGER_KEY, new ChatTrigger(radius, lines));
return (Prompt) context.getSessionData(WaypointTriggerPrompt.RETURN_PROMPT_KEY);
}
lines.add(input);
return this;
}
@Override
public String getPromptText(ConversationContext context) {
Messaging.sendTr((CommandSender) context.getForWhom(), Messages.CHAT_TRIGGER_PROMPT);
return "";
}
}

View File

@ -0,0 +1,48 @@
package net.citizensnpcs.trait.waypoint.triggers;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.citizensnpcs.api.persistence.Persist;
import net.citizensnpcs.trait.waypoint.WaypointProvider;
import net.citizensnpcs.trait.waypoint.Waypoints;
import org.bukkit.Bukkit;
import org.bukkit.Location;
public class DelayTrigger implements WaypointTrigger {
@Persist
private int delay = 0;
public DelayTrigger() {
}
public DelayTrigger(int delay) {
this.delay = delay;
}
@Override
public String description() {
return String.format("Delay for %d ticks", delay);
}
public int getDelay() {
return delay;
}
@Override
public void onWaypointReached(NPC npc, Location waypoint) {
if (delay > 0) {
scheduleTask(npc.getTrait(Waypoints.class).getCurrentProvider());
}
}
private void scheduleTask(final WaypointProvider provider) {
provider.setPaused(true);
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
@Override
public void run() {
provider.setPaused(false);
}
}, delay);
}
}

View File

@ -0,0 +1,22 @@
package net.citizensnpcs.trait.waypoint.triggers;
import net.citizensnpcs.api.util.Messaging;
import net.citizensnpcs.util.Messages;
import org.bukkit.conversations.ConversationContext;
import org.bukkit.conversations.NumericPrompt;
import org.bukkit.conversations.Prompt;
public class DelayTriggerPrompt extends NumericPrompt implements WaypointTriggerPrompt {
@Override
protected Prompt acceptValidatedInput(ConversationContext context, Number input) {
int delay = Math.max(input.intValue(), 0);
context.setSessionData(WaypointTriggerPrompt.CREATED_TRIGGER_KEY, new DelayTrigger(delay));
return (Prompt) context.getSessionData(WaypointTriggerPrompt.RETURN_PROMPT_KEY);
}
@Override
public String getPromptText(ConversationContext context) {
return Messaging.tr(Messages.DELAY_TRIGGER_PROMPT);
}
}

Some files were not shown because too many files have changed in this diff Show More