First pass at 1.11
This commit is contained in:
parent
0c833e2566
commit
0189157400
|
@ -4,7 +4,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>net.citizensnpcs</groupId>
|
<groupId>net.citizensnpcs</groupId>
|
||||||
<artifactId>citizens-parent</artifactId>
|
<artifactId>citizens-parent</artifactId>
|
||||||
<version>2.0.20-SNAPSHOT</version>
|
<version>2.0.21-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>citizens</artifactId>
|
<artifactId>citizens</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
@ -40,13 +40,20 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>${project.groupId}</groupId>
|
<groupId>${project.groupId}</groupId>
|
||||||
<artifactId>citizens-v1_10_R1</artifactId>
|
<artifactId>citizens-v1_10_R1</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
<type>jar</type>
|
<type>jar</type>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>citizens-v1_11_R1</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<type>jar</type>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.citizensnpcs.npc.ai;
|
||||||
|
|
||||||
|
import net.citizensnpcs.api.npc.NPC;
|
||||||
|
|
||||||
|
public interface NPCHolder {
|
||||||
|
public NPC getNPC();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package net.citizensnpcs.trait;
|
||||||
|
|
||||||
|
public interface Toggleable {
|
||||||
|
public boolean toggle();
|
||||||
|
}
|
|
@ -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 + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Loading…
Reference in New Issue