mirror of
https://github.com/CitizensDev/Citizens2.git
synced 2024-11-14 22:56:24 +01:00
First pass at 1.11
This commit is contained in:
parent
0c833e2566
commit
0189157400
9
dist/pom.xml
vendored
9
dist/pom.xml
vendored
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>net.citizensnpcs</groupId>
|
||||
<artifactId>citizens-parent</artifactId>
|
||||
<version>2.0.20-SNAPSHOT</version>
|
||||
<version>2.0.21-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>citizens</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@ -48,5 +48,12 @@
|
||||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>citizens-v1_11_R1</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>jar</type>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
470
main/java/net/citizensnpcs/Citizens.java
Normal file
470
main/java/net/citizensnpcs/Citizens.java
Normal file
@ -0,0 +1,470 @@
|
||||
package net.citizensnpcs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.CitizensPlugin;
|
||||
import net.citizensnpcs.api.ai.speech.SpeechFactory;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.CommandManager;
|
||||
import net.citizensnpcs.api.command.CommandManager.CommandInfo;
|
||||
import net.citizensnpcs.api.command.Injector;
|
||||
import net.citizensnpcs.api.event.CitizensDisableEvent;
|
||||
import net.citizensnpcs.api.event.CitizensEnableEvent;
|
||||
import net.citizensnpcs.api.event.CitizensPreReloadEvent;
|
||||
import net.citizensnpcs.api.event.CitizensReloadEvent;
|
||||
import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCDataStore;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.api.npc.SimpleNPCDataStore;
|
||||
import net.citizensnpcs.api.scripting.EventRegistrar;
|
||||
import net.citizensnpcs.api.scripting.ObjectProvider;
|
||||
import net.citizensnpcs.api.scripting.ScriptCompiler;
|
||||
import net.citizensnpcs.api.trait.TraitFactory;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.api.util.NBTStorage;
|
||||
import net.citizensnpcs.api.util.Storage;
|
||||
import net.citizensnpcs.api.util.Translator;
|
||||
import net.citizensnpcs.api.util.YamlStorage;
|
||||
import net.citizensnpcs.commands.AdminCommands;
|
||||
import net.citizensnpcs.commands.EditorCommands;
|
||||
import net.citizensnpcs.commands.NPCCommands;
|
||||
import net.citizensnpcs.commands.TemplateCommands;
|
||||
import net.citizensnpcs.commands.TraitCommands;
|
||||
import net.citizensnpcs.commands.WaypointCommands;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.npc.CitizensNPCRegistry;
|
||||
import net.citizensnpcs.npc.CitizensTraitFactory;
|
||||
import net.citizensnpcs.npc.NPCSelector;
|
||||
import net.citizensnpcs.npc.ai.speech.Chat;
|
||||
import net.citizensnpcs.npc.ai.speech.CitizensSpeechFactory;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.npc.skin.Skin;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.PlayerUpdateTask;
|
||||
import net.citizensnpcs.util.StringHelper;
|
||||
import net.citizensnpcs.util.Util;
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
|
||||
public class Citizens extends JavaPlugin implements CitizensPlugin {
|
||||
private final CommandManager commands = new CommandManager();
|
||||
private boolean compatible;
|
||||
private Settings config;
|
||||
private CitizensNPCRegistry npcRegistry;
|
||||
private NPCDataStore saves;
|
||||
private NPCSelector selector;
|
||||
private CitizensSpeechFactory speechFactory;
|
||||
private final Map<String, NPCRegistry> storedRegistries = Maps.newHashMap();
|
||||
private CitizensTraitFactory traitFactory;
|
||||
|
||||
@Override
|
||||
public NPCRegistry createAnonymousNPCRegistry(NPCDataStore store) {
|
||||
return new CitizensNPCRegistry(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPCRegistry createNamedNPCRegistry(String name, NPCDataStore store) {
|
||||
NPCRegistry created = new CitizensNPCRegistry(store);
|
||||
storedRegistries.put(name, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private NPCDataStore createStorage(File folder) {
|
||||
Storage saves = null;
|
||||
String type = Setting.STORAGE_TYPE.asString();
|
||||
if (type.equalsIgnoreCase("nbt")) {
|
||||
saves = new NBTStorage(new File(folder + File.separator + Setting.STORAGE_FILE.asString()),
|
||||
"Citizens NPC Storage");
|
||||
}
|
||||
if (saves == null) {
|
||||
saves = new YamlStorage(new File(folder, Setting.STORAGE_FILE.asString()), "Citizens NPC Storage");
|
||||
}
|
||||
if (!saves.load())
|
||||
return null;
|
||||
return SimpleNPCDataStore.create(saves);
|
||||
}
|
||||
|
||||
private void despawnNPCs() {
|
||||
Iterator<NPC> itr = npcRegistry.iterator();
|
||||
while (itr.hasNext()) {
|
||||
NPC npc = itr.next();
|
||||
try {
|
||||
npc.despawn(DespawnReason.RELOAD);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
// ensure that all entities are despawned
|
||||
}
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableSubPlugins() {
|
||||
File root = new File(getDataFolder(), Setting.SUBPLUGIN_FOLDER.asString());
|
||||
if (!root.exists() || !root.isDirectory())
|
||||
return;
|
||||
File[] files = root.listFiles();
|
||||
for (File file : files) {
|
||||
Plugin plugin;
|
||||
try {
|
||||
plugin = Bukkit.getPluginManager().loadPlugin(file);
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
if (plugin == null)
|
||||
continue;
|
||||
// code beneath modified from CraftServer
|
||||
try {
|
||||
Messaging.logTr(Messages.LOADING_SUB_PLUGIN, plugin.getDescription().getFullName());
|
||||
plugin.onLoad();
|
||||
} catch (Throwable ex) {
|
||||
Messaging.severeTr(Messages.ERROR_INITALISING_SUB_PLUGIN, ex.getMessage(),
|
||||
plugin.getDescription().getFullName());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
NMS.loadPlugins();
|
||||
}
|
||||
|
||||
public CommandInfo getCommandInfo(String rootCommand, String modifier) {
|
||||
return commands.getCommand(rootCommand, modifier);
|
||||
}
|
||||
|
||||
public Iterable<CommandInfo> getCommands(String base) {
|
||||
return commands.getCommands(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.citizensnpcs.api.npc.NPCSelector getDefaultNPCSelector() {
|
||||
return selector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPCRegistry getNamedNPCRegistry(String name) {
|
||||
return storedRegistries.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<NPCRegistry> getNPCRegistries() {
|
||||
return new Iterable<NPCRegistry>() {
|
||||
@Override
|
||||
public Iterator<NPCRegistry> iterator() {
|
||||
return new Iterator<NPCRegistry>() {
|
||||
Iterator<NPCRegistry> stored;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return stored == null ? true : stored.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPCRegistry next() {
|
||||
if (stored == null) {
|
||||
stored = storedRegistries.values().iterator();
|
||||
return npcRegistry;
|
||||
}
|
||||
return stored.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPCRegistry getNPCRegistry() {
|
||||
return npcRegistry;
|
||||
}
|
||||
|
||||
public NPCSelector getNPCSelector() {
|
||||
return selector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClassLoader getOwningClassLoader() {
|
||||
return getClassLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getScriptFolder() {
|
||||
return new File(getDataFolder(), "scripts");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpeechFactory getSpeechFactory() {
|
||||
return speechFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TraitFactory getTraitFactory() {
|
||||
return traitFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String cmdName, String[] args) {
|
||||
String modifier = args.length > 0 ? args[0] : "";
|
||||
if (!commands.hasCommand(command, modifier) && !modifier.isEmpty()) {
|
||||
return suggestClosestModifier(sender, command.getName(), modifier);
|
||||
}
|
||||
|
||||
NPC npc = selector == null ? null : selector.getSelected(sender);
|
||||
// TODO: change the args supplied to a context style system for
|
||||
// flexibility (ie. adding more context in the future without
|
||||
// changing everything)
|
||||
|
||||
Object[] methodArgs = { sender, npc };
|
||||
return commands.executeSafe(command, args, sender, methodArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
Bukkit.getPluginManager().callEvent(new CitizensDisableEvent());
|
||||
Editor.leaveAll();
|
||||
|
||||
if (compatible) {
|
||||
saves.storeAll(npcRegistry);
|
||||
saves.saveToDiskImmediate();
|
||||
despawnNPCs();
|
||||
npcRegistry = null;
|
||||
}
|
||||
|
||||
CitizensAPI.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
setupTranslator();
|
||||
CitizensAPI.setImplementation(this);
|
||||
config = new Settings(getDataFolder());
|
||||
// Disable if the server is not using the compatible Minecraft version
|
||||
String mcVersion = Util.getMinecraftRevision();
|
||||
compatible = true;
|
||||
try {
|
||||
NMS.loadBridge(mcVersion);
|
||||
} catch (Exception e) {
|
||||
compatible = false;
|
||||
if (Messaging.isDebugging()) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Messaging.severeTr(Messages.CITIZENS_INCOMPATIBLE, getDescription().getVersion(), mcVersion);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
registerScriptHelpers();
|
||||
|
||||
saves = createStorage(getDataFolder());
|
||||
if (saves == null) {
|
||||
Messaging.severeTr(Messages.FAILED_LOAD_SAVES);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
npcRegistry = new CitizensNPCRegistry(saves);
|
||||
traitFactory = new CitizensTraitFactory();
|
||||
selector = new NPCSelector(this);
|
||||
speechFactory = new CitizensSpeechFactory();
|
||||
speechFactory.register(Chat.class, "chat");
|
||||
|
||||
getServer().getPluginManager().registerEvents(new EventListen(storedRegistries), this);
|
||||
|
||||
if (Setting.NPC_COST.asDouble() > 0) {
|
||||
setupEconomy();
|
||||
}
|
||||
|
||||
registerCommands();
|
||||
enableSubPlugins();
|
||||
|
||||
// Setup NPCs after all plugins have been enabled (allows for multiworld
|
||||
// support and for NPCs to properly register external settings)
|
||||
if (getServer().getScheduler().scheduleSyncDelayedTask(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
saves.loadInto(npcRegistry);
|
||||
Messaging.logTr(Messages.NUM_LOADED_NOTIFICATION, Iterables.size(npcRegistry), "?");
|
||||
startMetrics();
|
||||
scheduleSaveTask(Setting.SAVE_TASK_DELAY.asInt());
|
||||
Bukkit.getPluginManager().callEvent(new CitizensEnableEvent());
|
||||
new PlayerUpdateTask().runTaskTimer(Citizens.this, 0, 1);
|
||||
}
|
||||
}, 1) == -1) {
|
||||
Messaging.severeTr(Messages.LOAD_TASK_NOT_SCHEDULED);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImplementationChanged() {
|
||||
Messaging.severeTr(Messages.CITIZENS_IMPLEMENTATION_DISABLED);
|
||||
Bukkit.getPluginManager().disablePlugin(this);
|
||||
}
|
||||
|
||||
public void registerCommandClass(Class<?> clazz) {
|
||||
try {
|
||||
commands.register(clazz);
|
||||
} catch (Throwable ex) {
|
||||
Messaging.logTr(Messages.CITIZENS_INVALID_COMMAND_CLASS);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
commands.setInjector(new Injector(this));
|
||||
// Register command classes
|
||||
commands.register(AdminCommands.class);
|
||||
commands.register(EditorCommands.class);
|
||||
commands.register(NPCCommands.class);
|
||||
commands.register(TemplateCommands.class);
|
||||
commands.register(TraitCommands.class);
|
||||
commands.register(WaypointCommands.class);
|
||||
}
|
||||
|
||||
private void registerScriptHelpers() {
|
||||
ScriptCompiler compiler = CitizensAPI.getScriptCompiler();
|
||||
compiler.registerGlobalContextProvider(new EventRegistrar(this));
|
||||
compiler.registerGlobalContextProvider(new ObjectProvider("plugin", this));
|
||||
}
|
||||
|
||||
public void reload() throws NPCLoadException {
|
||||
Editor.leaveAll();
|
||||
config.reload();
|
||||
despawnNPCs();
|
||||
ProfileFetcher.reset();
|
||||
Skin.clearCache();
|
||||
getServer().getPluginManager().callEvent(new CitizensPreReloadEvent());
|
||||
|
||||
saves = createStorage(getDataFolder());
|
||||
saves.loadInto(npcRegistry);
|
||||
|
||||
getServer().getPluginManager().callEvent(new CitizensReloadEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNamedNPCRegistry(String name) {
|
||||
storedRegistries.remove(name);
|
||||
}
|
||||
|
||||
private void scheduleSaveTask(int delay) {
|
||||
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
storeNPCs();
|
||||
saves.saveToDisk();
|
||||
}
|
||||
}, delay, delay);
|
||||
}
|
||||
|
||||
private void setupEconomy() {
|
||||
try {
|
||||
RegisteredServiceProvider<Economy> provider = Bukkit.getServicesManager().getRegistration(Economy.class);
|
||||
if (provider != null && provider.getProvider() != null) {
|
||||
Economy economy = provider.getProvider();
|
||||
Bukkit.getPluginManager().registerEvents(new PaymentListener(economy), this);
|
||||
}
|
||||
} catch (NoClassDefFoundError e) {
|
||||
Messaging.logTr(Messages.ERROR_LOADING_ECONOMY);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupTranslator() {
|
||||
Locale locale = Locale.getDefault();
|
||||
String setting = Setting.LOCALE.asString();
|
||||
if (!setting.isEmpty()) {
|
||||
String[] parts = setting.split("[\\._]");
|
||||
switch (parts.length) {
|
||||
case 1:
|
||||
locale = new Locale(parts[0]);
|
||||
break;
|
||||
case 2:
|
||||
locale = new Locale(parts[0], parts[1]);
|
||||
break;
|
||||
case 3:
|
||||
locale = new Locale(parts[0], parts[1], parts[2]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
Translator.setInstance(new File(getDataFolder(), "lang"), locale);
|
||||
}
|
||||
|
||||
private void startMetrics() {
|
||||
try {
|
||||
Metrics metrics = new Metrics(Citizens.this);
|
||||
if (metrics.isOptOut())
|
||||
return;
|
||||
metrics.addCustomData(new Metrics.Plotter("Total NPCs") {
|
||||
@Override
|
||||
public int getValue() {
|
||||
if (npcRegistry == null)
|
||||
return 0;
|
||||
return Iterables.size(npcRegistry);
|
||||
}
|
||||
});
|
||||
metrics.addCustomData(new Metrics.Plotter("Total goals") {
|
||||
@Override
|
||||
public int getValue() {
|
||||
if (npcRegistry == null)
|
||||
return 0;
|
||||
int goalCount = 0;
|
||||
for (NPC npc : npcRegistry) {
|
||||
goalCount += Iterables.size(npc.getDefaultGoalController());
|
||||
}
|
||||
return goalCount;
|
||||
}
|
||||
});
|
||||
traitFactory.addPlotters(metrics.createGraph("traits"));
|
||||
metrics.start();
|
||||
} catch (IOException e) {
|
||||
Messaging.logTr(Messages.METRICS_ERROR_NOTIFICATION, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void storeNPCs() {
|
||||
if (saves == null)
|
||||
return;
|
||||
for (NPC npc : npcRegistry) {
|
||||
saves.store(npc);
|
||||
}
|
||||
}
|
||||
|
||||
public void storeNPCs(CommandContext args) {
|
||||
storeNPCs();
|
||||
boolean async = args.hasFlag('a');
|
||||
if (async) {
|
||||
saves.saveToDisk();
|
||||
} else {
|
||||
saves.saveToDiskImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean suggestClosestModifier(CommandSender sender, String command, String modifier) {
|
||||
String closest = commands.getClosestCommandModifier(command, modifier);
|
||||
if (!closest.isEmpty()) {
|
||||
sender.sendMessage(ChatColor.GRAY + Messaging.tr(Messages.UNKNOWN_COMMAND));
|
||||
sender.sendMessage(StringHelper.wrap(" /") + command + " " + StringHelper.wrap(closest));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
614
main/java/net/citizensnpcs/EventListen.java
Normal file
614
main/java/net/citizensnpcs/EventListen.java
Normal file
@ -0,0 +1,614 @@
|
||||
package net.citizensnpcs;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.FishHook;
|
||||
import org.bukkit.entity.Minecart;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.event.entity.EntityCombustByBlockEvent;
|
||||
import org.bukkit.event.entity.EntityCombustByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityCombustEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByBlockEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.EntityTargetEvent;
|
||||
import org.bukkit.event.entity.ProjectileHitEvent;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerFishEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
|
||||
import org.bukkit.event.vehicle.VehicleDestroyEvent;
|
||||
import org.bukkit.event.vehicle.VehicleEnterEvent;
|
||||
import org.bukkit.event.world.ChunkLoadEvent;
|
||||
import org.bukkit.event.world.ChunkUnloadEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.ai.event.NavigationBeginEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigationCompleteEvent;
|
||||
import net.citizensnpcs.api.event.CitizensDeserialiseMetaEvent;
|
||||
import net.citizensnpcs.api.event.CitizensPreReloadEvent;
|
||||
import net.citizensnpcs.api.event.CitizensSerialiseMetaEvent;
|
||||
import net.citizensnpcs.api.event.CommandSenderCreateNPCEvent;
|
||||
import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.event.EntityTargetNPCEvent;
|
||||
import net.citizensnpcs.api.event.NPCCombustByBlockEvent;
|
||||
import net.citizensnpcs.api.event.NPCCombustByEntityEvent;
|
||||
import net.citizensnpcs.api.event.NPCCombustEvent;
|
||||
import net.citizensnpcs.api.event.NPCDamageByBlockEvent;
|
||||
import net.citizensnpcs.api.event.NPCDamageByEntityEvent;
|
||||
import net.citizensnpcs.api.event.NPCDamageEntityEvent;
|
||||
import net.citizensnpcs.api.event.NPCDamageEvent;
|
||||
import net.citizensnpcs.api.event.NPCDeathEvent;
|
||||
import net.citizensnpcs.api.event.NPCDespawnEvent;
|
||||
import net.citizensnpcs.api.event.NPCLeftClickEvent;
|
||||
import net.citizensnpcs.api.event.NPCRightClickEvent;
|
||||
import net.citizensnpcs.api.event.NPCSpawnEvent;
|
||||
import net.citizensnpcs.api.event.PlayerCreateNPCEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.npc.skin.SkinUpdateTracker;
|
||||
import net.citizensnpcs.trait.Controllable;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
public class EventListen implements Listener {
|
||||
private final NPCRegistry npcRegistry = CitizensAPI.getNPCRegistry();
|
||||
private final Map<String, NPCRegistry> registries;
|
||||
private final SkinUpdateTracker skinUpdateTracker;
|
||||
private final ListMultimap<ChunkCoord, NPC> toRespawn = ArrayListMultimap.create();
|
||||
|
||||
EventListen(Map<String, NPCRegistry> registries) {
|
||||
this.registries = registries;
|
||||
this.skinUpdateTracker = new SkinUpdateTracker(npcRegistry, registries);
|
||||
}
|
||||
|
||||
private void checkCreationEvent(CommandSenderCreateNPCEvent event) {
|
||||
if (event.getCreator().hasPermission("citizens.admin.avoid-limits"))
|
||||
return;
|
||||
int limit = Setting.DEFAULT_NPC_LIMIT.asInt();
|
||||
int maxChecks = Setting.MAX_NPC_LIMIT_CHECKS.asInt();
|
||||
for (int i = maxChecks; i >= 0; i--) {
|
||||
if (!event.getCreator().hasPermission("citizens.npc.limit." + i))
|
||||
continue;
|
||||
limit = i;
|
||||
break;
|
||||
}
|
||||
if (limit < 0)
|
||||
return;
|
||||
int owned = 0;
|
||||
for (NPC npc : npcRegistry) {
|
||||
if (!event.getNPC().equals(npc) && npc.hasTrait(Owner.class)
|
||||
&& npc.getTrait(Owner.class).isOwnedBy(event.getCreator()))
|
||||
owned++;
|
||||
}
|
||||
int wouldOwn = owned + 1;
|
||||
if (wouldOwn > limit) {
|
||||
event.setCancelled(true);
|
||||
event.setCancelReason(Messaging.tr(Messages.OVER_NPC_LIMIT, limit));
|
||||
}
|
||||
}
|
||||
|
||||
private Iterable<NPC> getAllNPCs() {
|
||||
return Iterables.filter(Iterables.<NPC> concat(npcRegistry, Iterables.concat(registries.values())),
|
||||
Predicates.notNull());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onChunkLoad(ChunkLoadEvent event) {
|
||||
respawnAllFromCoord(toCoord(event.getChunk()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onChunkUnload(ChunkUnloadEvent event) {
|
||||
ChunkCoord coord = toCoord(event.getChunk());
|
||||
Location loc = new Location(null, 0, 0, 0);
|
||||
for (NPC npc : getAllNPCs()) {
|
||||
if (npc == null || !npc.isSpawned())
|
||||
continue;
|
||||
loc = npc.getEntity().getLocation(loc);
|
||||
boolean sameChunkCoordinates = coord.z == loc.getBlockZ() >> 4 && coord.x == loc.getBlockX() >> 4;
|
||||
if (!sameChunkCoordinates || !event.getWorld().equals(loc.getWorld()))
|
||||
continue;
|
||||
if (!npc.despawn(DespawnReason.CHUNK_UNLOAD)) {
|
||||
event.setCancelled(true);
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Cancelled chunk unload at [" + coord.x + "," + coord.z + "]");
|
||||
}
|
||||
respawnAllFromCoord(coord);
|
||||
return;
|
||||
}
|
||||
toRespawn.put(coord, npc);
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Despawned id", npc.getId(),
|
||||
"due to chunk unload at [" + coord.x + "," + coord.z + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onCitizensReload(CitizensPreReloadEvent event) {
|
||||
skinUpdateTracker.reset();
|
||||
toRespawn.clear();
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onCommandSenderCreateNPC(CommandSenderCreateNPCEvent event) {
|
||||
checkCreationEvent(event);
|
||||
}
|
||||
|
||||
/*
|
||||
* Entity events
|
||||
*/
|
||||
@EventHandler
|
||||
public void onEntityCombust(EntityCombustEvent event) {
|
||||
NPC npc = npcRegistry.getNPC(event.getEntity());
|
||||
if (npc == null)
|
||||
return;
|
||||
event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true));
|
||||
if (event instanceof EntityCombustByEntityEvent) {
|
||||
Bukkit.getPluginManager().callEvent(new NPCCombustByEntityEvent((EntityCombustByEntityEvent) event, npc));
|
||||
} else if (event instanceof EntityCombustByBlockEvent) {
|
||||
Bukkit.getPluginManager().callEvent(new NPCCombustByBlockEvent((EntityCombustByBlockEvent) event, npc));
|
||||
} else {
|
||||
Bukkit.getPluginManager().callEvent(new NPCCombustEvent(event, npc));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntityDamage(EntityDamageEvent event) {
|
||||
NPC npc = npcRegistry.getNPC(event.getEntity());
|
||||
|
||||
if (npc == null) {
|
||||
if (event instanceof EntityDamageByEntityEvent) {
|
||||
npc = npcRegistry.getNPC(((EntityDamageByEntityEvent) event).getDamager());
|
||||
if (npc == null)
|
||||
return;
|
||||
event.setCancelled(!npc.data().get(NPC.DAMAGE_OTHERS_METADATA, true));
|
||||
NPCDamageEntityEvent damageEvent = new NPCDamageEntityEvent(npc, (EntityDamageByEntityEvent) event);
|
||||
Bukkit.getPluginManager().callEvent(damageEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true));
|
||||
if (event instanceof EntityDamageByEntityEvent) {
|
||||
NPCDamageByEntityEvent damageEvent = new NPCDamageByEntityEvent(npc, (EntityDamageByEntityEvent) event);
|
||||
Bukkit.getPluginManager().callEvent(damageEvent);
|
||||
|
||||
if (!damageEvent.isCancelled() || !(damageEvent.getDamager() instanceof Player))
|
||||
return;
|
||||
Player damager = (Player) damageEvent.getDamager();
|
||||
|
||||
NPCLeftClickEvent leftClickEvent = new NPCLeftClickEvent(npc, damager);
|
||||
Bukkit.getPluginManager().callEvent(leftClickEvent);
|
||||
} else if (event instanceof EntityDamageByBlockEvent) {
|
||||
Bukkit.getPluginManager().callEvent(new NPCDamageByBlockEvent(npc, (EntityDamageByBlockEvent) event));
|
||||
} else {
|
||||
Bukkit.getPluginManager().callEvent(new NPCDamageEvent(npc, event));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onEntityDeath(EntityDeathEvent event) {
|
||||
final NPC npc = npcRegistry.getNPC(event.getEntity());
|
||||
if (npc == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!npc.data().get(NPC.DROPS_ITEMS_METADATA, false)) {
|
||||
event.getDrops().clear();
|
||||
}
|
||||
|
||||
final Location location = npc.getEntity().getLocation();
|
||||
Bukkit.getPluginManager().callEvent(new NPCDeathEvent(npc, event));
|
||||
npc.despawn(DespawnReason.DEATH);
|
||||
|
||||
if (npc.data().has(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA)) {
|
||||
String teamName = npc.data().get(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA);
|
||||
Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName);
|
||||
if (team != null) {
|
||||
team.unregister();
|
||||
}
|
||||
|
||||
npc.data().remove(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA);
|
||||
}
|
||||
|
||||
if (npc.data().get(NPC.RESPAWN_DELAY_METADATA, -1) >= 0) {
|
||||
int delay = npc.data().get(NPC.RESPAWN_DELAY_METADATA, -1);
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!npc.isSpawned() && npc.getOwningRegistry().getByUniqueId(npc.getUniqueId()) == npc) {
|
||||
npc.spawn(location);
|
||||
}
|
||||
}
|
||||
}, delay + 2);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onEntitySpawn(CreatureSpawnEvent event) {
|
||||
if (event.isCancelled() && npcRegistry.isNPC(event.getEntity())) {
|
||||
event.setCancelled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntityTarget(EntityTargetEvent event) {
|
||||
NPC npc = npcRegistry.getNPC(event.getTarget());
|
||||
if (npc == null)
|
||||
return;
|
||||
event.setCancelled(
|
||||
!npc.data().get(NPC.TARGETABLE_METADATA, !npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true)));
|
||||
Bukkit.getPluginManager().callEvent(new EntityTargetNPCEvent(event, npc));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMetaDeserialise(CitizensDeserialiseMetaEvent event) {
|
||||
if (event.getKey().keyExists("skull")) {
|
||||
String owner = event.getKey().getString("skull.owner", "");
|
||||
UUID uuid = event.getKey().keyExists("skull.uuid") ? UUID.fromString(event.getKey().getString("skull.uuid"))
|
||||
: null;
|
||||
if (owner.isEmpty() && uuid == null) {
|
||||
return;
|
||||
}
|
||||
GameProfile profile = new GameProfile(uuid, owner);
|
||||
for (DataKey sub : event.getKey().getRelative("skull.properties").getSubKeys()) {
|
||||
String propertyName = sub.name();
|
||||
for (DataKey property : sub.getIntegerSubKeys()) {
|
||||
profile.getProperties().put(propertyName,
|
||||
new Property(property.getString("name"), property.getString("value"),
|
||||
property.keyExists("signature") ? property.getString("signature") : null));
|
||||
}
|
||||
}
|
||||
SkullMeta meta = (SkullMeta) Bukkit.getItemFactory().getItemMeta(Material.SKULL_ITEM);
|
||||
NMS.setProfile(meta, profile);
|
||||
event.getItemStack().setItemMeta(meta);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onMetaSerialise(CitizensSerialiseMetaEvent event) {
|
||||
if (!(event.getMeta() instanceof SkullMeta))
|
||||
return;
|
||||
SkullMeta meta = (SkullMeta) event.getMeta();
|
||||
GameProfile profile = NMS.getProfile(meta);
|
||||
if (profile == null)
|
||||
return;
|
||||
if (profile.getName() != null) {
|
||||
event.getKey().setString("skull.owner", profile.getName());
|
||||
}
|
||||
if (profile.getId() != null) {
|
||||
event.getKey().setString("skull.uuid", profile.getId().toString());
|
||||
}
|
||||
if (profile.getProperties() != null) {
|
||||
for (Entry<String, Collection<Property>> entry : profile.getProperties().asMap().entrySet()) {
|
||||
DataKey relative = event.getKey().getRelative("skull.properties." + entry.getKey());
|
||||
int i = 0;
|
||||
for (Property value : entry.getValue()) {
|
||||
relative.getRelative(i).setString("name", value.getName());
|
||||
if (value.getSignature() != null) {
|
||||
relative.getRelative(i).setString("signature", value.getSignature());
|
||||
}
|
||||
relative.getRelative(i).setString("value", value.getValue());
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onNavigationBegin(NavigationBeginEvent event) {
|
||||
skinUpdateTracker.onNPCNavigationBegin(event.getNPC());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onNavigationComplete(NavigationCompleteEvent event) {
|
||||
skinUpdateTracker.onNPCNavigationComplete(event.getNPC());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onNeedsRespawn(NPCNeedsRespawnEvent event) {
|
||||
ChunkCoord coord = toCoord(event.getSpawnLocation());
|
||||
if (toRespawn.containsEntry(coord, event.getNPC()))
|
||||
return;
|
||||
Messaging.debug("Stored", event.getNPC().getId(), "for respawn from NPCNeedsRespawnEvent");
|
||||
toRespawn.put(coord, event.getNPC());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onNPCDespawn(NPCDespawnEvent event) {
|
||||
if (event.getReason() == DespawnReason.PLUGIN || event.getReason() == DespawnReason.REMOVAL
|
||||
|| event.getReason() == DespawnReason.RELOAD) {
|
||||
Messaging.debug("Preventing further respawns of " + event.getNPC().getId() + " due to DespawnReason."
|
||||
+ event.getReason().name());
|
||||
if (event.getNPC().getStoredLocation() != null) {
|
||||
toRespawn.remove(toCoord(event.getNPC().getStoredLocation()), event.getNPC());
|
||||
}
|
||||
} else {
|
||||
Messaging.debug("Removing " + event.getNPC().getId() + " from skin tracker due to DespawnReason."
|
||||
+ event.getReason().name());
|
||||
}
|
||||
skinUpdateTracker.onNPCDespawn(event.getNPC());
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onNPCSpawn(NPCSpawnEvent event) {
|
||||
skinUpdateTracker.onNPCSpawn(event.getNPC());
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerChangedWorld(PlayerChangedWorldEvent event) {
|
||||
if (npcRegistry.getNPC(event.getPlayer()) == null)
|
||||
return;
|
||||
NMS.removeFromServerPlayerList(event.getPlayer());
|
||||
// on teleport, player NPCs are added to the server player list. this is
|
||||
// undesirable as player NPCs are not real players and confuse plugins.
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerChangeWorld(PlayerChangedWorldEvent event) {
|
||||
skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerCreateNPC(PlayerCreateNPCEvent event) {
|
||||
checkCreationEvent(event);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerFish(PlayerFishEvent event) {
|
||||
if (npcRegistry.isNPC(event.getCaught()) && npcRegistry.getNPC(event.getCaught()).isProtected()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
|
||||
NPC npc = npcRegistry.getNPC(event.getRightClicked());
|
||||
if (npc == null || event.getHand() == EquipmentSlot.OFF_HAND) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
NPCRightClickEvent rightClickEvent = new NPCRightClickEvent(npc, player);
|
||||
Bukkit.getPluginManager().callEvent(rightClickEvent);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
skinUpdateTracker.updatePlayer(event.getPlayer(), 20, true);
|
||||
}
|
||||
|
||||
// recalculate player NPCs the first time a player moves and every time
|
||||
// a player moves a certain distance from their last position.
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerMove(final PlayerMoveEvent event) {
|
||||
skinUpdateTracker.onPlayerMove(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
Editor.leave(event.getPlayer());
|
||||
if (event.getPlayer().isInsideVehicle()) {
|
||||
NPC npc = npcRegistry.getNPC(event.getPlayer().getVehicle());
|
||||
if (npc != null) {
|
||||
event.getPlayer().leaveVehicle();
|
||||
}
|
||||
}
|
||||
skinUpdateTracker.removePlayer(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||
skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerTeleport(final PlayerTeleportEvent event) {
|
||||
if (event.getCause() == TeleportCause.PLUGIN && !event.getPlayer().hasMetadata("citizens-force-teleporting")
|
||||
&& npcRegistry.getNPC(event.getPlayer()) != null && Setting.TELEPORT_DELAY.asInt() > 0) {
|
||||
event.setCancelled(true);
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
event.getPlayer().setMetadata("citizens-force-teleporting",
|
||||
new FixedMetadataValue(CitizensAPI.getPlugin(), true));
|
||||
event.getPlayer().teleport(event.getTo());
|
||||
event.getPlayer().removeMetadata("citizens-force-teleporting", CitizensAPI.getPlugin());
|
||||
}
|
||||
}, Setting.TELEPORT_DELAY.asInt());
|
||||
}
|
||||
skinUpdateTracker.updatePlayer(event.getPlayer(), 15, true);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onProjectileHit(final ProjectileHitEvent event) {
|
||||
if (!(event.getEntity() instanceof FishHook))
|
||||
return;
|
||||
NMS.removeHookIfNecessary(npcRegistry, (FishHook) event.getEntity());
|
||||
new BukkitRunnable() {
|
||||
int n = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (n++ > 5) {
|
||||
cancel();
|
||||
}
|
||||
NMS.removeHookIfNecessary(npcRegistry, (FishHook) event.getEntity());
|
||||
}
|
||||
}.runTaskTimer(CitizensAPI.getPlugin(), 0, 1);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onVehicleDestroy(VehicleDestroyEvent event) {
|
||||
NPC npc = npcRegistry.getNPC(event.getVehicle());
|
||||
if (npc == null) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(npc.data().get(NPC.DEFAULT_PROTECTED_METADATA, true));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onVehicleEnter(VehicleEnterEvent event) {
|
||||
if (!npcRegistry.isNPC(event.getEntered()))
|
||||
return;
|
||||
NPC npc = npcRegistry.getNPC(event.getEntered());
|
||||
if ((npc.getEntity().getType() == EntityType.HORSE || npc.getEntity().getType() == EntityType.BOAT
|
||||
|| npc.getEntity() instanceof Minecart) && !npc.getTrait(Controllable.class).isEnabled()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldLoad(WorldLoadEvent event) {
|
||||
for (ChunkCoord chunk : toRespawn.keySet()) {
|
||||
if (!chunk.worldName.equals(event.getWorld().getName())
|
||||
|| !event.getWorld().isChunkLoaded(chunk.x, chunk.z))
|
||||
continue;
|
||||
respawnAllFromCoord(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onWorldUnload(WorldUnloadEvent event) {
|
||||
for (NPC npc : getAllNPCs()) {
|
||||
if (npc == null || !npc.isSpawned() || !npc.getEntity().getWorld().equals(event.getWorld()))
|
||||
continue;
|
||||
boolean despawned = npc.despawn(DespawnReason.WORLD_UNLOAD);
|
||||
if (event.isCancelled() || !despawned) {
|
||||
for (ChunkCoord coord : toRespawn.keySet()) {
|
||||
if (event.getWorld().getName().equals(coord.worldName)) {
|
||||
respawnAllFromCoord(coord);
|
||||
}
|
||||
}
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
if (npc.isSpawned()) {
|
||||
storeForRespawn(npc);
|
||||
Messaging.debug("Despawned", npc.getId() + "due to world unload at", event.getWorld().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void respawnAllFromCoord(ChunkCoord coord) {
|
||||
List<NPC> ids = toRespawn.get(coord);
|
||||
for (int i = 0; i < ids.size(); i++) {
|
||||
NPC npc = ids.get(i);
|
||||
boolean success = spawn(npc);
|
||||
if (!success) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Couldn't respawn id", npc.getId(),
|
||||
"during chunk event at [" + coord.x + "," + coord.z + "]");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
ids.remove(i--);
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Spawned id", npc.getId(), "due to chunk event at [" + coord.x + "," + coord.z + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean spawn(NPC npc) {
|
||||
Location spawn = npc.getTrait(CurrentLocation.class).getLocation();
|
||||
if (spawn == null) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Couldn't find a spawn location for despawned NPC id", npc.getId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return npc.spawn(spawn);
|
||||
}
|
||||
|
||||
private void storeForRespawn(NPC npc) {
|
||||
toRespawn.put(toCoord(npc.getEntity().getLocation()), npc);
|
||||
}
|
||||
|
||||
private ChunkCoord toCoord(Chunk chunk) {
|
||||
return new ChunkCoord(chunk);
|
||||
}
|
||||
|
||||
private ChunkCoord toCoord(Location loc) {
|
||||
return new ChunkCoord(loc.getWorld().getName(), loc.getBlockX() >> 4, loc.getBlockZ() >> 4);
|
||||
}
|
||||
|
||||
private static class ChunkCoord {
|
||||
private final String worldName;
|
||||
private final int x;
|
||||
private final int z;
|
||||
|
||||
private ChunkCoord(Chunk chunk) {
|
||||
this(chunk.getWorld().getName(), chunk.getX(), chunk.getZ());
|
||||
}
|
||||
|
||||
private ChunkCoord(String worldName, int x, int z) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.worldName = worldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ChunkCoord other = (ChunkCoord) obj;
|
||||
if (worldName == null) {
|
||||
if (other.worldName != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!worldName.equals(other.worldName)) {
|
||||
return false;
|
||||
}
|
||||
return x == other.x && z == other.z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
return prime * (prime * (prime + ((worldName == null) ? 0 : worldName.hashCode())) + x) + z;
|
||||
}
|
||||
}
|
||||
}
|
787
main/java/net/citizensnpcs/Metrics.java
Normal file
787
main/java/net/citizensnpcs/Metrics.java
Normal file
@ -0,0 +1,787 @@
|
||||
/*
|
||||
* Copyright 2011-2013 Tyler Blair. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are
|
||||
* permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
* conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
* of conditions and the following disclaimer in the documentation and/or other materials
|
||||
* provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those of the
|
||||
* authors and contributors and should not be interpreted as representing official policies,
|
||||
* either expressed or implied, of anybody else.
|
||||
*/
|
||||
package net.citizensnpcs;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
public class Metrics {
|
||||
/**
|
||||
* The plugin configuration file
|
||||
*/
|
||||
private final YamlConfiguration configuration;
|
||||
|
||||
/**
|
||||
* The plugin configuration file
|
||||
*/
|
||||
private final File configurationFile;
|
||||
|
||||
/**
|
||||
* Debug mode
|
||||
*/
|
||||
private final boolean debug;
|
||||
|
||||
/**
|
||||
* All of the custom graphs to submit to metrics
|
||||
*/
|
||||
private final Set<Graph> graphs = Collections.synchronizedSet(new HashSet<Graph>());
|
||||
|
||||
/**
|
||||
* Unique server id
|
||||
*/
|
||||
private final String guid;
|
||||
|
||||
/**
|
||||
* Lock for synchronization
|
||||
*/
|
||||
private final Object optOutLock = new Object();
|
||||
|
||||
/**
|
||||
* The plugin this metrics submits for
|
||||
*/
|
||||
private final Plugin plugin;
|
||||
|
||||
/**
|
||||
* The scheduled task
|
||||
*/
|
||||
private volatile BukkitTask task = null;
|
||||
|
||||
public Metrics(final Plugin plugin) throws IOException {
|
||||
if (plugin == null) {
|
||||
throw new IllegalArgumentException("Plugin cannot be null");
|
||||
}
|
||||
|
||||
this.plugin = plugin;
|
||||
|
||||
// load the config
|
||||
configurationFile = getConfigFile();
|
||||
configuration = YamlConfiguration.loadConfiguration(configurationFile);
|
||||
|
||||
// add some defaults
|
||||
configuration.addDefault("opt-out", false);
|
||||
configuration.addDefault("guid", UUID.randomUUID().toString());
|
||||
configuration.addDefault("debug", false);
|
||||
|
||||
// Do we need to create the file?
|
||||
if (configuration.get("guid", null) == null) {
|
||||
configuration.options().header("http://mcstats.org").copyDefaults(true);
|
||||
configuration.save(configurationFile);
|
||||
}
|
||||
|
||||
// Load the guid then
|
||||
guid = configuration.getString("guid");
|
||||
debug = configuration.getBoolean("debug", false);
|
||||
}
|
||||
|
||||
public void addCustomData(Plotter plotter) {
|
||||
Graph graph = new Graph(plotter.name);
|
||||
graph.addPlotter(plotter);
|
||||
addGraph(graph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend
|
||||
*
|
||||
* @param graph
|
||||
* The name of the graph
|
||||
*/
|
||||
public void addGraph(final Graph graph) {
|
||||
if (graph == null) {
|
||||
throw new IllegalArgumentException("Graph cannot be null");
|
||||
}
|
||||
|
||||
graphs.add(graph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct and create a Graph that can be used to separate specific plotters to their own graphs on the metrics
|
||||
* website. Plotters can be added to the graph object returned.
|
||||
*
|
||||
* @param name
|
||||
* The name of the graph
|
||||
* @return Graph object created. Will never return NULL under normal circumstances unless bad parameters are given
|
||||
*/
|
||||
public Graph createGraph(final String name) {
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("Graph name cannot be null");
|
||||
}
|
||||
|
||||
// Construct the graph object
|
||||
final Graph graph = new Graph(name);
|
||||
|
||||
// Now we can add our graph
|
||||
graphs.add(graph);
|
||||
|
||||
// and return back
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public void disable() throws IOException {
|
||||
// This has to be synchronized or it can collide with the check in the
|
||||
// task.
|
||||
synchronized (optOutLock) {
|
||||
// Check if the server owner has already set opt-out, if not, set
|
||||
// it.
|
||||
if (!isOptOut()) {
|
||||
configuration.set("opt-out", true);
|
||||
configuration.save(configurationFile);
|
||||
}
|
||||
|
||||
// Disable Task, if it is running
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
public void enable() throws IOException {
|
||||
// This has to be synchronized or it can collide with the check in the
|
||||
// task.
|
||||
synchronized (optOutLock) {
|
||||
// Check if the server owner has already set opt-out, if not, set
|
||||
// it.
|
||||
if (isOptOut()) {
|
||||
configuration.set("opt-out", false);
|
||||
configuration.save(configurationFile);
|
||||
}
|
||||
|
||||
// Enable Task, if it is not running
|
||||
if (task == null) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the File object of the config file that should be used to store data such as the GUID and opt-out status
|
||||
*
|
||||
* @return the File object for the config file
|
||||
*/
|
||||
public File getConfigFile() {
|
||||
// I believe the easiest way to get the base folder (e.g craftbukkit set
|
||||
// via -P) for plugins to use
|
||||
// is to abuse the plugin object we already have
|
||||
// plugin.getDataFolder() => base/plugins/PluginA/
|
||||
// pluginsFolder => base/plugins/
|
||||
// The base is not necessarily relative to the startup directory.
|
||||
File pluginsFolder = plugin.getDataFolder().getParentFile();
|
||||
|
||||
// return => base/plugins/PluginMetrics/config.yml
|
||||
return new File(new File(pluginsFolder, "PluginMetrics"), "config.yml");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mineshafter is present. If it is, we need to bypass it to send POST requests
|
||||
*
|
||||
* @return true if mineshafter is installed on the server
|
||||
*/
|
||||
private boolean isMineshafterPresent() {
|
||||
try {
|
||||
Class.forName("mineshafter.MineServer");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the server owner denied plugin metrics?
|
||||
*
|
||||
* @return true if metrics should be opted out of it
|
||||
*/
|
||||
public boolean isOptOut() {
|
||||
synchronized (optOutLock) {
|
||||
try {
|
||||
// Reload the metrics file
|
||||
configuration.load(getConfigFile());
|
||||
} catch (IOException ex) {
|
||||
if (debug) {
|
||||
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
|
||||
}
|
||||
return true;
|
||||
} catch (InvalidConfigurationException ex) {
|
||||
if (debug) {
|
||||
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + ex.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return configuration.getBoolean("opt-out", false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method that posts a plugin to the metrics website
|
||||
*/
|
||||
private void postPlugin(final boolean isPing) throws IOException {
|
||||
// Server software specific section
|
||||
PluginDescriptionFile description = plugin.getDescription();
|
||||
String pluginName = description.getName();
|
||||
boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if
|
||||
// online mode
|
||||
// is enabled
|
||||
String pluginVersion = description.getVersion();
|
||||
String serverVersion = Bukkit.getVersion();
|
||||
int playersOnline = Bukkit.getServer().getOnlinePlayers().size();
|
||||
|
||||
// END server software specific section -- all code below does not use
|
||||
// any code outside of this class / Java
|
||||
|
||||
// Construct the post data
|
||||
StringBuilder json = new StringBuilder(1024);
|
||||
json.append('{');
|
||||
|
||||
// The plugin's description file containg all of the plugin data such as
|
||||
// name, version, author, etc
|
||||
appendJSONPair(json, "guid", guid);
|
||||
appendJSONPair(json, "plugin_version", pluginVersion);
|
||||
appendJSONPair(json, "server_version", serverVersion);
|
||||
appendJSONPair(json, "players_online", Integer.toString(playersOnline));
|
||||
|
||||
// New data as of R6
|
||||
String osname = System.getProperty("os.name");
|
||||
String osarch = System.getProperty("os.arch");
|
||||
String osversion = System.getProperty("os.version");
|
||||
String java_version = System.getProperty("java.version");
|
||||
int coreCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
// normalize os arch .. amd64 -> x86_64
|
||||
if (osarch.equals("amd64")) {
|
||||
osarch = "x86_64";
|
||||
}
|
||||
|
||||
appendJSONPair(json, "osname", osname);
|
||||
appendJSONPair(json, "osarch", osarch);
|
||||
appendJSONPair(json, "osversion", osversion);
|
||||
appendJSONPair(json, "cores", Integer.toString(coreCount));
|
||||
appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0");
|
||||
appendJSONPair(json, "java_version", java_version);
|
||||
|
||||
// If we're pinging, append it
|
||||
if (isPing) {
|
||||
appendJSONPair(json, "ping", "1");
|
||||
}
|
||||
|
||||
if (graphs.size() > 0) {
|
||||
synchronized (graphs) {
|
||||
json.append(',');
|
||||
json.append('"');
|
||||
json.append("graphs");
|
||||
json.append('"');
|
||||
json.append(':');
|
||||
json.append('{');
|
||||
|
||||
boolean firstGraph = true;
|
||||
|
||||
final Iterator<Graph> iter = graphs.iterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
Graph graph = iter.next();
|
||||
|
||||
StringBuilder graphJson = new StringBuilder();
|
||||
graphJson.append('{');
|
||||
|
||||
for (Plotter plotter : graph.getPlotters()) {
|
||||
appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue()));
|
||||
}
|
||||
|
||||
graphJson.append('}');
|
||||
|
||||
if (!firstGraph) {
|
||||
json.append(',');
|
||||
}
|
||||
|
||||
json.append(escapeJSON(graph.getName()));
|
||||
json.append(':');
|
||||
json.append(graphJson);
|
||||
|
||||
firstGraph = false;
|
||||
}
|
||||
|
||||
json.append('}');
|
||||
}
|
||||
}
|
||||
|
||||
// close json
|
||||
json.append('}');
|
||||
|
||||
// Create the url
|
||||
URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName)));
|
||||
|
||||
// Connect to the website
|
||||
URLConnection connection;
|
||||
|
||||
// Mineshafter creates a socks proxy, so we can safely bypass it
|
||||
// It does not reroute POST requests so we need to go around it
|
||||
if (isMineshafterPresent()) {
|
||||
connection = url.openConnection(Proxy.NO_PROXY);
|
||||
} else {
|
||||
connection = url.openConnection();
|
||||
}
|
||||
|
||||
byte[] uncompressed = json.toString().getBytes();
|
||||
byte[] compressed = gzip(json.toString());
|
||||
|
||||
// Headers
|
||||
connection.addRequestProperty("User-Agent", "MCStats/" + REVISION);
|
||||
connection.addRequestProperty("Content-Type", "application/json");
|
||||
connection.addRequestProperty("Content-Encoding", "gzip");
|
||||
connection.addRequestProperty("Content-Length", Integer.toString(compressed.length));
|
||||
connection.addRequestProperty("Accept", "application/json");
|
||||
connection.addRequestProperty("Connection", "close");
|
||||
|
||||
connection.setDoOutput(true);
|
||||
|
||||
if (debug) {
|
||||
System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length
|
||||
+ " compressed=" + compressed.length);
|
||||
}
|
||||
|
||||
// Write the data
|
||||
OutputStream os = connection.getOutputStream();
|
||||
os.write(compressed);
|
||||
os.flush();
|
||||
|
||||
// Now read the response
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String response = reader.readLine();
|
||||
|
||||
// close resources
|
||||
os.close();
|
||||
reader.close();
|
||||
|
||||
if (response == null || response.startsWith("ERR") || response.startsWith("7")) {
|
||||
if (response == null) {
|
||||
response = "null";
|
||||
} else if (response.startsWith("7")) {
|
||||
response = response.substring(response.startsWith("7,") ? 2 : 1);
|
||||
}
|
||||
|
||||
throw new IOException(response);
|
||||
} else {
|
||||
// Is this the first update this hour?
|
||||
if (response.equals("1") || response.contains("This is your first update this hour")) {
|
||||
synchronized (graphs) {
|
||||
final Iterator<Graph> iter = graphs.iterator();
|
||||
|
||||
while (iter.hasNext()) {
|
||||
final Graph graph = iter.next();
|
||||
|
||||
for (Plotter plotter : graph.getPlotters()) {
|
||||
plotter.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start measuring statistics. This will immediately create an async repeating task as the plugin and send the
|
||||
* initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200
|
||||
* ticks.
|
||||
*
|
||||
* @return True if statistics measuring is running, otherwise false.
|
||||
*/
|
||||
public boolean start() {
|
||||
synchronized (optOutLock) {
|
||||
// Did we opt out?
|
||||
if (isOptOut()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is metrics already running?
|
||||
if (task != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Begin hitting the server with glorious data
|
||||
task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() {
|
||||
|
||||
private boolean firstPost = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// This has to be synchronized or it can collide with
|
||||
// the disable method.
|
||||
synchronized (optOutLock) {
|
||||
// Disable Task, if it is running and the server
|
||||
// owner decided to opt-out
|
||||
if (isOptOut() && task != null) {
|
||||
task.cancel();
|
||||
task = null;
|
||||
// Tell all plotters to stop gathering
|
||||
// information.
|
||||
for (Graph graph : graphs) {
|
||||
graph.onOptOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use the inverse of firstPost because if it is the
|
||||
// first time we are posting,
|
||||
// it is not a interval ping, so it evaluates to FALSE
|
||||
// Each time thereafter it will evaluate to TRUE, i.e
|
||||
// PING!
|
||||
postPlugin(!firstPost);
|
||||
|
||||
// After the first post we set firstPost to false
|
||||
// Each post thereafter will be a ping
|
||||
firstPost = false;
|
||||
} catch (IOException e) {
|
||||
if (debug) {
|
||||
Bukkit.getLogger().log(Level.INFO, "[Metrics] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0, PING_INTERVAL * 1200);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom graph on the website
|
||||
*/
|
||||
public static class Graph {
|
||||
|
||||
/**
|
||||
* The graph's name, alphanumeric and spaces only :) If it does not comply to the above when submitted, it is
|
||||
* rejected
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* The set of plotters that are contained within this graph
|
||||
*/
|
||||
private final Set<Plotter> plotters = new LinkedHashSet<Plotter>();
|
||||
|
||||
private Graph(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a plotter to the graph, which will be used to plot entries
|
||||
*
|
||||
* @param plotter
|
||||
* the plotter to add to the graph
|
||||
*/
|
||||
public void addPlotter(final Plotter plotter) {
|
||||
plotters.add(plotter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object object) {
|
||||
if (!(object instanceof Graph)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Graph graph = (Graph) object;
|
||||
return graph.name.equals(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the graph's name
|
||||
*
|
||||
* @return the Graph's name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an <b>unmodifiable</b> set of the plotter objects in the graph
|
||||
*
|
||||
* @return an unmodifiable {@link java.util.Set} of the plotter objects
|
||||
*/
|
||||
public Set<Plotter> getPlotters() {
|
||||
return Collections.unmodifiableSet(plotters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the server owner decides to opt-out of BukkitMetrics while the server is running.
|
||||
*/
|
||||
protected void onOptOut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a plotter from the graph
|
||||
*
|
||||
* @param plotter
|
||||
* the plotter to remove from the graph
|
||||
*/
|
||||
public void removePlotter(final Plotter plotter) {
|
||||
plotters.remove(plotter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to collect custom data for a plugin
|
||||
*/
|
||||
public static abstract class Plotter {
|
||||
|
||||
/**
|
||||
* The plot's name
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Construct a plotter with the default plot name
|
||||
*/
|
||||
public Plotter() {
|
||||
this("Default");
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a plotter with a specific plot name
|
||||
*
|
||||
* @param name
|
||||
* the name of the plotter to use, which will show up on the website
|
||||
*/
|
||||
public Plotter(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object object) {
|
||||
if (!(object instanceof Plotter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Plotter plotter = (Plotter) object;
|
||||
return plotter.name.equals(name) && plotter.getValue() == getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column name for the plotted point
|
||||
*
|
||||
* @return the plotted point's column name
|
||||
*/
|
||||
public String getColumnName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value for the plotted point. Since this function defers to an external function it may or may
|
||||
* not return immediately thus cannot be guaranteed to be thread friendly or safe. This function can be called
|
||||
* from any thread so care should be taken when accessing resources that need to be synchronized.
|
||||
*
|
||||
* @return the current value for the point to be plotted.
|
||||
*/
|
||||
public abstract int getValue();
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getColumnName().hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the website graphs have been updated
|
||||
*/
|
||||
public void reset() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a json encoded key/value pair to the given string builder.
|
||||
*
|
||||
* @param json
|
||||
* @param key
|
||||
* @param value
|
||||
* @throws UnsupportedEncodingException
|
||||
*/
|
||||
private static void appendJSONPair(StringBuilder json, String key, String value)
|
||||
throws UnsupportedEncodingException {
|
||||
boolean isValueNumeric = false;
|
||||
|
||||
try {
|
||||
if (value.equals("0") || !value.endsWith("0")) {
|
||||
Double.parseDouble(value);
|
||||
isValueNumeric = true;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
isValueNumeric = false;
|
||||
}
|
||||
|
||||
if (json.charAt(json.length() - 1) != '{') {
|
||||
json.append(',');
|
||||
}
|
||||
|
||||
json.append(escapeJSON(key));
|
||||
json.append(':');
|
||||
|
||||
if (isValueNumeric) {
|
||||
json.append(value);
|
||||
} else {
|
||||
json.append(escapeJSON(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string to create a valid JSON string
|
||||
*
|
||||
* @param text
|
||||
* @return
|
||||
*/
|
||||
private static String escapeJSON(String text) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append('"');
|
||||
for (int index = 0; index < text.length(); index++) {
|
||||
char chr = text.charAt(index);
|
||||
|
||||
switch (chr) {
|
||||
case '"':
|
||||
case '\\':
|
||||
builder.append('\\');
|
||||
builder.append(chr);
|
||||
break;
|
||||
case '\b':
|
||||
builder.append("\\b");
|
||||
break;
|
||||
case '\t':
|
||||
builder.append("\\t");
|
||||
break;
|
||||
case '\n':
|
||||
builder.append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.append("\\r");
|
||||
break;
|
||||
default:
|
||||
if (chr < ' ') {
|
||||
String t = "000" + Integer.toHexString(chr);
|
||||
builder.append("\\u" + t.substring(t.length() - 4));
|
||||
} else {
|
||||
builder.append(chr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
builder.append('"');
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* GZip compress a string of bytes
|
||||
*
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
public static byte[] gzip(String input) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
GZIPOutputStream gzos = null;
|
||||
|
||||
try {
|
||||
gzos = new GZIPOutputStream(baos);
|
||||
gzos.write(input.getBytes("UTF-8"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (gzos != null)
|
||||
try {
|
||||
gzos.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode text as UTF-8
|
||||
*
|
||||
* @param text
|
||||
* the text to encode
|
||||
* @return the encoded text, as UTF-8
|
||||
*/
|
||||
private static String urlEncode(final String text) throws UnsupportedEncodingException {
|
||||
return URLEncoder.encode(text, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* The base url of the metrics domain
|
||||
*/
|
||||
private static final String BASE_URL = "http://report.mcstats.org";
|
||||
|
||||
/**
|
||||
* Interval of time to ping (in minutes)
|
||||
*/
|
||||
private static final int PING_INTERVAL = 15;
|
||||
|
||||
/**
|
||||
* The url used to report a server's status
|
||||
*/
|
||||
private static final String REPORT_URL = "/plugin/%s";
|
||||
|
||||
/**
|
||||
* The current revision number
|
||||
*/
|
||||
private final static int REVISION = 7;
|
||||
}
|
31
main/java/net/citizensnpcs/NPCNeedsRespawnEvent.java
Normal file
31
main/java/net/citizensnpcs/NPCNeedsRespawnEvent.java
Normal file
@ -0,0 +1,31 @@
|
||||
package net.citizensnpcs;
|
||||
|
||||
import net.citizensnpcs.api.event.NPCEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
public class NPCNeedsRespawnEvent extends NPCEvent {
|
||||
private final Location spawn;
|
||||
|
||||
public NPCNeedsRespawnEvent(NPC npc, Location at) {
|
||||
super(npc);
|
||||
this.spawn = at;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
public Location getSpawnLocation() {
|
||||
return spawn;
|
||||
}
|
||||
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
}
|
38
main/java/net/citizensnpcs/PaymentListener.java
Normal file
38
main/java/net/citizensnpcs/PaymentListener.java
Normal file
@ -0,0 +1,38 @@
|
||||
package net.citizensnpcs;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.event.PlayerCreateNPCEvent;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.milkbowl.vault.economy.Economy;
|
||||
import net.milkbowl.vault.economy.EconomyResponse;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
public class PaymentListener implements Listener {
|
||||
private final Economy provider;
|
||||
|
||||
public PaymentListener(Economy provider) {
|
||||
Preconditions.checkNotNull(provider, "provider cannot be null");
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerCreateNPC(PlayerCreateNPCEvent event) {
|
||||
boolean hasAccount = provider.hasAccount(event.getCreator());
|
||||
if (!hasAccount || event.getCreator().hasPermission("citizens.npc.ignore-cost"))
|
||||
return;
|
||||
double cost = Setting.NPC_COST.asDouble();
|
||||
EconomyResponse response = provider.withdrawPlayer(event.getCreator(), cost);
|
||||
if (!response.transactionSuccess()) {
|
||||
event.setCancelled(true);
|
||||
event.setCancelReason(response.errorMessage);
|
||||
return;
|
||||
}
|
||||
String formattedCost = provider.format(cost);
|
||||
Messaging.sendTr(event.getCreator(), Messages.MONEY_WITHDRAWN, formattedCost);
|
||||
}
|
||||
}
|
181
main/java/net/citizensnpcs/Settings.java
Normal file
181
main/java/net/citizensnpcs/Settings.java
Normal file
@ -0,0 +1,181 @@
|
||||
package net.citizensnpcs;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.api.util.Storage;
|
||||
import net.citizensnpcs.api.util.YamlStorage;
|
||||
|
||||
public class Settings {
|
||||
private final Storage config;
|
||||
private final DataKey root;
|
||||
|
||||
public Settings(File folder) {
|
||||
config = new YamlStorage(new File(folder, "config.yml"), "Citizens Configuration");
|
||||
root = config.getKey("");
|
||||
|
||||
config.load();
|
||||
for (Setting setting : Setting.values()) {
|
||||
if (!root.keyExists(setting.path)) {
|
||||
setting.setAtKey(root);
|
||||
} else
|
||||
setting.loadFromKey(root);
|
||||
}
|
||||
updateMessagingSettings();
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
config.load();
|
||||
for (Setting setting : Setting.values()) {
|
||||
if (root.keyExists(setting.path)) {
|
||||
setting.loadFromKey(root);
|
||||
}
|
||||
}
|
||||
updateMessagingSettings();
|
||||
save();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
config.save();
|
||||
}
|
||||
|
||||
private void updateMessagingSettings() {
|
||||
File file = null;
|
||||
if (!Setting.DEBUG_FILE.asString().isEmpty()) {
|
||||
file = new File(CitizensAPI.getPlugin().getDataFolder(), Setting.DEBUG_FILE.asString());
|
||||
}
|
||||
Messaging.configure(file, Setting.DEBUG_MODE.asBoolean(), Setting.MESSAGE_COLOUR.asString(),
|
||||
Setting.HIGHLIGHT_COLOUR.asString());
|
||||
}
|
||||
|
||||
public enum Setting {
|
||||
AUTH_SERVER_URL("general.authlib.profile-url", "https://sessionserver.mojang.com/session/minecraft/profile/"),
|
||||
CHAT_BYSTANDERS_HEAR_TARGETED_CHAT("npc.chat.options.bystanders-hear-targeted-chat", true),
|
||||
CHAT_FORMAT("npc.chat.format.no-targets", "[<npc>]: <text>"),
|
||||
CHAT_FORMAT_TO_BYSTANDERS("npc.chat.format.with-target-to-bystanders", "[<npc>] -> [<target>]: <text>"),
|
||||
CHAT_FORMAT_TO_TARGET("npc.chat.format.to-target", "[<npc>] -> You: <text>"),
|
||||
CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS("npc.chat.format.with-targets-to-bystanders",
|
||||
"[<npc>] -> [<targets>]: <text>"),
|
||||
CHAT_MAX_NUMBER_OF_TARGETS("npc.chat.options.max-number-of-targets-to-show", 2),
|
||||
CHAT_MULTIPLE_TARGETS_FORMAT("npc.chat.options.multiple-targets-format",
|
||||
"<target>|, <target>| & <target>| & others"),
|
||||
CHAT_RANGE("npc.chat.options.range", 5),
|
||||
CHECK_MINECRAFT_VERSION("advanced.check-minecraft-version", true),
|
||||
DEBUG_FILE("general.debug-file", ""),
|
||||
DEBUG_MODE("general.debug-mode", false),
|
||||
DEBUG_PATHFINDING("general.debug-pathfinding", false),
|
||||
DEFAULT_DISTANCE_MARGIN("npc.pathfinding.default-distance-margin", 2),
|
||||
DEFAULT_LOOK_CLOSE("npc.default.look-close.enabled", false),
|
||||
DEFAULT_LOOK_CLOSE_RANGE("npc.default.look-close.range", 5),
|
||||
DEFAULT_NPC_LIMIT("npc.limits.default-limit", 10),
|
||||
DEFAULT_PATHFINDER_UPDATE_PATH_RATE("npc.pathfinding.update-path-rate", 20),
|
||||
DEFAULT_PATHFINDING_RANGE("npc.default.pathfinding.range", 25F),
|
||||
DEFAULT_RANDOM_TALKER("npc.default.random-talker", true),
|
||||
DEFAULT_REALISTIC_LOOKING("npc.default.realistic-looking", false),
|
||||
DEFAULT_STATIONARY_TICKS("npc.default.stationary-ticks", -1),
|
||||
DEFAULT_TALK_CLOSE("npc.default.talk-close.enabled", false),
|
||||
DEFAULT_TALK_CLOSE_RANGE("npc.default.talk-close.range", 5),
|
||||
DEFAULT_TEXT("npc.default.text.0", "Hi, I'm <npc>!") {
|
||||
@Override
|
||||
public void loadFromKey(DataKey root) {
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (DataKey key : root.getRelative("npc.default.text").getSubKeys())
|
||||
list.add(key.getString(""));
|
||||
value = list;
|
||||
}
|
||||
},
|
||||
DISABLE_TABLIST("npc.tablist.disable", true),
|
||||
HIGHLIGHT_COLOUR("general.color-scheme.message-highlight", "<e>"),
|
||||
KEEP_CHUNKS_LOADED("npc.chunks.always-keep-loaded", false),
|
||||
LOCALE("general.translation.locale", ""),
|
||||
MAX_NPC_LIMIT_CHECKS("npc.limits.max-permission-checks", 100),
|
||||
MAX_NPC_SKIN_RETRIES("npc.skins.max-retries", -1),
|
||||
MAX_PACKET_ENTRIES("npc.limits.max-packet-entries", 15),
|
||||
MAX_SPEED("npc.limits.max-speed", 100),
|
||||
MAX_TEXT_RANGE("npc.chat.options.max-text-range", 500),
|
||||
MESSAGE_COLOUR("general.color-scheme.message", "<a>"),
|
||||
NEW_PATHFINDER_OPENS_DOORS("npc.pathfinding.new-finder-open-doors", false),
|
||||
NPC_ATTACK_DISTANCE("npc.pathfinding.attack-range", 1.75 * 1.75),
|
||||
NPC_COST("economy.npc.cost", 100D),
|
||||
NPC_SKIN_RETRY_DELAY("npc.skins.retry-delay", 120),
|
||||
NPC_SKIN_ROTATION_UPDATE_DEGREES("npc.skins.rotation-update-degrees", 90f),
|
||||
NPC_SKIN_USE_LATEST("npc.skins.use-latest", true),
|
||||
NPC_SKIN_VIEW_DISTANCE("npc.skins.view-distance", 100D),
|
||||
PACKET_UPDATE_DELAY("npc.packets.update-delay", 30),
|
||||
QUICK_SELECT("npc.selection.quick-select", false),
|
||||
REMOVE_PLAYERS_FROM_PLAYER_LIST("npc.player.remove-from-list", true),
|
||||
SAVE_TASK_DELAY("storage.save-task.delay", 20 * 60 * 60),
|
||||
SELECTION_ITEM("npc.selection.item", "280"),
|
||||
SELECTION_MESSAGE("npc.selection.message", "<b>You selected <a><npc><b>!"),
|
||||
SERVER_OWNS_NPCS("npc.server-ownership", false),
|
||||
STORAGE_FILE("storage.file", "saves.yml"),
|
||||
STORAGE_TYPE("storage.type", "yaml"),
|
||||
SUBPLUGIN_FOLDER("subplugins.folder", "plugins"),
|
||||
TALK_CLOSE_MAXIMUM_COOLDOWN("npc.text.max-talk-cooldown", 5),
|
||||
TALK_CLOSE_MINIMUM_COOLDOWN("npc.text.min-talk-cooldown", 10),
|
||||
TALK_ITEM("npc.text.talk-item", "340"),
|
||||
TELEPORT_DELAY("npc.teleport-delay", -1),
|
||||
USE_BOAT_CONTROLS("npc.controllable.use-boat-controls", true),
|
||||
USE_NEW_PATHFINDER("npc.pathfinding.use-new-finder", false),
|
||||
USE_SCOREBOARD_TEAMS("npc.player-scoreboard-teams.enable", true);
|
||||
|
||||
protected String path;
|
||||
protected Object value;
|
||||
|
||||
Setting(String path, Object value) {
|
||||
this.path = path;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean asBoolean() {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
public double asDouble() {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
|
||||
public float asFloat() {
|
||||
return ((Number) value).floatValue();
|
||||
}
|
||||
|
||||
public int asInt() {
|
||||
if (value instanceof String) {
|
||||
return Integer.parseInt(value.toString());
|
||||
}
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> asList() {
|
||||
if (!(value instanceof List)) {
|
||||
value = Lists.newArrayList(value);
|
||||
}
|
||||
return (List<String>) value;
|
||||
}
|
||||
|
||||
public long asLong() {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
protected void loadFromKey(DataKey root) {
|
||||
value = root.getRaw(path);
|
||||
}
|
||||
|
||||
protected void setAtKey(DataKey root) {
|
||||
root.setRaw(path, value);
|
||||
}
|
||||
}
|
||||
}
|
67
main/java/net/citizensnpcs/commands/AdminCommands.java
Normal file
67
main/java/net/citizensnpcs/commands/AdminCommands.java
Normal file
@ -0,0 +1,67 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import net.citizensnpcs.Citizens;
|
||||
import net.citizensnpcs.api.command.Command;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.Requirements;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.StringHelper;
|
||||
|
||||
@Requirements
|
||||
public class AdminCommands {
|
||||
private final Citizens plugin;
|
||||
|
||||
public AdminCommands(Citizens plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Command(aliases = { "citizens" }, desc = "Show basic plugin information", max = 0, permission = "citizens.admin")
|
||||
public void citizens(CommandContext args, CommandSender sender, NPC npc) {
|
||||
Messaging.send(sender,
|
||||
" " + StringHelper.wrapHeader("<e>Citizens v" + plugin.getDescription().getVersion()));
|
||||
Messaging.send(sender, " <7>-- <c>Written by fullwall and aPunch");
|
||||
Messaging.send(sender, " <7>-- <c>Source Code: http://github.com/CitizensDev");
|
||||
Messaging.send(sender, " <7>-- <c>Website: " + plugin.getDescription().getWebsite());
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "citizens" },
|
||||
usage = "reload",
|
||||
desc = "Reload Citizens",
|
||||
modifiers = { "reload" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.admin")
|
||||
public void reload(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
Messaging.sendTr(sender, Messages.CITIZENS_RELOADING);
|
||||
try {
|
||||
plugin.reload();
|
||||
Messaging.sendTr(sender, Messages.CITIZENS_RELOADED);
|
||||
} catch (NPCLoadException ex) {
|
||||
ex.printStackTrace();
|
||||
throw new CommandException(Messages.CITIZENS_RELOAD_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "citizens" },
|
||||
usage = "save (-a)",
|
||||
desc = "Save NPCs",
|
||||
help = Messages.COMMAND_SAVE_HELP,
|
||||
modifiers = { "save" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
flags = "a",
|
||||
permission = "citizens.admin")
|
||||
public void save(CommandContext args, CommandSender sender, NPC npc) {
|
||||
Messaging.sendTr(sender, Messages.CITIZENS_SAVING);
|
||||
plugin.storeNPCs(args);
|
||||
Messaging.sendTr(sender, Messages.CITIZENS_SAVED);
|
||||
}
|
||||
}
|
70
main/java/net/citizensnpcs/commands/EditorCommands.java
Normal file
70
main/java/net/citizensnpcs/commands/EditorCommands.java
Normal file
@ -0,0 +1,70 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import net.citizensnpcs.api.command.Command;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.Requirements;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.editor.CopierEditor;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.editor.EquipmentEditor;
|
||||
import net.citizensnpcs.trait.text.Text;
|
||||
import net.citizensnpcs.trait.waypoint.Waypoints;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@Requirements(selected = true, ownership = true)
|
||||
public class EditorCommands {
|
||||
@Command(
|
||||
aliases = { "npc" },
|
||||
usage = "copier",
|
||||
desc = "Toggle the NPC copier",
|
||||
modifiers = { "copier" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.npc.edit.copier")
|
||||
public void copier(CommandContext args, Player player, NPC npc) {
|
||||
Editor.enterOrLeave(player, new CopierEditor(player, npc));
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "npc" },
|
||||
usage = "equip",
|
||||
desc = "Toggle the equipment editor",
|
||||
modifiers = { "equip" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.npc.edit.equip")
|
||||
public void equip(CommandContext args, Player player, NPC npc) {
|
||||
Editor.enterOrLeave(player, new EquipmentEditor(player, npc));
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "npc" },
|
||||
usage = "path",
|
||||
desc = "Toggle the waypoint editor",
|
||||
modifiers = { "path" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
flags = "*",
|
||||
permission = "citizens.npc.edit.path")
|
||||
@Requirements(selected = true, ownership = true)
|
||||
public void path(CommandContext args, CommandSender player, NPC npc) {
|
||||
Editor editor = npc.getTrait(Waypoints.class).getEditor(player, args);
|
||||
if (editor == null)
|
||||
return;
|
||||
Editor.enterOrLeave((Player) player, editor);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "npc" },
|
||||
usage = "text",
|
||||
desc = "Toggle the text editor",
|
||||
modifiers = { "text" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.npc.edit.text")
|
||||
public void text(CommandContext args, Player player, NPC npc) {
|
||||
Editor.enterOrLeave(player, npc.getTrait(Text.class).getEditor(player));
|
||||
}
|
||||
}
|
121
main/java/net/citizensnpcs/commands/NPCCommandSelector.java
Normal file
121
main/java/net/citizensnpcs/commands/NPCCommandSelector.java
Normal file
@ -0,0 +1,121 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.CommandMessages;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.command.exception.CommandUsageException;
|
||||
import net.citizensnpcs.api.command.exception.ServerCommandException;
|
||||
import net.citizensnpcs.api.command.exception.UnhandledCommandException;
|
||||
import net.citizensnpcs.api.command.exception.WrappedCommandException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.conversations.Conversable;
|
||||
import org.bukkit.conversations.Conversation;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.ConversationFactory;
|
||||
import org.bukkit.conversations.NumericPrompt;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
public class NPCCommandSelector extends NumericPrompt {
|
||||
private final Callback callback;
|
||||
private final List<NPC> choices;
|
||||
|
||||
public NPCCommandSelector(Callback callback, List<NPC> possible) {
|
||||
this.callback = callback;
|
||||
this.choices = possible;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Prompt acceptValidatedInput(ConversationContext context, Number input) {
|
||||
boolean found = false;
|
||||
for (NPC npc : choices) {
|
||||
if (input.intValue() == npc.getId()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CommandSender sender = (CommandSender) context.getForWhom();
|
||||
if (!found) {
|
||||
Messaging.sendErrorTr(sender, Messages.SELECTION_PROMPT_INVALID_CHOICE, input);
|
||||
return this;
|
||||
}
|
||||
NPC toSelect = CitizensAPI.getNPCRegistry().getById(input.intValue());
|
||||
try {
|
||||
callback.run(toSelect);
|
||||
} catch (ServerCommandException ex) {
|
||||
Messaging.sendTr(sender, CommandMessages.MUST_BE_INGAME);
|
||||
} catch (CommandUsageException ex) {
|
||||
Messaging.sendError(sender, ex.getMessage());
|
||||
Messaging.sendError(sender, ex.getUsage());
|
||||
} catch (UnhandledCommandException ex) {
|
||||
ex.printStackTrace();
|
||||
} catch (WrappedCommandException ex) {
|
||||
ex.getCause().printStackTrace();
|
||||
} catch (CommandException ex) {
|
||||
Messaging.sendError(sender, ex.getMessage());
|
||||
} catch (NumberFormatException ex) {
|
||||
Messaging.sendErrorTr(sender, CommandMessages.INVALID_NUMBER);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
String text = Messaging.tr(Messages.SELECTION_PROMPT);
|
||||
for (NPC npc : choices) {
|
||||
text += "\n - " + npc.getId();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public static interface Callback {
|
||||
public void run(NPC npc) throws CommandException;
|
||||
}
|
||||
|
||||
public static void start(Callback callback, Conversable player, List<NPC> possible) {
|
||||
final Conversation conversation = new ConversationFactory(CitizensAPI.getPlugin()).withLocalEcho(false)
|
||||
.withEscapeSequence("exit").withModality(false)
|
||||
.withFirstPrompt(new NPCCommandSelector(callback, possible)).buildConversation(player);
|
||||
conversation.begin();
|
||||
}
|
||||
|
||||
public static void startWithCallback(Callback callback, NPCRegistry npcRegistry, CommandSender sender,
|
||||
CommandContext args, String raw) throws CommandException {
|
||||
try {
|
||||
int id = Integer.parseInt(raw);
|
||||
callback.run(npcRegistry.getById(id));
|
||||
return;
|
||||
} catch (NumberFormatException ex) {
|
||||
String name = args.getString(1);
|
||||
List<NPC> possible = Lists.newArrayList();
|
||||
double range = -1;
|
||||
if (args.hasValueFlag("r")) {
|
||||
range = Math.abs(args.getFlagDouble("r"));
|
||||
}
|
||||
for (NPC test : npcRegistry) {
|
||||
if (test.getName().equalsIgnoreCase(name)) {
|
||||
if (range > 0 && test.isSpawned() && !Util.locationWithinRange(args.getSenderLocation(),
|
||||
test.getEntity().getLocation(), range))
|
||||
continue;
|
||||
possible.add(test);
|
||||
}
|
||||
}
|
||||
if (possible.size() == 1) {
|
||||
callback.run(possible.get(0));
|
||||
} else if (possible.size() > 1) {
|
||||
NPCCommandSelector.start(callback, (Conversable) sender, possible);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1997
main/java/net/citizensnpcs/commands/NPCCommands.java
Normal file
1997
main/java/net/citizensnpcs/commands/NPCCommands.java
Normal file
File diff suppressed because it is too large
Load Diff
120
main/java/net/citizensnpcs/commands/TemplateCommands.java
Normal file
120
main/java/net/citizensnpcs/commands/TemplateCommands.java
Normal file
@ -0,0 +1,120 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import net.citizensnpcs.Citizens;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.command.Command;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.Requirements;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.npc.Template;
|
||||
import net.citizensnpcs.npc.Template.TemplateBuilder;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
@Requirements(selected = true, ownership = true)
|
||||
public class TemplateCommands {
|
||||
public TemplateCommands(Citizens plugin) {
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "template", "tpl" },
|
||||
usage = "apply [template name] (id id2...)",
|
||||
desc = "Applies a template to the selected NPC",
|
||||
modifiers = { "apply" },
|
||||
min = 2,
|
||||
permission = "citizens.templates.apply")
|
||||
@Requirements
|
||||
public void apply(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
Template template = Template.byName(args.getString(1));
|
||||
if (template == null)
|
||||
throw new CommandException(Messages.TEMPLATE_MISSING);
|
||||
int appliedCount = 0;
|
||||
if (args.argsLength() == 2) {
|
||||
if (npc == null)
|
||||
throw new CommandException(Messaging.tr(Messages.COMMAND_MUST_HAVE_SELECTED));
|
||||
template.apply(npc);
|
||||
appliedCount++;
|
||||
} else {
|
||||
String joined = args.getJoinedStrings(2, ',');
|
||||
List<Integer> ids = Lists.newArrayList();
|
||||
for (String id : Splitter.on(',').trimResults().split(joined)) {
|
||||
int parsed = Integer.parseInt(id);
|
||||
ids.add(parsed);
|
||||
}
|
||||
Iterable<NPC> transformed = Iterables.transform(ids, new Function<Integer, NPC>() {
|
||||
@Override
|
||||
public NPC apply(@Nullable Integer arg0) {
|
||||
if (arg0 == null)
|
||||
return null;
|
||||
return CitizensAPI.getNPCRegistry().getById(arg0);
|
||||
}
|
||||
});
|
||||
for (NPC toApply : transformed) {
|
||||
template.apply(toApply);
|
||||
appliedCount++;
|
||||
}
|
||||
}
|
||||
Messaging.sendTr(sender, Messages.TEMPLATE_APPLIED, appliedCount);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "template", "tpl" },
|
||||
usage = "create [template name] (-o)",
|
||||
desc = "Creates a template from the selected NPC",
|
||||
modifiers = { "create" },
|
||||
min = 2,
|
||||
max = 2,
|
||||
flags = "o",
|
||||
permission = "citizens.templates.create")
|
||||
public void create(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
String name = args.getString(1);
|
||||
if (Template.byName(name) != null)
|
||||
throw new CommandException(Messages.TEMPLATE_CONFLICT);
|
||||
|
||||
TemplateBuilder.create(name).from(npc).override(args.hasFlag('o')).buildAndSave();
|
||||
Messaging.sendTr(sender, Messages.TEMPLATE_CREATED);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "template", "tpl" },
|
||||
usage = "delete [template name]",
|
||||
desc = "Deletes a template",
|
||||
modifiers = { "delete" },
|
||||
min = 2,
|
||||
max = 2,
|
||||
permission = "citizens.templates.delete")
|
||||
public void delete(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
String name = args.getString(1);
|
||||
if (Template.byName(name) == null)
|
||||
throw new CommandException(Messages.TEMPLATE_MISSING);
|
||||
Template.byName(name).delete();
|
||||
Messaging.sendTr(sender, Messages.TEMPLATE_DELETED, name);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "template", "tpl" },
|
||||
usage = "list",
|
||||
desc = "Lists available templates",
|
||||
modifiers = { "list" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.templates.list")
|
||||
public void list(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
Messaging.sendTr(sender, Messages.TEMPLATE_LIST_HEADER);
|
||||
for (Template template : Template.allTemplates()) {
|
||||
Messaging.send(sender, "[[-]] " + template.getName());
|
||||
}
|
||||
}
|
||||
}
|
167
main/java/net/citizensnpcs/commands/TraitCommands.java
Normal file
167
main/java/net/citizensnpcs/commands/TraitCommands.java
Normal file
@ -0,0 +1,167 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.command.Command;
|
||||
import net.citizensnpcs.api.command.CommandConfigurable;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.Requirements;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.command.exception.NoPermissionsException;
|
||||
import net.citizensnpcs.api.event.NPCTraitCommandAttachEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.StringHelper;
|
||||
|
||||
@Requirements(selected = true, ownership = true)
|
||||
public class TraitCommands {
|
||||
@Command(
|
||||
aliases = { "trait", "tr" },
|
||||
usage = "add [trait name]...",
|
||||
desc = "Adds traits to the NPC",
|
||||
modifiers = { "add", "a" },
|
||||
min = 2,
|
||||
permission = "citizens.npc.trait")
|
||||
public void add(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
List<String> added = Lists.newArrayList();
|
||||
List<String> failed = Lists.newArrayList();
|
||||
for (String traitName : Splitter.on(',').split(args.getJoinedStrings(1))) {
|
||||
if (!sender.hasPermission("citizens.npc.trait." + traitName)
|
||||
&& !sender.hasPermission("citizens.npc.trait.*")) {
|
||||
failed.add(String.format("%s: No permission", traitName));
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<? extends Trait> clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName);
|
||||
if (clazz == null) {
|
||||
failed.add(String.format("%s: Trait not found", traitName));
|
||||
continue;
|
||||
}
|
||||
if (npc.hasTrait(clazz)) {
|
||||
failed.add(String.format("%s: Already added", traitName));
|
||||
continue;
|
||||
}
|
||||
addTrait(npc, clazz, sender);
|
||||
added.add(StringHelper.wrap(traitName));
|
||||
}
|
||||
if (added.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_ADDED, Joiner.on(", ").join(added));
|
||||
if (failed.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_FAILED_TO_ADD, Joiner.on(", ").join(failed));
|
||||
}
|
||||
|
||||
private void addTrait(NPC npc, Class<? extends Trait> clazz, CommandSender sender) {
|
||||
npc.addTrait(clazz);
|
||||
Bukkit.getPluginManager().callEvent(new NPCTraitCommandAttachEvent(npc, clazz, sender));
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "traitc", "trc" },
|
||||
usage = "[trait name] (flags)",
|
||||
desc = "Configures a trait",
|
||||
modifiers = { "*" },
|
||||
min = 1,
|
||||
flags = "*",
|
||||
permission = "citizens.npc.trait-configure")
|
||||
public void configure(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
String traitName = args.getString(0);
|
||||
if (!sender.hasPermission("citizens.npc.trait-configure." + traitName)
|
||||
&& !sender.hasPermission("citizens.npc.trait-configure.*"))
|
||||
throw new NoPermissionsException();
|
||||
Class<? extends Trait> clazz = CitizensAPI.getTraitFactory().getTraitClass(args.getString(0));
|
||||
if (clazz == null)
|
||||
throw new CommandException(Messages.TRAIT_NOT_FOUND);
|
||||
if (!CommandConfigurable.class.isAssignableFrom(clazz))
|
||||
throw new CommandException(Messages.TRAIT_NOT_CONFIGURABLE);
|
||||
if (!npc.hasTrait(clazz))
|
||||
throw new CommandException(Messages.TRAIT_NOT_FOUND_ON_NPC);
|
||||
CommandConfigurable trait = (CommandConfigurable) npc.getTrait(clazz);
|
||||
trait.configure(args);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "trait", "tr" },
|
||||
usage = "remove [trait name]...",
|
||||
desc = "Removes traits on the NPC",
|
||||
modifiers = { "remove", "rem", "r" },
|
||||
min = 1,
|
||||
permission = "citizens.npc.trait")
|
||||
public void remove(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
List<String> removed = Lists.newArrayList();
|
||||
List<String> failed = Lists.newArrayList();
|
||||
for (String traitName : Splitter.on(',').split(args.getJoinedStrings(0))) {
|
||||
if (!sender.hasPermission("citizens.npc.trait." + traitName)
|
||||
&& !sender.hasPermission("citizens.npc.trait.*")) {
|
||||
failed.add(String.format("%s: No permission", traitName));
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<? extends Trait> clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName);
|
||||
if (clazz == null) {
|
||||
failed.add(String.format("%s: Trait not found", traitName));
|
||||
continue;
|
||||
}
|
||||
boolean hasTrait = npc.hasTrait(clazz);
|
||||
if (!hasTrait) {
|
||||
failed.add(String.format("%s: Trait not attached", traitName));
|
||||
continue;
|
||||
}
|
||||
npc.removeTrait(clazz);
|
||||
removed.add(StringHelper.wrap(traitName));
|
||||
}
|
||||
if (removed.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_REMOVED, Joiner.on(", ").join(removed));
|
||||
if (failed.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.FAILED_TO_REMOVE, Joiner.on(", ").join(failed));
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "trait", "tr" },
|
||||
usage = "[trait name], [trait name]...",
|
||||
desc = "Toggles traits on the NPC",
|
||||
modifiers = { "*" },
|
||||
min = 1,
|
||||
permission = "citizens.npc.trait")
|
||||
public void toggle(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
List<String> added = Lists.newArrayList();
|
||||
List<String> removed = Lists.newArrayList();
|
||||
List<String> failed = Lists.newArrayList();
|
||||
for (String traitName : Splitter.on(',').split(args.getJoinedStrings(0))) {
|
||||
if (!sender.hasPermission("citizens.npc.trait." + traitName)
|
||||
&& !sender.hasPermission("citizens.npc.trait.*")) {
|
||||
failed.add(String.format("%s: No permission", traitName));
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<? extends Trait> clazz = CitizensAPI.getTraitFactory().getTraitClass(traitName);
|
||||
if (clazz == null) {
|
||||
failed.add(String.format("%s: Trait not found", traitName));
|
||||
continue;
|
||||
}
|
||||
boolean remove = npc.hasTrait(clazz);
|
||||
if (remove) {
|
||||
npc.removeTrait(clazz);
|
||||
removed.add(StringHelper.wrap(traitName));
|
||||
continue;
|
||||
}
|
||||
addTrait(npc, clazz, sender);
|
||||
added.add(StringHelper.wrap(traitName));
|
||||
}
|
||||
if (added.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_ADDED, Joiner.on(", ").join(added));
|
||||
if (removed.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_REMOVED, Joiner.on(", ").join(removed));
|
||||
if (failed.size() > 0)
|
||||
Messaging.sendTr(sender, Messages.TRAITS_FAILED_TO_CHANGE, Joiner.on(", ").join(failed));
|
||||
}
|
||||
}
|
58
main/java/net/citizensnpcs/commands/WaypointCommands.java
Normal file
58
main/java/net/citizensnpcs/commands/WaypointCommands.java
Normal file
@ -0,0 +1,58 @@
|
||||
package net.citizensnpcs.commands;
|
||||
|
||||
import net.citizensnpcs.Citizens;
|
||||
import net.citizensnpcs.api.command.Command;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.command.Requirements;
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.trait.waypoint.Waypoints;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
@Requirements(ownership = true, selected = true)
|
||||
public class WaypointCommands {
|
||||
public WaypointCommands(Citizens plugin) {
|
||||
}
|
||||
|
||||
// TODO: refactor into a policy style system
|
||||
@Command(
|
||||
aliases = { "waypoints", "waypoint", "wp" },
|
||||
usage = "disableteleport",
|
||||
desc = "Disables teleportation when stuck (temporary command)",
|
||||
modifiers = { "disableteleport" },
|
||||
min = 1,
|
||||
max = 1,
|
||||
permission = "citizens.waypoints.disableteleport")
|
||||
public void disableTeleporting(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
npc.getNavigator().getDefaultParameters().stuckAction(null);
|
||||
Messaging.sendTr(sender, Messages.WAYPOINT_TELEPORTING_DISABLED);
|
||||
}
|
||||
|
||||
@Command(
|
||||
aliases = { "waypoints", "waypoint", "wp" },
|
||||
usage = "provider [provider name] (-d)",
|
||||
desc = "Sets the current waypoint provider",
|
||||
modifiers = { "provider" },
|
||||
min = 1,
|
||||
max = 2,
|
||||
flags = "d",
|
||||
permission = "citizens.waypoints.provider")
|
||||
public void provider(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
|
||||
Waypoints waypoints = npc.getTrait(Waypoints.class);
|
||||
if (args.argsLength() == 1) {
|
||||
if (args.hasFlag('d')) {
|
||||
waypoints.describeProviders(sender);
|
||||
} else {
|
||||
Messaging.sendTr(sender, Messages.CURRENT_WAYPOINT_PROVIDER, waypoints.getCurrentProviderName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
boolean success = waypoints.setWaypointProvider(args.getString(1));
|
||||
if (!success)
|
||||
throw new CommandException("Provider not found.");
|
||||
Messaging.sendTr(sender, Messages.WAYPOINT_PROVIDER_SET, args.getString(1));
|
||||
}
|
||||
}
|
54
main/java/net/citizensnpcs/editor/CopierEditor.java
Normal file
54
main/java/net/citizensnpcs/editor/CopierEditor.java
Normal file
@ -0,0 +1,54 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
|
||||
|
||||
public class CopierEditor extends Editor {
|
||||
private final String name;
|
||||
private final NPC npc;
|
||||
private final Player player;
|
||||
|
||||
public CopierEditor(Player player, NPC npc) {
|
||||
this.player = player;
|
||||
this.npc = npc;
|
||||
this.name = npc.getFullName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
Messaging.sendTr(player, Messages.COPIER_EDITOR_BEGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
Messaging.sendTr(player, Messages.COPIER_EDITOR_END);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onBlockClick(PlayerInteractEvent event) {
|
||||
if (event.getClickedBlock() == null) {
|
||||
return;
|
||||
}
|
||||
NPC copy = npc.clone();
|
||||
if (!copy.getFullName().equals(name)) {
|
||||
copy.setName(name);
|
||||
}
|
||||
|
||||
if (copy.isSpawned() && player.isOnline()) {
|
||||
Location location = player.getLocation();
|
||||
location.getChunk().load();
|
||||
copy.teleport(location, TeleportCause.PLUGIN);
|
||||
copy.getTrait(CurrentLocation.class).setLocation(location);
|
||||
}
|
||||
|
||||
Messaging.sendTr(player, Messages.NPC_COPIED, npc.getName());
|
||||
}
|
||||
}
|
60
main/java/net/citizensnpcs/editor/Editor.java
Normal file
60
main/java/net/citizensnpcs/editor/Editor.java
Normal file
@ -0,0 +1,60 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public abstract class Editor implements Listener {
|
||||
public abstract void begin();
|
||||
|
||||
public abstract void end();
|
||||
|
||||
private static void enter(Player player, Editor editor) {
|
||||
editor.begin();
|
||||
player.getServer().getPluginManager().registerEvents(editor,
|
||||
player.getServer().getPluginManager().getPlugin("Citizens"));
|
||||
EDITING.put(player.getName(), editor);
|
||||
}
|
||||
|
||||
public static void enterOrLeave(Player player, Editor editor) {
|
||||
if (editor == null)
|
||||
return;
|
||||
Editor edit = EDITING.get(player.getName());
|
||||
if (edit == null) {
|
||||
enter(player, editor);
|
||||
} else if (edit.getClass() == editor.getClass()) {
|
||||
leave(player);
|
||||
} else {
|
||||
Messaging.sendErrorTr(player, Messages.ALREADY_IN_EDITOR);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasEditor(Player player) {
|
||||
return EDITING.containsKey(player.getName());
|
||||
}
|
||||
|
||||
public static void leave(Player player) {
|
||||
if (!hasEditor(player))
|
||||
return;
|
||||
Editor editor = EDITING.remove(player.getName());
|
||||
HandlerList.unregisterAll(editor);
|
||||
editor.end();
|
||||
}
|
||||
|
||||
public static void leaveAll() {
|
||||
for (Entry<String, Editor> entry : EDITING.entrySet()) {
|
||||
entry.getValue().end();
|
||||
HandlerList.unregisterAll(entry.getValue());
|
||||
}
|
||||
EDITING.clear();
|
||||
}
|
||||
|
||||
private static final Map<String, Editor> EDITING = new HashMap<String, Editor>();
|
||||
}
|
42
main/java/net/citizensnpcs/editor/EndermanEquipper.java
Normal file
42
main/java/net/citizensnpcs/editor/EndermanEquipper.java
Normal file
@ -0,0 +1,42 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Enderman;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.material.MaterialData;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public class EndermanEquipper implements Equipper {
|
||||
@Override
|
||||
public void equip(Player equipper, NPC npc) {
|
||||
ItemStack hand = equipper.getInventory().getItemInMainHand();
|
||||
if (!hand.getType().isBlock()) {
|
||||
Messaging.sendErrorTr(equipper, Messages.EQUIPMENT_EDITOR_INVALID_BLOCK);
|
||||
return;
|
||||
}
|
||||
|
||||
MaterialData carried = ((Enderman) npc.getEntity()).getCarriedMaterial();
|
||||
if (carried.getItemType() == Material.AIR) {
|
||||
if (hand.getType() == Material.AIR) {
|
||||
Messaging.sendErrorTr(equipper, Messages.EQUIPMENT_EDITOR_INVALID_BLOCK);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
equipper.getWorld().dropItemNaturally(npc.getEntity().getLocation(), carried.toItemStack(1));
|
||||
((Enderman) npc.getEntity()).setCarriedMaterial(hand.getData());
|
||||
}
|
||||
|
||||
ItemStack set = hand.clone();
|
||||
if (set.getType() != Material.AIR) {
|
||||
set.setAmount(1);
|
||||
hand.setAmount(hand.getAmount() - 1);
|
||||
equipper.getInventory().setItemInMainHand(hand);
|
||||
}
|
||||
npc.getTrait(Equipment.class).set(0, set);
|
||||
}
|
||||
}
|
113
main/java/net/citizensnpcs/editor/EquipmentEditor.java
Normal file
113
main/java/net/citizensnpcs/editor/EquipmentEditor.java
Normal file
@ -0,0 +1,113 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event.Result;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.AsyncPlayerChatEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment.EquipmentSlot;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public class EquipmentEditor extends Editor {
|
||||
private final NPC npc;
|
||||
private final Player player;
|
||||
|
||||
public EquipmentEditor(Player player, NPC npc) {
|
||||
this.player = player;
|
||||
this.npc = npc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
Messaging.sendTr(player, Messages.EQUIPMENT_EDITOR_BEGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
Messaging.sendTr(player, Messages.EQUIPMENT_EDITOR_END);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerChat(final AsyncPlayerChatEvent event) {
|
||||
EquipmentSlot slot = null;
|
||||
if (event.getMessage().equals("helmet")
|
||||
&& event.getPlayer().hasPermission("citizens.npc.edit.equip.any-helmet")) {
|
||||
slot = EquipmentSlot.HELMET;
|
||||
}
|
||||
if (event.getMessage().equals("offhand")
|
||||
&& event.getPlayer().hasPermission("citizens.npc.edit.equip.offhand")) {
|
||||
slot = EquipmentSlot.OFF_HAND;
|
||||
}
|
||||
if (slot == null) {
|
||||
return;
|
||||
}
|
||||
final EquipmentSlot finalSlot = slot;
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!event.getPlayer().isValid())
|
||||
return;
|
||||
ItemStack hand = event.getPlayer().getInventory().getItemInMainHand();
|
||||
if (hand.getType() == Material.AIR || hand.getAmount() <= 0) {
|
||||
return;
|
||||
}
|
||||
ItemStack old = npc.getTrait(Equipment.class).get(finalSlot);
|
||||
if (old != null && old.getType() != Material.AIR) {
|
||||
event.getPlayer().getWorld().dropItemNaturally(event.getPlayer().getLocation(), old);
|
||||
}
|
||||
ItemStack newStack = hand.clone();
|
||||
newStack.setAmount(1);
|
||||
npc.getTrait(Equipment.class).set(finalSlot, newStack);
|
||||
hand.setAmount(hand.getAmount() - 1);
|
||||
event.getPlayer().getInventory().setItemInMainHand(hand);
|
||||
}
|
||||
});
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (event.getAction() == Action.RIGHT_CLICK_AIR && Editor.hasEditor(event.getPlayer())) {
|
||||
event.setUseItemInHand(Result.DENY);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
|
||||
if (!npc.isSpawned() || !event.getPlayer().equals(player)
|
||||
|| event.getHand() != org.bukkit.inventory.EquipmentSlot.HAND
|
||||
|| !npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getRightClicked())))
|
||||
return;
|
||||
|
||||
Equipper equipper = EQUIPPERS.get(npc.getEntity().getType());
|
||||
if (equipper == null) {
|
||||
equipper = new GenericEquipper();
|
||||
}
|
||||
equipper.equip(event.getPlayer(), npc);
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
private static final Map<EntityType, Equipper> EQUIPPERS = Maps.newEnumMap(EntityType.class);
|
||||
|
||||
static {
|
||||
EQUIPPERS.put(EntityType.PIG, new PigEquipper());
|
||||
EQUIPPERS.put(EntityType.SHEEP, new SheepEquipper());
|
||||
EQUIPPERS.put(EntityType.ENDERMAN, new EndermanEquipper());
|
||||
EQUIPPERS.put(EntityType.HORSE, new HorseEquipper());
|
||||
}
|
||||
}
|
9
main/java/net/citizensnpcs/editor/Equipper.java
Normal file
9
main/java/net/citizensnpcs/editor/Equipper.java
Normal file
@ -0,0 +1,9 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public interface Equipper {
|
||||
public void equip(Player equipper, NPC toEquip);
|
||||
}
|
94
main/java/net/citizensnpcs/editor/GenericEquipper.java
Normal file
94
main/java/net/citizensnpcs/editor/GenericEquipper.java
Normal file
@ -0,0 +1,94 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment.EquipmentSlot;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public class GenericEquipper implements Equipper {
|
||||
@Override
|
||||
public void equip(Player equipper, NPC toEquip) {
|
||||
ItemStack hand = equipper.getInventory().getItemInMainHand();
|
||||
Equipment trait = toEquip.getTrait(Equipment.class);
|
||||
EquipmentSlot slot = EquipmentSlot.HAND;
|
||||
Material type = hand == null ? Material.AIR : hand.getType();
|
||||
// First, determine the slot to edit
|
||||
switch (type) {
|
||||
case SKULL_ITEM:
|
||||
case PUMPKIN:
|
||||
case JACK_O_LANTERN:
|
||||
case LEATHER_HELMET:
|
||||
case CHAINMAIL_HELMET:
|
||||
case GOLD_HELMET:
|
||||
case IRON_HELMET:
|
||||
case DIAMOND_HELMET:
|
||||
if (!equipper.isSneaking()) {
|
||||
slot = EquipmentSlot.HELMET;
|
||||
}
|
||||
break;
|
||||
case ELYTRA:
|
||||
case LEATHER_CHESTPLATE:
|
||||
case CHAINMAIL_CHESTPLATE:
|
||||
case GOLD_CHESTPLATE:
|
||||
case IRON_CHESTPLATE:
|
||||
case DIAMOND_CHESTPLATE:
|
||||
if (!equipper.isSneaking()) {
|
||||
slot = EquipmentSlot.CHESTPLATE;
|
||||
}
|
||||
break;
|
||||
case LEATHER_LEGGINGS:
|
||||
case CHAINMAIL_LEGGINGS:
|
||||
case GOLD_LEGGINGS:
|
||||
case IRON_LEGGINGS:
|
||||
case DIAMOND_LEGGINGS:
|
||||
if (!equipper.isSneaking()) {
|
||||
slot = EquipmentSlot.LEGGINGS;
|
||||
}
|
||||
break;
|
||||
case LEATHER_BOOTS:
|
||||
case CHAINMAIL_BOOTS:
|
||||
case GOLD_BOOTS:
|
||||
case IRON_BOOTS:
|
||||
case DIAMOND_BOOTS:
|
||||
if (!equipper.isSneaking()) {
|
||||
slot = EquipmentSlot.BOOTS;
|
||||
}
|
||||
break;
|
||||
case AIR:
|
||||
if (equipper.isSneaking()) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (trait.get(i) != null && trait.get(i).getType() != Material.AIR) {
|
||||
equipper.getWorld().dropItemNaturally(toEquip.getEntity().getLocation(), trait.get(i));
|
||||
trait.set(i, null);
|
||||
}
|
||||
}
|
||||
Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_ALL_ITEMS_REMOVED, toEquip.getName());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Drop any previous equipment on the ground
|
||||
ItemStack equippedItem = trait.get(slot);
|
||||
if (equippedItem != null && equippedItem.getType() != Material.AIR) {
|
||||
equipper.getWorld().dropItemNaturally(toEquip.getEntity().getLocation(), equippedItem);
|
||||
}
|
||||
|
||||
// Now edit the equipment based on the slot
|
||||
if (type != Material.AIR) {
|
||||
// Set the proper slot with one of the item
|
||||
ItemStack clone = hand.clone();
|
||||
clone.setAmount(1);
|
||||
trait.set(slot, clone);
|
||||
hand.setAmount(hand.getAmount() - 1);
|
||||
equipper.getInventory().setItemInMainHand(hand);
|
||||
}
|
||||
}
|
||||
}
|
15
main/java/net/citizensnpcs/editor/HorseEquipper.java
Normal file
15
main/java/net/citizensnpcs/editor/HorseEquipper.java
Normal file
@ -0,0 +1,15 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
import org.bukkit.entity.Horse;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class HorseEquipper implements Equipper {
|
||||
@Override
|
||||
public void equip(Player equipper, NPC toEquip) {
|
||||
Horse horse = (Horse) toEquip.getEntity();
|
||||
NMS.openHorseScreen(horse, equipper);
|
||||
}
|
||||
}
|
31
main/java/net/citizensnpcs/editor/PigEquipper.java
Normal file
31
main/java/net/citizensnpcs/editor/PigEquipper.java
Normal file
@ -0,0 +1,31 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Pig;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.trait.Saddle;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public class PigEquipper implements Equipper {
|
||||
@Override
|
||||
public void equip(Player equipper, NPC toEquip) {
|
||||
ItemStack hand = equipper.getInventory().getItemInMainHand();
|
||||
Pig pig = (Pig) toEquip.getEntity();
|
||||
if (hand.getType() == Material.SADDLE) {
|
||||
if (!pig.hasSaddle()) {
|
||||
toEquip.getTrait(Saddle.class).toggle();
|
||||
hand.setAmount(0);
|
||||
Messaging.sendTr(equipper, Messages.SADDLED_SET, toEquip.getName());
|
||||
}
|
||||
} else if (pig.hasSaddle()) {
|
||||
equipper.getWorld().dropItemNaturally(pig.getLocation(), new ItemStack(Material.SADDLE, 1));
|
||||
toEquip.getTrait(Saddle.class).toggle();
|
||||
Messaging.sendTr(equipper, Messages.SADDLED_STOPPED, toEquip.getName());
|
||||
}
|
||||
equipper.getInventory().setItemInMainHand(hand);
|
||||
}
|
||||
}
|
40
main/java/net/citizensnpcs/editor/SheepEquipper.java
Normal file
40
main/java/net/citizensnpcs/editor/SheepEquipper.java
Normal file
@ -0,0 +1,40 @@
|
||||
package net.citizensnpcs.editor;
|
||||
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Sheep;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.material.Dye;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.trait.SheepTrait;
|
||||
import net.citizensnpcs.trait.WoolColor;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
public class SheepEquipper implements Equipper {
|
||||
@Override
|
||||
public void equip(Player equipper, NPC toEquip) {
|
||||
ItemStack hand = equipper.getInventory().getItemInMainHand();
|
||||
Sheep sheep = (Sheep) toEquip.getEntity();
|
||||
if (hand.getType() == Material.SHEARS) {
|
||||
Messaging.sendTr(equipper, toEquip.getTrait(SheepTrait.class).toggleSheared() ? Messages.SHEARED_SET
|
||||
: Messages.SHEARED_STOPPED, toEquip.getName());
|
||||
} else if (hand.getType() == Material.INK_SACK) {
|
||||
Dye dye = (Dye) hand.getData();
|
||||
if (sheep.getColor() == dye.getColor())
|
||||
return;
|
||||
DyeColor color = dye.getColor();
|
||||
toEquip.getTrait(WoolColor.class).setColor(color);
|
||||
Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_SHEEP_COLOURED, toEquip.getName(),
|
||||
color.name().toLowerCase().replace("_", " "));
|
||||
|
||||
hand.setAmount(hand.getAmount() - 1);
|
||||
} else {
|
||||
toEquip.getTrait(WoolColor.class).setColor(DyeColor.WHITE);
|
||||
Messaging.sendTr(equipper, Messages.EQUIPMENT_EDITOR_SHEEP_COLOURED, toEquip.getName(), "white");
|
||||
}
|
||||
equipper.getInventory().setItemInMainHand(hand);
|
||||
}
|
||||
}
|
38
main/java/net/citizensnpcs/npc/AbstractEntityController.java
Normal file
38
main/java/net/citizensnpcs/npc/AbstractEntityController.java
Normal file
@ -0,0 +1,38 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
public abstract class AbstractEntityController implements EntityController {
|
||||
private Entity bukkitEntity;
|
||||
|
||||
public AbstractEntityController() {
|
||||
}
|
||||
|
||||
public AbstractEntityController(Class<?> clazz) {
|
||||
NMS.registerEntityClass(clazz);
|
||||
}
|
||||
|
||||
protected abstract Entity createEntity(Location at, NPC npc);
|
||||
|
||||
@Override
|
||||
public Entity getBukkitEntity() {
|
||||
return bukkitEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (bukkitEntity == null)
|
||||
return;
|
||||
bukkitEntity.remove();
|
||||
bukkitEntity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawn(Location at, NPC npc) {
|
||||
bukkitEntity = createEntity(at, npc);
|
||||
}
|
||||
}
|
345
main/java/net/citizensnpcs/npc/CitizensNPC.java
Normal file
345
main/java/net/citizensnpcs/npc/CitizensNPC.java
Normal file
@ -0,0 +1,345 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
import org.bukkit.scoreboard.Team.Option;
|
||||
import org.bukkit.scoreboard.Team.OptionStatus;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import net.citizensnpcs.NPCNeedsRespawnEvent;
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.ai.Navigator;
|
||||
import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.event.NPCDespawnEvent;
|
||||
import net.citizensnpcs.api.event.NPCSpawnEvent;
|
||||
import net.citizensnpcs.api.npc.AbstractNPC;
|
||||
import net.citizensnpcs.api.npc.BlockBreaker;
|
||||
import net.citizensnpcs.api.npc.BlockBreaker.BlockBreakerConfiguration;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.trait.MobType;
|
||||
import net.citizensnpcs.api.trait.trait.Spawned;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.npc.ai.CitizensBlockBreaker;
|
||||
import net.citizensnpcs.npc.ai.CitizensNavigator;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
public class CitizensNPC extends AbstractNPC {
|
||||
private EntityController entityController;
|
||||
private final CitizensNavigator navigator = new CitizensNavigator(this);
|
||||
private int updateCounter = 0;
|
||||
|
||||
public CitizensNPC(UUID uuid, int id, String name, EntityController entityController, NPCRegistry registry) {
|
||||
super(uuid, id, name, registry);
|
||||
Preconditions.checkNotNull(entityController);
|
||||
this.entityController = entityController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean despawn(DespawnReason reason) {
|
||||
if (!isSpawned() && reason != DespawnReason.DEATH) {
|
||||
Messaging.debug("Tried to despawn", getId(), "while already despawned.");
|
||||
if (reason == DespawnReason.REMOVAL) {
|
||||
Bukkit.getPluginManager().callEvent(new NPCDespawnEvent(this, reason));
|
||||
}
|
||||
if (reason == DespawnReason.RELOAD) {
|
||||
unloadEvents();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
NPCDespawnEvent event = new NPCDespawnEvent(this, reason);
|
||||
if (reason == DespawnReason.CHUNK_UNLOAD) {
|
||||
event.setCancelled(Setting.KEEP_CHUNKS_LOADED.asBoolean());
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
if (event.isCancelled()) {
|
||||
getEntity().getLocation().getChunk();
|
||||
Messaging.debug("Couldn't despawn", getId(), "due to despawn event cancellation. Force loaded chunk.",
|
||||
getEntity().isValid());
|
||||
return false;
|
||||
}
|
||||
boolean keepSelected = getTrait(Spawned.class).shouldSpawn();
|
||||
if (!keepSelected) {
|
||||
data().remove("selectors");
|
||||
}
|
||||
navigator.onDespawn();
|
||||
if (reason == DespawnReason.RELOAD) {
|
||||
unloadEvents();
|
||||
}
|
||||
for (Trait trait : new ArrayList<Trait>(traits.values())) {
|
||||
trait.onDespawn();
|
||||
}
|
||||
Messaging.debug("Despawned", getId(), "DespawnReason.", reason);
|
||||
entityController.remove();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void faceLocation(Location location) {
|
||||
if (!isSpawned())
|
||||
return;
|
||||
Util.faceLocation(getEntity(), location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockBreaker getBlockBreaker(Block targetBlock, BlockBreakerConfiguration config) {
|
||||
return new CitizensBlockBreaker(getEntity(), targetBlock, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getEntity() {
|
||||
return entityController == null ? null : entityController.getBukkitEntity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Navigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getStoredLocation() {
|
||||
return isSpawned() ? getEntity().getLocation() : getTrait(CurrentLocation.class).getLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFlyable() {
|
||||
updateFlyableState();
|
||||
return super.isFlyable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(final DataKey root) {
|
||||
super.load(root);
|
||||
|
||||
// Spawn the NPC
|
||||
CurrentLocation spawnLocation = getTrait(CurrentLocation.class);
|
||||
if (getTrait(Spawned.class).shouldSpawn() && spawnLocation.getLocation() != null) {
|
||||
spawn(spawnLocation.getLocation());
|
||||
}
|
||||
if (getTrait(Spawned.class).shouldSpawn() && spawnLocation.getLocation() == null) {
|
||||
Messaging.debug("Tried to spawn", getId(), "on load but world was null");
|
||||
}
|
||||
|
||||
navigator.load(root.getRelative("navigator"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey root) {
|
||||
super.save(root);
|
||||
if (!data().get(NPC.SHOULD_SAVE_METADATA, true))
|
||||
return;
|
||||
navigator.save(root.getRelative("navigator"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBukkitEntityType(EntityType type) {
|
||||
EntityController controller = EntityControllers.createForType(type);
|
||||
if (controller == null)
|
||||
throw new IllegalArgumentException("Unsupported entity type " + type);
|
||||
setEntityController(controller);
|
||||
}
|
||||
|
||||
public void setEntityController(EntityController newController) {
|
||||
Preconditions.checkNotNull(newController);
|
||||
boolean wasSpawned = isSpawned();
|
||||
Location prev = null;
|
||||
if (wasSpawned) {
|
||||
prev = getEntity().getLocation();
|
||||
despawn(DespawnReason.PENDING_RESPAWN);
|
||||
}
|
||||
entityController = newController;
|
||||
if (wasSpawned) {
|
||||
spawn(prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFlyable(boolean flyable) {
|
||||
super.setFlyable(flyable);
|
||||
updateFlyableState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawn(Location at) {
|
||||
Preconditions.checkNotNull(at, "location cannot be null");
|
||||
if (isSpawned()) {
|
||||
Messaging.debug("Tried to spawn", getId(), "while already spawned.");
|
||||
return false;
|
||||
}
|
||||
data().get(NPC.DEFAULT_PROTECTED_METADATA, true);
|
||||
|
||||
at = at.clone();
|
||||
getTrait(CurrentLocation.class).setLocation(at);
|
||||
|
||||
entityController.spawn(at, this);
|
||||
|
||||
getEntity().setMetadata(NPC_METADATA_MARKER, new FixedMetadataValue(CitizensAPI.getPlugin(), true));
|
||||
|
||||
boolean couldSpawn = !Util.isLoaded(at) ? false : NMS.addEntityToWorld(getEntity(), SpawnReason.CUSTOM);
|
||||
|
||||
// send skin packets, if applicable, before other NMS packets are sent
|
||||
if (couldSpawn) {
|
||||
SkinnableEntity skinnable = getEntity() instanceof SkinnableEntity ? ((SkinnableEntity) getEntity()) : null;
|
||||
if (skinnable != null) {
|
||||
skinnable.getSkinTracker().onSpawnNPC();
|
||||
}
|
||||
}
|
||||
|
||||
getEntity().teleport(at);
|
||||
|
||||
if (!couldSpawn) {
|
||||
Messaging.debug("Retrying spawn of", getId(), "later due to chunk being unloaded.",
|
||||
Util.isLoaded(at) ? "Util.isLoaded true" : "Util.isLoaded false");
|
||||
// we need to wait for a chunk load before trying to spawn
|
||||
entityController.remove();
|
||||
Bukkit.getPluginManager().callEvent(new NPCNeedsRespawnEvent(this, at));
|
||||
return false;
|
||||
}
|
||||
|
||||
NMS.setHeadYaw(getEntity(), at.getYaw());
|
||||
|
||||
// Set the spawned state
|
||||
getTrait(CurrentLocation.class).setLocation(at);
|
||||
getTrait(Spawned.class).setSpawned(true);
|
||||
|
||||
NPCSpawnEvent spawnEvent = new NPCSpawnEvent(this, at);
|
||||
Bukkit.getPluginManager().callEvent(spawnEvent);
|
||||
|
||||
if (spawnEvent.isCancelled()) {
|
||||
entityController.remove();
|
||||
Messaging.debug("Couldn't spawn", getId(), "due to event cancellation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
navigator.onSpawn();
|
||||
|
||||
// Modify NPC using traits after the entity has been created
|
||||
Collection<Trait> onSpawn = traits.values();
|
||||
|
||||
// work around traits modifying the map during this iteration.
|
||||
for (Trait trait : onSpawn.toArray(new Trait[onSpawn.size()])) {
|
||||
try {
|
||||
trait.onSpawn();
|
||||
} catch (Throwable ex) {
|
||||
Messaging.severeTr(Messages.TRAIT_ONSPAWN_FAILED, trait.getName(), getId());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (getEntity() instanceof LivingEntity) {
|
||||
LivingEntity entity = (LivingEntity) getEntity();
|
||||
entity.setRemoveWhenFarAway(false);
|
||||
|
||||
if (NMS.getStepHeight(entity) < 1) {
|
||||
NMS.setStepHeight(entity, 1);
|
||||
}
|
||||
if (getEntity() instanceof Player) {
|
||||
NMS.replaceTrackerEntry((Player) getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
try {
|
||||
super.update();
|
||||
if (!isSpawned())
|
||||
return;
|
||||
if (data().get(NPC.SWIMMING_METADATA, true)) {
|
||||
NMS.trySwim(getEntity());
|
||||
}
|
||||
navigator.run();
|
||||
|
||||
getEntity().setGlowing(data().get(NPC.GLOWING_METADATA, false));
|
||||
if (!getNavigator().isNavigating() && updateCounter++ > Setting.PACKET_UPDATE_DELAY.asInt()) {
|
||||
updateCounter = 0;
|
||||
if (getEntity() instanceof LivingEntity) {
|
||||
OptionStatus nameVisibility = OptionStatus.NEVER;
|
||||
if (!getEntity().isCustomNameVisible()) {
|
||||
getEntity().setCustomName("");
|
||||
} else {
|
||||
nameVisibility = OptionStatus.ALWAYS;
|
||||
getEntity().setCustomName(getFullName());
|
||||
}
|
||||
String teamName = data().get(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA, "");
|
||||
if (getEntity() instanceof Player
|
||||
&& Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName) != null) {
|
||||
Team team = Bukkit.getScoreboardManager().getMainScoreboard().getTeam(teamName);
|
||||
if (!Setting.USE_SCOREBOARD_TEAMS.asBoolean()) {
|
||||
team.unregister();
|
||||
data().remove(NPC.SCOREBOARD_FAKE_TEAM_NAME_METADATA);
|
||||
} else {
|
||||
team.setOption(Option.NAME_TAG_VISIBILITY, nameVisibility);
|
||||
if (data().has(NPC.GLOWING_COLOR_METADATA)) {
|
||||
if (team.getPrefix() == null || team.getPrefix().length() == 0
|
||||
|| (data().has("previous-glowing-color")
|
||||
&& !team.getPrefix().equals(data().get("previous-glowing-color")))) {
|
||||
team.setPrefix(ChatColor.valueOf(data().<String> get(NPC.GLOWING_COLOR_METADATA))
|
||||
.toString());
|
||||
data().set("previous-glowing-color", team.getPrefix());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Player player = getEntity() instanceof Player ? (Player) getEntity() : null;
|
||||
NMS.sendPositionUpdate(player, getEntity(), getStoredLocation());
|
||||
}
|
||||
|
||||
if (getEntity() instanceof LivingEntity) {
|
||||
boolean nameplateVisible = data().get(NPC.NAMEPLATE_VISIBLE_METADATA, true);
|
||||
((LivingEntity) getEntity()).setCustomNameVisible(nameplateVisible);
|
||||
|
||||
if (data().get(NPC.DEFAULT_PROTECTED_METADATA, true)) {
|
||||
NMS.setKnockbackResistance((LivingEntity) getEntity(), 1D);
|
||||
} else {
|
||||
NMS.setKnockbackResistance((LivingEntity) getEntity(), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
if (data().has(NPC.SILENT_METADATA)) {
|
||||
getEntity().setSilent(Boolean.parseBoolean(data().get(NPC.SILENT_METADATA).toString()));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Throwable error = Throwables.getRootCause(ex);
|
||||
Messaging.logTr(Messages.EXCEPTION_UPDATING_NPC, getId(), error.getMessage());
|
||||
error.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFlyableState() {
|
||||
EntityType type = isSpawned() ? getEntity().getType() : getTrait(MobType.class).getType();
|
||||
if (type == null)
|
||||
return;
|
||||
if (Util.isAlwaysFlyable(type)) {
|
||||
data().setPersistent(NPC.FLYABLE_METADATA, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String NPC_METADATA_MARKER = "NPC";
|
||||
}
|
251
main/java/net/citizensnpcs/npc/CitizensNPCRegistry.java
Normal file
251
main/java/net/citizensnpcs/npc/CitizensNPCRegistry.java
Normal file
@ -0,0 +1,251 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import gnu.trove.map.hash.TIntObjectHashMap;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.event.NPCCreateEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCDataStore;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.npc.ai.NPCHolder;
|
||||
import net.citizensnpcs.trait.ArmorStandTrait;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
public class CitizensNPCRegistry implements NPCRegistry {
|
||||
private final NPCCollection npcs = TROVE_EXISTS ? new TroveNPCCollection() : new MapNPCCollection();
|
||||
private final NPCDataStore saves;
|
||||
|
||||
public CitizensNPCRegistry(NPCDataStore store) {
|
||||
saves = store;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC createNPC(EntityType type, String name) {
|
||||
return createNPC(type, UUID.randomUUID(), generateUniqueId(), name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC createNPC(EntityType type, UUID uuid, int id, String name) {
|
||||
Preconditions.checkNotNull(name, "name cannot be null");
|
||||
Preconditions.checkNotNull(type, "type cannot be null");
|
||||
CitizensNPC npc = getByType(type, uuid, id, name);
|
||||
|
||||
if (npc == null)
|
||||
throw new IllegalStateException("Could not create NPC.");
|
||||
npcs.put(npc.getId(), npc);
|
||||
Bukkit.getPluginManager().callEvent(new NPCCreateEvent(npc));
|
||||
if (type == EntityType.ARMOR_STAND && !npc.hasTrait(ArmorStandTrait.class)) {
|
||||
npc.addTrait(ArmorStandTrait.class);
|
||||
}
|
||||
return npc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregister(NPC npc) {
|
||||
npcs.remove(npc);
|
||||
if (saves != null) {
|
||||
saves.clearData(npc);
|
||||
}
|
||||
npc.despawn(DespawnReason.REMOVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterAll() {
|
||||
Iterator<NPC> itr = iterator();
|
||||
while (itr.hasNext()) {
|
||||
NPC npc = itr.next();
|
||||
itr.remove();
|
||||
npc.despawn(DespawnReason.REMOVAL);
|
||||
for (Trait t : npc.getTraits()) {
|
||||
t.onRemove();
|
||||
}
|
||||
if (saves != null) {
|
||||
saves.clearData(npc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int generateUniqueId() {
|
||||
return saves.createUniqueNPCId(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getById(int id) {
|
||||
if (id < 0)
|
||||
throw new IllegalArgumentException("invalid id");
|
||||
return npcs.get(id);
|
||||
}
|
||||
|
||||
private CitizensNPC getByType(EntityType type, UUID uuid, int id, String name) {
|
||||
return new CitizensNPC(uuid, id, name, EntityControllers.createForType(type), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getByUniqueId(UUID uuid) {
|
||||
return npcs.get(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getByUniqueIdGlobal(UUID uuid) {
|
||||
NPC npc = getByUniqueId(uuid);
|
||||
if (npc != null)
|
||||
return npc;
|
||||
for (NPCRegistry registry : CitizensAPI.getNPCRegistries()) {
|
||||
if (registry != this) {
|
||||
NPC other = registry.getByUniqueId(uuid);
|
||||
if (other != null) {
|
||||
return other;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getNPC(Entity entity) {
|
||||
if (entity == null)
|
||||
return null;
|
||||
if (entity instanceof NPCHolder)
|
||||
return ((NPCHolder) entity).getNPC();
|
||||
return NMS.getNPC(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNPC(Entity entity) {
|
||||
return getNPC(entity) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<NPC> iterator() {
|
||||
return npcs.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<NPC> sorted() {
|
||||
return npcs.sorted();
|
||||
}
|
||||
|
||||
public static class MapNPCCollection implements NPCCollection {
|
||||
private final Map<Integer, NPC> npcs = Maps.newHashMap();
|
||||
private final Map<UUID, NPC> uniqueNPCs = Maps.newHashMap();
|
||||
|
||||
@Override
|
||||
public NPC get(int id) {
|
||||
return npcs.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC get(UUID uuid) {
|
||||
return uniqueNPCs.get(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<NPC> iterator() {
|
||||
return npcs.values().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(int id, NPC npc) {
|
||||
npcs.put(id, npc);
|
||||
uniqueNPCs.put(npc.getUniqueId(), npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(NPC npc) {
|
||||
npcs.remove(npc.getId());
|
||||
uniqueNPCs.remove(npc.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<NPC> sorted() {
|
||||
List<NPC> vals = new ArrayList<NPC>(npcs.values());
|
||||
Collections.sort(vals, NPC_COMPARATOR);
|
||||
return vals;
|
||||
}
|
||||
}
|
||||
|
||||
public static interface NPCCollection extends Iterable<NPC> {
|
||||
public NPC get(int id);
|
||||
|
||||
public NPC get(UUID uuid);
|
||||
|
||||
public void put(int id, NPC npc);
|
||||
|
||||
public void remove(NPC npc);
|
||||
|
||||
public Iterable<NPC> sorted();
|
||||
}
|
||||
|
||||
public static class TroveNPCCollection implements NPCCollection {
|
||||
private final TIntObjectHashMap<NPC> npcs = new TIntObjectHashMap<NPC>();
|
||||
private final Map<UUID, NPC> uniqueNPCs = Maps.newHashMap();
|
||||
|
||||
@Override
|
||||
public NPC get(int id) {
|
||||
return npcs.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC get(UUID uuid) {
|
||||
return uniqueNPCs.get(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<NPC> iterator() {
|
||||
return npcs.valueCollection().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(int id, NPC npc) {
|
||||
npcs.put(id, npc);
|
||||
uniqueNPCs.put(npc.getUniqueId(), npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(NPC npc) {
|
||||
npcs.remove(npc.getId());
|
||||
uniqueNPCs.remove(npc.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<NPC> sorted() {
|
||||
List<NPC> vals = new ArrayList<NPC>(npcs.valueCollection());
|
||||
Collections.sort(vals, NPC_COMPARATOR);
|
||||
return vals;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<NPC> NPC_COMPARATOR = new Comparator<NPC>() {
|
||||
@Override
|
||||
public int compare(NPC o1, NPC o2) {
|
||||
return o1.getId() - o2.getId();
|
||||
}
|
||||
};
|
||||
|
||||
private static boolean TROVE_EXISTS = false;
|
||||
|
||||
static {
|
||||
// allow trove dependency to be optional for debugging purposes
|
||||
try {
|
||||
Class.forName("gnu.trove.map.hash.TIntObjectHashMap").newInstance();
|
||||
TROVE_EXISTS = true;
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
177
main/java/net/citizensnpcs/npc/CitizensTraitFactory.java
Normal file
177
main/java/net/citizensnpcs/npc/CitizensTraitFactory.java
Normal file
@ -0,0 +1,177 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import net.citizensnpcs.Metrics;
|
||||
import net.citizensnpcs.Metrics.Graph;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitFactory;
|
||||
import net.citizensnpcs.api.trait.TraitInfo;
|
||||
import net.citizensnpcs.api.trait.trait.Equipment;
|
||||
import net.citizensnpcs.api.trait.trait.Inventory;
|
||||
import net.citizensnpcs.api.trait.trait.MobType;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.api.trait.trait.Spawned;
|
||||
import net.citizensnpcs.api.trait.trait.Speech;
|
||||
import net.citizensnpcs.trait.Age;
|
||||
import net.citizensnpcs.trait.Anchors;
|
||||
import net.citizensnpcs.trait.ArmorStandTrait;
|
||||
import net.citizensnpcs.trait.BossBarTrait;
|
||||
import net.citizensnpcs.trait.Controllable;
|
||||
import net.citizensnpcs.trait.CurrentLocation;
|
||||
import net.citizensnpcs.trait.Gravity;
|
||||
import net.citizensnpcs.trait.HorseModifiers;
|
||||
import net.citizensnpcs.trait.LookClose;
|
||||
import net.citizensnpcs.trait.MountTrait;
|
||||
import net.citizensnpcs.trait.NPCSkeletonType;
|
||||
import net.citizensnpcs.trait.OcelotModifiers;
|
||||
import net.citizensnpcs.trait.Poses;
|
||||
import net.citizensnpcs.trait.Powered;
|
||||
import net.citizensnpcs.trait.RabbitType;
|
||||
import net.citizensnpcs.trait.Saddle;
|
||||
import net.citizensnpcs.trait.ScriptTrait;
|
||||
import net.citizensnpcs.trait.SheepTrait;
|
||||
import net.citizensnpcs.trait.SkinLayers;
|
||||
import net.citizensnpcs.trait.SlimeSize;
|
||||
import net.citizensnpcs.trait.VillagerProfession;
|
||||
import net.citizensnpcs.trait.WitherTrait;
|
||||
import net.citizensnpcs.trait.WolfModifiers;
|
||||
import net.citizensnpcs.trait.WoolColor;
|
||||
import net.citizensnpcs.trait.ZombieModifier;
|
||||
import net.citizensnpcs.trait.text.Text;
|
||||
import net.citizensnpcs.trait.waypoint.Waypoints;
|
||||
|
||||
public class CitizensTraitFactory implements TraitFactory {
|
||||
private final List<TraitInfo> defaultTraits = Lists.newArrayList();
|
||||
private final Map<String, TraitInfo> registered = Maps.newHashMap();
|
||||
|
||||
public CitizensTraitFactory() {
|
||||
registerTrait(TraitInfo.create(Age.class));
|
||||
registerTrait(TraitInfo.create(ArmorStandTrait.class));
|
||||
registerTrait(TraitInfo.create(Anchors.class));
|
||||
registerTrait(TraitInfo.create(BossBarTrait.class));
|
||||
registerTrait(TraitInfo.create(Controllable.class));
|
||||
registerTrait(TraitInfo.create(Equipment.class));
|
||||
registerTrait(TraitInfo.create(Gravity.class));
|
||||
registerTrait(TraitInfo.create(HorseModifiers.class));
|
||||
registerTrait(TraitInfo.create(Inventory.class));
|
||||
registerTrait(TraitInfo.create(CurrentLocation.class));
|
||||
registerTrait(TraitInfo.create(LookClose.class));
|
||||
registerTrait(TraitInfo.create(OcelotModifiers.class));
|
||||
registerTrait(TraitInfo.create(Owner.class));
|
||||
registerTrait(TraitInfo.create(Poses.class));
|
||||
registerTrait(TraitInfo.create(Powered.class));
|
||||
registerTrait(TraitInfo.create(RabbitType.class));
|
||||
registerTrait(TraitInfo.create(Saddle.class));
|
||||
registerTrait(TraitInfo.create(ScriptTrait.class));
|
||||
registerTrait(TraitInfo.create(SheepTrait.class));
|
||||
registerTrait(TraitInfo.create(SkinLayers.class));
|
||||
registerTrait(TraitInfo.create(MountTrait.class));
|
||||
registerTrait(TraitInfo.create(NPCSkeletonType.class));
|
||||
registerTrait(TraitInfo.create(SlimeSize.class));
|
||||
registerTrait(TraitInfo.create(Spawned.class));
|
||||
registerTrait(TraitInfo.create(Speech.class));
|
||||
registerTrait(TraitInfo.create(Text.class));
|
||||
registerTrait(TraitInfo.create(MobType.class).asDefaultTrait());
|
||||
registerTrait(TraitInfo.create(Waypoints.class));
|
||||
registerTrait(TraitInfo.create(WitherTrait.class));
|
||||
registerTrait(TraitInfo.create(WoolColor.class));
|
||||
registerTrait(TraitInfo.create(WolfModifiers.class));
|
||||
registerTrait(TraitInfo.create(VillagerProfession.class));
|
||||
registerTrait(TraitInfo.create(ZombieModifier.class));
|
||||
|
||||
for (String trait : registered.keySet()) {
|
||||
INTERNAL_TRAITS.add(trait);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDefaultTraits(NPC npc) {
|
||||
for (TraitInfo info : defaultTraits) {
|
||||
npc.addTrait(create(info));
|
||||
}
|
||||
}
|
||||
|
||||
public void addPlotters(Graph graph) {
|
||||
for (Map.Entry<String, TraitInfo> entry : registered.entrySet()) {
|
||||
if (INTERNAL_TRAITS.contains(entry.getKey()))
|
||||
continue;
|
||||
final Class<? extends Trait> traitClass = entry.getValue().getTraitClass();
|
||||
graph.addPlotter(new Metrics.Plotter(entry.getKey()) {
|
||||
@Override
|
||||
public int getValue() {
|
||||
int numberUsingTrait = 0;
|
||||
for (NPC npc : CitizensAPI.getNPCRegistry()) {
|
||||
if (npc.hasTrait(traitClass))
|
||||
++numberUsingTrait;
|
||||
}
|
||||
return numberUsingTrait;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Trait> T create(TraitInfo info) {
|
||||
return info.tryCreateInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deregisterTrait(TraitInfo info) {
|
||||
Preconditions.checkNotNull(info, "info cannot be null");
|
||||
registered.remove(info.getTraitName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Trait> T getTrait(Class<T> clazz) {
|
||||
for (TraitInfo entry : registered.values()) {
|
||||
if (clazz == entry.getTraitClass()) {
|
||||
return create(entry);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Trait> T getTrait(String name) {
|
||||
TraitInfo info = registered.get(name.toLowerCase());
|
||||
if (info == null)
|
||||
return null;
|
||||
return (T) create(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Trait> getTraitClass(String name) {
|
||||
TraitInfo info = registered.get(name.toLowerCase());
|
||||
return info == null ? null : info.getTraitClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternalTrait(Trait trait) {
|
||||
return INTERNAL_TRAITS.contains(trait.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTrait(TraitInfo info) {
|
||||
Preconditions.checkNotNull(info, "info cannot be null");
|
||||
if (registered.containsKey(info.getTraitName())) {
|
||||
System.out.println(info.getTraitClass());
|
||||
throw new IllegalArgumentException("trait name already registered");
|
||||
}
|
||||
registered.put(info.getTraitName(), info);
|
||||
if (info.isDefaultTrait()) {
|
||||
defaultTraits.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Set<String> INTERNAL_TRAITS = Sets.newHashSet();
|
||||
}
|
14
main/java/net/citizensnpcs/npc/EntityController.java
Normal file
14
main/java/net/citizensnpcs/npc/EntityController.java
Normal file
@ -0,0 +1,14 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
public interface EntityController {
|
||||
Entity getBukkitEntity();
|
||||
|
||||
void remove();
|
||||
|
||||
void spawn(Location at, NPC npc);
|
||||
}
|
32
main/java/net/citizensnpcs/npc/EntityControllers.java
Normal file
32
main/java/net/citizensnpcs/npc/EntityControllers.java
Normal file
@ -0,0 +1,32 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
public class EntityControllers {
|
||||
public static boolean controllerExistsForType(EntityType type) {
|
||||
return TYPES.containsKey(type);
|
||||
}
|
||||
|
||||
public static EntityController createForType(EntityType type) {
|
||||
Class<? extends EntityController> controllerClass = TYPES.get(type);
|
||||
if (controllerClass == null)
|
||||
throw new IllegalArgumentException("Unknown EntityType: " + type);
|
||||
try {
|
||||
return controllerClass.newInstance();
|
||||
} catch (Throwable ex) {
|
||||
Throwables.getRootCause(ex).printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setEntityControllerForType(EntityType type, Class<? extends EntityController> controller) {
|
||||
TYPES.put(type, controller);
|
||||
}
|
||||
|
||||
private static final Map<EntityType, Class<? extends EntityController>> TYPES = Maps.newEnumMap(EntityType.class);
|
||||
}
|
148
main/java/net/citizensnpcs/npc/NPCSelector.java
Normal file
148
main/java/net/citizensnpcs/npc/NPCSelector.java
Normal file
@ -0,0 +1,148 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.event.NPCRemoveEvent;
|
||||
import net.citizensnpcs.api.event.NPCRightClickEvent;
|
||||
import net.citizensnpcs.api.event.NPCSelectEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.metadata.FixedMetadataValue;
|
||||
import org.bukkit.metadata.MetadataValue;
|
||||
import org.bukkit.metadata.Metadatable;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
public class NPCSelector implements Listener, net.citizensnpcs.api.npc.NPCSelector {
|
||||
private UUID consoleSelectedNPC;
|
||||
private final Plugin plugin;
|
||||
|
||||
public NPCSelector(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getSelected(CommandSender sender) {
|
||||
if (sender instanceof Player) {
|
||||
return getSelectedFromMetadatable((Player) sender);
|
||||
} else if (sender instanceof BlockCommandSender) {
|
||||
return getSelectedFromMetadatable(((BlockCommandSender) sender).getBlock());
|
||||
} else if (sender instanceof ConsoleCommandSender) {
|
||||
if (consoleSelectedNPC == null)
|
||||
return null;
|
||||
return CitizensAPI.getNPCRegistry().getByUniqueIdGlobal(consoleSelectedNPC);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private NPC getSelectedFromMetadatable(Metadatable sender) {
|
||||
List<MetadataValue> metadata = sender.getMetadata("selected");
|
||||
if (metadata.size() == 0)
|
||||
return null;
|
||||
return CitizensAPI.getNPCRegistry().getByUniqueIdGlobal((UUID) metadata.get(0).value());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onNPCRemove(NPCRemoveEvent event) {
|
||||
NPC npc = event.getNPC();
|
||||
List<String> selectors = npc.data().get("selectors");
|
||||
if (selectors == null)
|
||||
return;
|
||||
for (String value : selectors) {
|
||||
if (value.equals("console")) {
|
||||
consoleSelectedNPC = null;
|
||||
} else if (value.startsWith("@")) {
|
||||
String[] parts = value.substring(1, value.length()).split(":");
|
||||
World world = Bukkit.getWorld(parts[0]);
|
||||
if (world != null) {
|
||||
Block block = world.getBlockAt(Integer.parseInt(parts[1]), Integer.parseInt(parts[2]),
|
||||
Integer.parseInt(parts[3]));
|
||||
removeMetadata(block);
|
||||
}
|
||||
} else {
|
||||
Player search = Bukkit.getPlayerExact(value);
|
||||
removeMetadata(search);
|
||||
}
|
||||
}
|
||||
npc.data().remove("selectors");
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onNPCRightClick(NPCRightClickEvent event) {
|
||||
Player player = event.getClicker();
|
||||
NPC npc = event.getNPC();
|
||||
List<MetadataValue> selected = player.getMetadata("selected");
|
||||
if (selected == null || selected.size() == 0 || selected.get(0).asInt() != npc.getId()) {
|
||||
if (Util.matchesItemInHand(player, Setting.SELECTION_ITEM.asString())
|
||||
&& npc.getTrait(Owner.class).isOwnedBy(player)) {
|
||||
player.removeMetadata("selected", plugin);
|
||||
select(player, npc);
|
||||
Messaging.sendWithNPC(player, Setting.SELECTION_MESSAGE.asString(), npc);
|
||||
if (!Setting.QUICK_SELECT.asBoolean())
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMetadata(Metadatable metadatable) {
|
||||
if (metadatable != null) {
|
||||
metadatable.removeMetadata("selected", plugin);
|
||||
}
|
||||
}
|
||||
|
||||
public void select(CommandSender sender, NPC npc) {
|
||||
// Remove existing selection if any
|
||||
List<String> selectors = npc.data().get("selectors");
|
||||
if (selectors == null) {
|
||||
selectors = Lists.newArrayList();
|
||||
npc.data().set("selectors", selectors);
|
||||
}
|
||||
if (sender instanceof Player) {
|
||||
Player player = (Player) sender;
|
||||
setMetadata(npc, player);
|
||||
selectors.add(sender.getName());
|
||||
|
||||
// Remove editor if the player has one
|
||||
Editor.leave(player);
|
||||
} else if (sender instanceof BlockCommandSender) {
|
||||
Block block = ((BlockCommandSender) sender).getBlock();
|
||||
setMetadata(npc, block);
|
||||
selectors.add(toName(block));
|
||||
} else if (sender instanceof ConsoleCommandSender) {
|
||||
consoleSelectedNPC = npc.getUniqueId();
|
||||
selectors.add("console");
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().callEvent(new NPCSelectEvent(npc, sender));
|
||||
}
|
||||
|
||||
private void setMetadata(NPC npc, Metadatable metadatable) {
|
||||
if (metadatable.hasMetadata("selected")) {
|
||||
metadatable.removeMetadata("selected", plugin);
|
||||
}
|
||||
metadatable.setMetadata("selected", new FixedMetadataValue(plugin, npc.getUniqueId()));
|
||||
}
|
||||
|
||||
private String toName(Block block) {
|
||||
return '@' + block.getWorld().getName() + ":" + Integer.toString(block.getX()) + ":"
|
||||
+ Integer.toString(block.getY()) + ":" + Integer.toString(block.getZ());
|
||||
}
|
||||
}
|
138
main/java/net/citizensnpcs/npc/Template.java
Normal file
138
main/java/net/citizensnpcs/npc/Template.java
Normal file
@ -0,0 +1,138 @@
|
||||
package net.citizensnpcs.npc;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.MemoryDataKey;
|
||||
import net.citizensnpcs.api.util.YamlStorage;
|
||||
import net.citizensnpcs.api.util.YamlStorage.YamlKey;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
public class Template {
|
||||
private final String name;
|
||||
private final boolean override;
|
||||
private final Map<String, Object> replacements;
|
||||
|
||||
private Template(String name, Map<String, Object> replacements, boolean override) {
|
||||
this.replacements = replacements;
|
||||
this.override = override;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void apply(NPC npc) {
|
||||
MemoryDataKey memoryKey = new MemoryDataKey();
|
||||
npc.save(memoryKey);
|
||||
List<Node> queue = Lists.newArrayList(new Node("", replacements));
|
||||
for (int i = 0; i < queue.size(); i++) {
|
||||
Node node = queue.get(i);
|
||||
for (Entry<String, Object> entry : node.map.entrySet()) {
|
||||
String fullKey = node.headKey.isEmpty() ? entry.getKey() : node.headKey + '.' + entry.getKey();
|
||||
if (entry.getValue() instanceof Map<?, ?>) {
|
||||
queue.add(new Node(fullKey, (Map<String, Object>) entry.getValue()));
|
||||
continue;
|
||||
}
|
||||
boolean overwrite = memoryKey.keyExists(fullKey) | override;
|
||||
if (!overwrite || fullKey.equals("uuid"))
|
||||
continue;
|
||||
memoryKey.setRaw(fullKey, entry.getValue());
|
||||
}
|
||||
}
|
||||
npc.load(memoryKey);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
templates.load();
|
||||
templates.getKey("").removeKey(name);
|
||||
templates.save();
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static class Node {
|
||||
String headKey;
|
||||
Map<String, Object> map;
|
||||
|
||||
private Node(String headKey, Map<String, Object> map) {
|
||||
this.headKey = headKey;
|
||||
this.map = map;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TemplateBuilder {
|
||||
private final String name;
|
||||
private boolean override;
|
||||
private final Map<String, Object> replacements = Maps.newHashMap();
|
||||
|
||||
private TemplateBuilder(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Template buildAndSave() {
|
||||
save();
|
||||
return new Template(name, replacements, override);
|
||||
}
|
||||
|
||||
public TemplateBuilder from(NPC npc) {
|
||||
replacements.clear();
|
||||
MemoryDataKey key = new MemoryDataKey();
|
||||
((CitizensNPC) npc).save(key);
|
||||
replacements.putAll(key.getValuesDeep());
|
||||
return this;
|
||||
}
|
||||
|
||||
public TemplateBuilder override(boolean override) {
|
||||
this.override = override;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void save() {
|
||||
templates.load();
|
||||
DataKey root = templates.getKey(name);
|
||||
root.setBoolean("override", override);
|
||||
root.setRaw("replacements", replacements);
|
||||
templates.save();
|
||||
}
|
||||
|
||||
public static TemplateBuilder create(String name) {
|
||||
return new TemplateBuilder(name);
|
||||
}
|
||||
}
|
||||
|
||||
public static Iterable<Template> allTemplates() {
|
||||
templates.load();
|
||||
return Iterables.transform(templates.getKey("").getSubKeys(), new Function<DataKey, Template>() {
|
||||
@Override
|
||||
public Template apply(DataKey arg0) {
|
||||
return Template.byName(arg0.name());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Template byName(String name) {
|
||||
templates.load();
|
||||
if (!templates.getKey("").keyExists(name))
|
||||
return null;
|
||||
YamlKey key = templates.getKey(name);
|
||||
boolean override = key.getBoolean("override", false);
|
||||
Map<String, Object> replacements = key.getRelative("replacements").getValuesDeep();
|
||||
return new Template(name, replacements, override);
|
||||
}
|
||||
|
||||
private static YamlStorage templates = new YamlStorage(new File(CitizensAPI.getDataFolder(), "templates.yml"));
|
||||
|
||||
static {
|
||||
templates.load();
|
||||
}
|
||||
}
|
120
main/java/net/citizensnpcs/npc/ai/AStarNavigationStrategy.java
Normal file
120
main/java/net/citizensnpcs/npc/ai/AStarNavigationStrategy.java
Normal file
@ -0,0 +1,120 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Effect;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.ai.AbstractPathStrategy;
|
||||
import net.citizensnpcs.api.ai.NavigatorParameters;
|
||||
import net.citizensnpcs.api.ai.TargetType;
|
||||
import net.citizensnpcs.api.ai.event.CancelReason;
|
||||
import net.citizensnpcs.api.astar.AStarMachine;
|
||||
import net.citizensnpcs.api.astar.pathfinder.ChunkBlockSource;
|
||||
import net.citizensnpcs.api.astar.pathfinder.Path;
|
||||
import net.citizensnpcs.api.astar.pathfinder.VectorGoal;
|
||||
import net.citizensnpcs.api.astar.pathfinder.VectorNode;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
public class AStarNavigationStrategy extends AbstractPathStrategy {
|
||||
private final Location destination;
|
||||
private final NPC npc;
|
||||
private final NavigatorParameters params;
|
||||
private Path plan;
|
||||
private boolean planned = false;
|
||||
private Vector vector;
|
||||
|
||||
public AStarNavigationStrategy(NPC npc, Iterable<Vector> path, NavigatorParameters params) {
|
||||
super(TargetType.LOCATION);
|
||||
List<Vector> list = Lists.newArrayList(path);
|
||||
this.params = params;
|
||||
this.destination = list.get(list.size() - 1).toLocation(npc.getStoredLocation().getWorld());
|
||||
this.npc = npc;
|
||||
setPlan(new Path(list));
|
||||
}
|
||||
|
||||
public AStarNavigationStrategy(NPC npc, Location dest, NavigatorParameters params) {
|
||||
super(TargetType.LOCATION);
|
||||
this.params = params;
|
||||
this.destination = dest;
|
||||
this.npc = npc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Vector> getPath() {
|
||||
return plan == null ? null : plan.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getTargetAsLocation() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setPlan(Path path) {
|
||||
this.plan = path;
|
||||
this.planned = true;
|
||||
if (plan == null || plan.isComplete()) {
|
||||
setCancelReason(CancelReason.STUCK);
|
||||
} else {
|
||||
vector = plan.getCurrentVector();
|
||||
if (Setting.DEBUG_PATHFINDING.asBoolean()) {
|
||||
plan.debug();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (plan != null && Setting.DEBUG_PATHFINDING.asBoolean()) {
|
||||
plan.debugEnd();
|
||||
}
|
||||
plan = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update() {
|
||||
if (!planned) {
|
||||
Location location = npc.getEntity().getLocation();
|
||||
VectorGoal goal = new VectorGoal(destination, (float) params.pathDistanceMargin());
|
||||
setPlan(ASTAR.runFully(goal,
|
||||
new VectorNode(goal, location, new ChunkBlockSource(location, params.range()), params.examiners()),
|
||||
50000));
|
||||
}
|
||||
if (getCancelReason() != null || plan == null || plan.isComplete()) {
|
||||
return true;
|
||||
}
|
||||
Location currLoc = npc.getEntity().getLocation(NPC_LOCATION);
|
||||
if (currLoc.toVector().distanceSquared(vector) <= params.distanceMargin()) {
|
||||
plan.update(npc);
|
||||
if (plan.isComplete()) {
|
||||
return true;
|
||||
}
|
||||
vector = plan.getCurrentVector();
|
||||
}
|
||||
double dX = vector.getBlockX() - currLoc.getX();
|
||||
double dZ = vector.getBlockZ() - currLoc.getZ();
|
||||
double dY = vector.getY() - currLoc.getY();
|
||||
double xzDistance = dX * dX + dZ * dZ;
|
||||
double distance = xzDistance + dY * dY;
|
||||
if (Setting.DEBUG_PATHFINDING.asBoolean()) {
|
||||
npc.getEntity().getWorld().playEffect(vector.toLocation(npc.getEntity().getWorld()), Effect.ENDER_SIGNAL,
|
||||
0);
|
||||
}
|
||||
if (distance > 0 && dY > NMS.getStepHeight(npc.getEntity()) && xzDistance <= 2.75) {
|
||||
NMS.setShouldJump(npc.getEntity());
|
||||
}
|
||||
double destX = vector.getX() + 0.5, destZ = vector.getZ() + 0.5;
|
||||
NMS.setDestination(npc.getEntity(), destX, vector.getY(), destZ, params.speed());
|
||||
params.run();
|
||||
plan.run(npc);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final AStarMachine<VectorNode, Path> ASTAR = AStarMachine.createWithDefaultStorage();
|
||||
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
|
||||
}
|
172
main/java/net/citizensnpcs/npc/ai/CitizensBlockBreaker.java
Normal file
172
main/java/net/citizensnpcs/npc/ai/CitizensBlockBreaker.java
Normal file
@ -0,0 +1,172 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.craftbukkit.v1_10_R1.entity.CraftEntity;
|
||||
import org.bukkit.craftbukkit.v1_10_R1.inventory.CraftItemStack;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import net.citizensnpcs.api.ai.tree.BehaviorStatus;
|
||||
import net.citizensnpcs.api.npc.BlockBreaker;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.PlayerAnimation;
|
||||
import net.citizensnpcs.util.Util;
|
||||
import net.minecraft.server.v1_10_R1.BlockPosition;
|
||||
import net.minecraft.server.v1_10_R1.Blocks;
|
||||
import net.minecraft.server.v1_10_R1.EnchantmentManager;
|
||||
import net.minecraft.server.v1_10_R1.Entity;
|
||||
import net.minecraft.server.v1_10_R1.EntityLiving;
|
||||
import net.minecraft.server.v1_10_R1.EntityPlayer;
|
||||
import net.minecraft.server.v1_10_R1.EnumItemSlot;
|
||||
import net.minecraft.server.v1_10_R1.IBlockData;
|
||||
import net.minecraft.server.v1_10_R1.ItemStack;
|
||||
import net.minecraft.server.v1_10_R1.Material;
|
||||
import net.minecraft.server.v1_10_R1.MobEffects;
|
||||
|
||||
public class CitizensBlockBreaker extends BlockBreaker {
|
||||
private final BlockBreakerConfiguration configuration;
|
||||
private int currentDamage;
|
||||
private int currentTick;
|
||||
private final Entity entity;
|
||||
private boolean isDigging = true;
|
||||
private final Location location;
|
||||
private int startDigTick;
|
||||
private final int x, y, z;
|
||||
|
||||
public CitizensBlockBreaker(org.bukkit.entity.Entity entity, org.bukkit.block.Block target,
|
||||
BlockBreakerConfiguration config) {
|
||||
this.entity = ((CraftEntity) entity).getHandle();
|
||||
this.x = target.getX();
|
||||
this.y = target.getY();
|
||||
this.z = target.getZ();
|
||||
this.location = target.getLocation();
|
||||
this.startDigTick = (int) (System.currentTimeMillis() / 50);
|
||||
this.configuration = config;
|
||||
}
|
||||
|
||||
private double distanceSquared() {
|
||||
return Math.pow(entity.locX - x, 2) + Math.pow(entity.locY - y, 2) + Math.pow(entity.locZ - z, 2);
|
||||
}
|
||||
|
||||
private net.minecraft.server.v1_10_R1.ItemStack getCurrentItem() {
|
||||
return configuration.item() != null ? CraftItemStack.asNMSCopy(configuration.item())
|
||||
: entity instanceof EntityLiving ? ((EntityLiving) entity).getEquipment(EnumItemSlot.MAINHAND) : null;
|
||||
}
|
||||
|
||||
private float getStrength(IBlockData block) {
|
||||
float base = block.getBlock().b(block, null, new BlockPosition(0, 0, 0));
|
||||
return base < 0.0F ? 0.0F : (!isDestroyable(block) ? 1.0F / base / 100.0F : strengthMod(block) / base / 30.0F);
|
||||
}
|
||||
|
||||
private boolean isDestroyable(IBlockData block) {
|
||||
if (block.getMaterial().isAlwaysDestroyable()) {
|
||||
return true;
|
||||
} else {
|
||||
ItemStack current = getCurrentItem();
|
||||
return current != null ? current.b(block) : false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
if (configuration.callback() != null) {
|
||||
configuration.callback().run();
|
||||
}
|
||||
isDigging = false;
|
||||
setBlockDamage(currentDamage = -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BehaviorStatus run() {
|
||||
if (entity.dead) {
|
||||
return BehaviorStatus.FAILURE;
|
||||
}
|
||||
if (!isDigging) {
|
||||
return BehaviorStatus.SUCCESS;
|
||||
}
|
||||
currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit
|
||||
if (configuration.radiusSquared() > 0 && distanceSquared() >= configuration.radiusSquared()) {
|
||||
startDigTick = currentTick;
|
||||
if (entity instanceof NPCHolder) {
|
||||
NPC npc = ((NPCHolder) entity).getNPC();
|
||||
if (npc != null && !npc.getNavigator().isNavigating()) {
|
||||
npc.getNavigator()
|
||||
.setTarget(entity.world.getWorld().getBlockAt(x, y, z).getLocation().add(0, 1, 0));
|
||||
}
|
||||
}
|
||||
return BehaviorStatus.RUNNING;
|
||||
}
|
||||
Util.faceLocation(entity.getBukkitEntity(), location);
|
||||
if (entity instanceof EntityPlayer) {
|
||||
PlayerAnimation.ARM_SWING.play((Player) entity.getBukkitEntity());
|
||||
}
|
||||
IBlockData block = entity.world.getType(new BlockPosition(x, y, z));
|
||||
if (block == null || block == Blocks.AIR) {
|
||||
return BehaviorStatus.SUCCESS;
|
||||
} else {
|
||||
int tickDifference = currentTick - startDigTick;
|
||||
float damage = getStrength(block) * (tickDifference + 1) * configuration.blockStrengthModifier();
|
||||
if (damage >= 1F) {
|
||||
entity.world.getWorld().getBlockAt(x, y, z)
|
||||
.breakNaturally(CraftItemStack.asCraftMirror(getCurrentItem()));
|
||||
return BehaviorStatus.SUCCESS;
|
||||
}
|
||||
int modifiedDamage = (int) (damage * 10.0F);
|
||||
if (modifiedDamage != currentDamage) {
|
||||
setBlockDamage(modifiedDamage);
|
||||
currentDamage = modifiedDamage;
|
||||
}
|
||||
}
|
||||
return BehaviorStatus.RUNNING;
|
||||
}
|
||||
|
||||
private void setBlockDamage(int modifiedDamage) {
|
||||
entity.world.c(entity.getId(), new BlockPosition(x, y, z), modifiedDamage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldExecute() {
|
||||
return entity.world.getType(new BlockPosition(x, y, z)).getBlock() != Blocks.AIR;
|
||||
}
|
||||
|
||||
private float strengthMod(IBlockData block) {
|
||||
ItemStack itemstack = getCurrentItem();
|
||||
float f = itemstack.a(block);
|
||||
if (entity instanceof EntityLiving) {
|
||||
EntityLiving handle = (EntityLiving) entity;
|
||||
if (f > 1.0F) {
|
||||
int i = EnchantmentManager.getDigSpeedEnchantmentLevel(handle);
|
||||
if (i > 0) {
|
||||
f += i * i + 1;
|
||||
}
|
||||
}
|
||||
if (handle.hasEffect(MobEffects.FASTER_DIG)) {
|
||||
f *= (1.0F + (handle.getEffect(MobEffects.FASTER_DIG).getAmplifier() + 1) * 0.2F);
|
||||
}
|
||||
if (handle.hasEffect(MobEffects.SLOWER_DIG)) {
|
||||
float f1 = 1.0F;
|
||||
switch (handle.getEffect(MobEffects.SLOWER_DIG).getAmplifier()) {
|
||||
case 0:
|
||||
f1 = 0.3F;
|
||||
break;
|
||||
case 1:
|
||||
f1 = 0.09F;
|
||||
break;
|
||||
case 2:
|
||||
f1 = 0.0027F;
|
||||
break;
|
||||
case 3:
|
||||
default:
|
||||
f1 = 8.1E-4F;
|
||||
}
|
||||
f *= f1;
|
||||
}
|
||||
if ((handle.a(Material.WATER)) && (!EnchantmentManager.i(handle))) {
|
||||
f /= 5.0F;
|
||||
}
|
||||
}
|
||||
if (!entity.onGround) {
|
||||
f /= 5.0F;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
}
|
407
main/java/net/citizensnpcs/npc/ai/CitizensNavigator.java
Normal file
407
main/java/net/citizensnpcs/npc/ai/CitizensNavigator.java
Normal file
@ -0,0 +1,407 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.ai.EntityTarget;
|
||||
import net.citizensnpcs.api.ai.Navigator;
|
||||
import net.citizensnpcs.api.ai.NavigatorParameters;
|
||||
import net.citizensnpcs.api.ai.PathStrategy;
|
||||
import net.citizensnpcs.api.ai.StuckAction;
|
||||
import net.citizensnpcs.api.ai.TargetType;
|
||||
import net.citizensnpcs.api.ai.TeleportStuckAction;
|
||||
import net.citizensnpcs.api.ai.event.CancelReason;
|
||||
import net.citizensnpcs.api.ai.event.NavigationBeginEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigationCancelEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigationCompleteEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigationReplaceEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigationStuckEvent;
|
||||
import net.citizensnpcs.api.ai.event.NavigatorCallback;
|
||||
import net.citizensnpcs.api.astar.pathfinder.BlockExaminer;
|
||||
import net.citizensnpcs.api.astar.pathfinder.BlockSource;
|
||||
import net.citizensnpcs.api.astar.pathfinder.MinecraftBlockExaminer;
|
||||
import net.citizensnpcs.api.astar.pathfinder.PathPoint;
|
||||
import net.citizensnpcs.api.astar.pathfinder.PathPoint.PathCallback;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
public class CitizensNavigator implements Navigator, Runnable {
|
||||
private final NavigatorParameters defaultParams = new NavigatorParameters().baseSpeed(UNINITIALISED_SPEED)
|
||||
.range(Setting.DEFAULT_PATHFINDING_RANGE.asFloat())
|
||||
.defaultAttackStrategy(MCTargetStrategy.DEFAULT_ATTACK_STRATEGY)
|
||||
.attackRange(Setting.NPC_ATTACK_DISTANCE.asDouble())
|
||||
.updatePathRate(Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt())
|
||||
.distanceMargin(Setting.DEFAULT_DISTANCE_MARGIN.asDouble())
|
||||
.stationaryTicks(Setting.DEFAULT_STATIONARY_TICKS.asInt()).stuckAction(TeleportStuckAction.INSTANCE)
|
||||
.examiner(new MinecraftBlockExaminer()).useNewPathfinder(Setting.USE_NEW_PATHFINDER.asBoolean());
|
||||
private PathStrategy executing;
|
||||
private int lastX, lastY, lastZ;
|
||||
private NavigatorParameters localParams = defaultParams;
|
||||
private final NPC npc;
|
||||
private boolean paused;
|
||||
private int stationaryTicks;
|
||||
|
||||
public CitizensNavigator(NPC npc) {
|
||||
this.npc = npc;
|
||||
if (Setting.NEW_PATHFINDER_OPENS_DOORS.asBoolean()) {
|
||||
defaultParams.examiner(new DoorExaminer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelNavigation() {
|
||||
stopNavigating(CancelReason.PLUGIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigatorParameters getDefaultParameters() {
|
||||
return defaultParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityTarget getEntityTarget() {
|
||||
return executing instanceof EntityTarget ? (EntityTarget) executing : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigatorParameters getLocalParameters() {
|
||||
if (!isNavigating()) {
|
||||
return defaultParams;
|
||||
}
|
||||
return localParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NPC getNPC() {
|
||||
return npc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathStrategy getPathStrategy() {
|
||||
return executing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getTargetAsLocation() {
|
||||
return isNavigating() ? executing.getTargetAsLocation() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetType getTargetType() {
|
||||
return isNavigating() ? executing.getTargetType() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNavigating() {
|
||||
return executing != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
public void load(DataKey root) {
|
||||
if (root.keyExists("pathfindingrange")) {
|
||||
defaultParams.range((float) root.getDouble("pathfindingrange"));
|
||||
}
|
||||
if (root.keyExists("stationaryticks")) {
|
||||
defaultParams.stationaryTicks(root.getInt("stationaryticks"));
|
||||
}
|
||||
if (root.keyExists("distancemargin")) {
|
||||
defaultParams.distanceMargin(root.getDouble("distancemargin"));
|
||||
}
|
||||
if (root.keyExists("updatepathrate")) {
|
||||
defaultParams.updatePathRate(root.getInt("updatepathrate"));
|
||||
}
|
||||
defaultParams.speedModifier((float) root.getDouble("speedmodifier", 1F));
|
||||
defaultParams.avoidWater(root.getBoolean("avoidwater"));
|
||||
if (!root.getBoolean("usedefaultstuckaction") && defaultParams.stuckAction() == TeleportStuckAction.INSTANCE) {
|
||||
defaultParams.stuckAction(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void onDespawn() {
|
||||
stopNavigating(CancelReason.NPC_DESPAWNED);
|
||||
}
|
||||
|
||||
public void onSpawn() {
|
||||
if (defaultParams.baseSpeed() == UNINITIALISED_SPEED) {
|
||||
defaultParams.baseSpeed(NMS.getSpeedFor(npc));
|
||||
}
|
||||
updatePathfindingRange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
updateMountedStatus();
|
||||
if (!isNavigating() || !npc.isSpawned() || paused)
|
||||
return;
|
||||
if (Math.pow(localParams.range(), 2) < npc.getStoredLocation().distanceSquared(getTargetAsLocation())) {
|
||||
stopNavigating(CancelReason.STUCK);
|
||||
return;
|
||||
}
|
||||
if (updateStationaryStatus())
|
||||
return;
|
||||
|
||||
updatePathfindingRange();
|
||||
boolean finished = executing.update();
|
||||
if (localParams.lookAtFunction() != null) {
|
||||
Util.faceLocation(npc.getEntity(), localParams.lookAtFunction().apply(this), true);
|
||||
}
|
||||
if (!finished) {
|
||||
return;
|
||||
}
|
||||
if (executing.getCancelReason() != null) {
|
||||
stopNavigating(executing.getCancelReason());
|
||||
} else {
|
||||
NavigationCompleteEvent event = new NavigationCompleteEvent(this);
|
||||
PathStrategy old = executing;
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
if (old == executing) {
|
||||
stopNavigating(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void save(DataKey root) {
|
||||
if (defaultParams.range() != Setting.DEFAULT_PATHFINDING_RANGE.asFloat()) {
|
||||
root.setDouble("pathfindingrange", defaultParams.range());
|
||||
} else {
|
||||
root.removeKey("pathfindingrange");
|
||||
}
|
||||
if (defaultParams.stationaryTicks() != Setting.DEFAULT_STATIONARY_TICKS.asInt()) {
|
||||
root.setInt("stationaryticks", defaultParams.stationaryTicks());
|
||||
} else {
|
||||
root.removeKey("stationaryticks");
|
||||
}
|
||||
if (defaultParams.distanceMargin() != Setting.DEFAULT_DISTANCE_MARGIN.asDouble()) {
|
||||
root.setDouble("distancemargin", defaultParams.distanceMargin());
|
||||
} else {
|
||||
root.removeKey("distancemargin");
|
||||
}
|
||||
if (defaultParams.updatePathRate() != Setting.DEFAULT_PATHFINDER_UPDATE_PATH_RATE.asInt()) {
|
||||
root.setInt("updatepathrate", defaultParams.updatePathRate());
|
||||
} else {
|
||||
root.removeKey("updatepathrate");
|
||||
}
|
||||
root.setDouble("speedmodifier", defaultParams.speedModifier());
|
||||
root.setBoolean("avoidwater", defaultParams.avoidWater());
|
||||
root.setBoolean("usedefaultstuckaction", defaultParams.stuckAction() == TeleportStuckAction.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPaused(boolean paused) {
|
||||
this.paused = paused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTarget(Entity target, boolean aggressive) {
|
||||
if (!npc.isSpawned())
|
||||
throw new IllegalStateException("npc is not spawned");
|
||||
if (target == null) {
|
||||
cancelNavigation();
|
||||
return;
|
||||
}
|
||||
switchParams();
|
||||
updatePathfindingRange();
|
||||
PathStrategy newStrategy = new MCTargetStrategy(npc, target, aggressive, localParams);
|
||||
switchStrategyTo(newStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTarget(Iterable<Vector> path) {
|
||||
if (!npc.isSpawned())
|
||||
throw new IllegalStateException("npc is not spawned");
|
||||
if (path == null || Iterables.size(path) == 0) {
|
||||
cancelNavigation();
|
||||
return;
|
||||
}
|
||||
switchParams();
|
||||
updatePathfindingRange();
|
||||
PathStrategy newStrategy;
|
||||
if (npc.isFlyable()) {
|
||||
newStrategy = new FlyingAStarNavigationStrategy(npc, path, localParams);
|
||||
} else if (localParams.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)) {
|
||||
newStrategy = new AStarNavigationStrategy(npc, path, localParams);
|
||||
} else {
|
||||
newStrategy = new MCNavigationStrategy(npc, path, localParams);
|
||||
}
|
||||
switchStrategyTo(newStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTarget(Location target) {
|
||||
if (!npc.isSpawned())
|
||||
throw new IllegalStateException("npc is not spawned");
|
||||
if (target == null) {
|
||||
cancelNavigation();
|
||||
return;
|
||||
}
|
||||
switchParams();
|
||||
updatePathfindingRange();
|
||||
PathStrategy newStrategy;
|
||||
if (npc.isFlyable()) {
|
||||
newStrategy = new FlyingAStarNavigationStrategy(npc, target, localParams);
|
||||
} else if (localParams.useNewPathfinder() || !(npc.getEntity() instanceof LivingEntity)) {
|
||||
newStrategy = new AStarNavigationStrategy(npc, target, localParams);
|
||||
} else {
|
||||
newStrategy = new MCNavigationStrategy(npc, target, localParams);
|
||||
}
|
||||
switchStrategyTo(newStrategy);
|
||||
}
|
||||
|
||||
private void stopNavigating() {
|
||||
if (executing != null) {
|
||||
executing.stop();
|
||||
}
|
||||
executing = null;
|
||||
localParams = defaultParams;
|
||||
stationaryTicks = 0;
|
||||
if (npc.isSpawned()) {
|
||||
Vector velocity = npc.getEntity().getVelocity();
|
||||
velocity.setX(0).setY(0).setZ(0);
|
||||
npc.getEntity().setVelocity(velocity);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopNavigating(CancelReason reason) {
|
||||
if (!isNavigating())
|
||||
return;
|
||||
Iterator<NavigatorCallback> itr = localParams.callbacks().iterator();
|
||||
while (itr.hasNext()) {
|
||||
itr.next().onCompletion(reason);
|
||||
itr.remove();
|
||||
}
|
||||
if (reason == null) {
|
||||
stopNavigating();
|
||||
return;
|
||||
}
|
||||
if (reason == CancelReason.STUCK) {
|
||||
StuckAction action = localParams.stuckAction();
|
||||
NavigationStuckEvent event = new NavigationStuckEvent(this, action);
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
action = event.getAction();
|
||||
boolean shouldContinue = action != null ? action.run(npc, this) : false;
|
||||
if (shouldContinue) {
|
||||
stationaryTicks = 0;
|
||||
executing.clearCancelReason();
|
||||
return;
|
||||
}
|
||||
}
|
||||
NavigationCancelEvent event = new NavigationCancelEvent(this, reason);
|
||||
PathStrategy old = executing;
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
if (old == executing) {
|
||||
stopNavigating();
|
||||
}
|
||||
}
|
||||
|
||||
private void switchParams() {
|
||||
localParams = defaultParams.clone();
|
||||
}
|
||||
|
||||
private void switchStrategyTo(PathStrategy newStrategy) {
|
||||
if (executing != null) {
|
||||
Bukkit.getPluginManager().callEvent(new NavigationReplaceEvent(this));
|
||||
}
|
||||
executing = newStrategy;
|
||||
stationaryTicks = 0;
|
||||
if (npc.isSpawned()) {
|
||||
NMS.updateNavigationWorld(npc.getEntity(), npc.getEntity().getWorld());
|
||||
}
|
||||
Bukkit.getPluginManager().callEvent(new NavigationBeginEvent(this));
|
||||
}
|
||||
|
||||
private void updateMountedStatus() {
|
||||
if (!isNavigating())
|
||||
return;
|
||||
Entity vehicle = NMS.getVehicle(npc.getEntity());
|
||||
if (!(vehicle instanceof NPCHolder))
|
||||
return;
|
||||
NPC mount = ((NPCHolder) vehicle).getNPC();
|
||||
switch (getTargetType()) {
|
||||
case ENTITY:
|
||||
mount.getNavigator().setTarget(getEntityTarget().getTarget(), getEntityTarget().isAggressive());
|
||||
break;
|
||||
case LOCATION:
|
||||
mount.getNavigator().setTarget(getTargetAsLocation());
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
cancelNavigation();
|
||||
}
|
||||
|
||||
private void updatePathfindingRange() {
|
||||
NMS.updatePathfindingRange(npc, localParams.range());
|
||||
}
|
||||
|
||||
private boolean updateStationaryStatus() {
|
||||
if (localParams.stationaryTicks() < 0)
|
||||
return false;
|
||||
Location current = npc.getEntity().getLocation(STATIONARY_LOCATION);
|
||||
if (current.getY() < -5) {
|
||||
stopNavigating(CancelReason.STUCK);
|
||||
return true;
|
||||
}
|
||||
if (lastX == current.getBlockX() && lastY == current.getBlockY() && lastZ == current.getBlockZ()) {
|
||||
if (++stationaryTicks >= localParams.stationaryTicks()) {
|
||||
stopNavigating(CancelReason.STUCK);
|
||||
return true;
|
||||
}
|
||||
} else
|
||||
stationaryTicks = 0;
|
||||
lastX = current.getBlockX();
|
||||
lastY = current.getBlockY();
|
||||
lastZ = current.getBlockZ();
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class DoorExaminer implements BlockExaminer {
|
||||
@Override
|
||||
public float getCost(BlockSource source, PathPoint point) {
|
||||
return 0F;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PassableState isPassable(BlockSource source, PathPoint point) {
|
||||
Material in = source.getMaterialAt(point.getVector());
|
||||
if (MinecraftBlockExaminer.isDoor(in)) {
|
||||
point.addCallback(new DoorOpener());
|
||||
return PassableState.PASSABLE;
|
||||
}
|
||||
return PassableState.IGNORE;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DoorOpener implements PathCallback {
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void run(NPC npc, Block point, ListIterator<Block> path) {
|
||||
if (npc.getStoredLocation().distance(point.getLocation()) < 2) {
|
||||
boolean bottom = (point.getData() & 8) == 0;
|
||||
Block set = bottom ? point : point.getRelative(BlockFace.DOWN);
|
||||
set.setData((byte) ((set.getData() & 7) | 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Location STATIONARY_LOCATION = new Location(null, 0, 0, 0);
|
||||
|
||||
private static int UNINITIALISED_SPEED = Integer.MIN_VALUE;
|
||||
}
|
@ -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);
|
||||
}
|
97
main/java/net/citizensnpcs/npc/ai/MCNavigationStrategy.java
Normal file
97
main/java/net/citizensnpcs/npc/ai/MCNavigationStrategy.java
Normal file
@ -0,0 +1,97 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.citizensnpcs.api.ai.AbstractPathStrategy;
|
||||
import net.citizensnpcs.api.ai.NavigatorParameters;
|
||||
import net.citizensnpcs.api.ai.TargetType;
|
||||
import net.citizensnpcs.api.ai.event.CancelReason;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
public class MCNavigationStrategy extends AbstractPathStrategy {
|
||||
private final Entity handle;
|
||||
private final MCNavigator navigator;
|
||||
private final NavigatorParameters parameters;
|
||||
private final Location target;
|
||||
|
||||
MCNavigationStrategy(final NPC npc, Iterable<Vector> path, NavigatorParameters params) {
|
||||
super(TargetType.LOCATION);
|
||||
List<Vector> list = Lists.newArrayList(path);
|
||||
this.target = list.get(list.size() - 1).toLocation(npc.getStoredLocation().getWorld());
|
||||
this.parameters = params;
|
||||
handle = npc.getEntity();
|
||||
this.navigator = NMS.getTargetNavigator(npc.getEntity(), list, params);
|
||||
}
|
||||
|
||||
MCNavigationStrategy(final NPC npc, Location dest, NavigatorParameters params) {
|
||||
super(TargetType.LOCATION);
|
||||
this.target = dest;
|
||||
this.parameters = params;
|
||||
handle = npc.getEntity();
|
||||
this.navigator = NMS.getTargetNavigator(npc.getEntity(), dest, params);
|
||||
}
|
||||
|
||||
private double distanceSquared() {
|
||||
return handle.getLocation(HANDLE_LOCATION).distanceSquared(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Vector> getPath() {
|
||||
return navigator.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getTargetAsLocation() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetType getTargetType() {
|
||||
return TargetType.LOCATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
navigator.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MCNavigationStrategy [target=" + target + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update() {
|
||||
if (navigator.getCancelReason() != null) {
|
||||
setCancelReason(navigator.getCancelReason());
|
||||
}
|
||||
if (getCancelReason() != null)
|
||||
return true;
|
||||
boolean wasFinished = navigator.update();
|
||||
parameters.run();
|
||||
if (distanceSquared() < parameters.distanceMargin()) {
|
||||
stop();
|
||||
return true;
|
||||
}
|
||||
return wasFinished;
|
||||
}
|
||||
|
||||
public static interface MCNavigator {
|
||||
CancelReason getCancelReason();
|
||||
|
||||
Iterable<Vector> getPath();
|
||||
|
||||
void stop();
|
||||
|
||||
boolean update();
|
||||
}
|
||||
|
||||
private static final Location HANDLE_LOCATION = new Location(null, 0, 0, 0);
|
||||
}
|
204
main/java/net/citizensnpcs/npc/ai/MCTargetStrategy.java
Normal file
204
main/java/net/citizensnpcs/npc/ai/MCTargetStrategy.java
Normal file
@ -0,0 +1,204 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import net.citizensnpcs.api.ai.AttackStrategy;
|
||||
import net.citizensnpcs.api.ai.EntityTarget;
|
||||
import net.citizensnpcs.api.ai.NavigatorParameters;
|
||||
import net.citizensnpcs.api.ai.PathStrategy;
|
||||
import net.citizensnpcs.api.ai.TargetType;
|
||||
import net.citizensnpcs.api.ai.event.CancelReason;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.util.BoundingBox;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
public class MCTargetStrategy implements PathStrategy, EntityTarget {
|
||||
private final boolean aggro;
|
||||
private int attackTicks;
|
||||
private CancelReason cancelReason;
|
||||
private final Entity handle;
|
||||
private final NPC npc;
|
||||
private final NavigatorParameters parameters;
|
||||
private final Entity target;
|
||||
private final TargetNavigator targetNavigator;
|
||||
private int updateCounter = -1;
|
||||
|
||||
public MCTargetStrategy(NPC npc, org.bukkit.entity.Entity target, boolean aggro, NavigatorParameters params) {
|
||||
this.npc = npc;
|
||||
this.parameters = params;
|
||||
this.handle = npc.getEntity();
|
||||
this.target = target;
|
||||
TargetNavigator nav = NMS.getTargetNavigator(npc.getEntity(), target, params);
|
||||
this.targetNavigator = nav != null && !params.useNewPathfinder() ? nav : new AStarTargeter();
|
||||
this.aggro = aggro;
|
||||
}
|
||||
|
||||
private boolean canAttack() {
|
||||
BoundingBox handleBB = NMS.getBoundingBox(handle), targetBB = NMS.getBoundingBox(target);
|
||||
return attackTicks == 0 && (handleBB.maxY > targetBB.minY && handleBB.minY < targetBB.maxY)
|
||||
&& closeEnough(distanceSquared()) && hasLineOfSight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCancelReason() {
|
||||
cancelReason = null;
|
||||
}
|
||||
|
||||
private boolean closeEnough(double distance) {
|
||||
return distance <= parameters.attackRange();
|
||||
}
|
||||
|
||||
private double distanceSquared() {
|
||||
return handle.getLocation(HANDLE_LOCATION).distanceSquared(target.getLocation(TARGET_LOCATION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CancelReason getCancelReason() {
|
||||
return cancelReason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Vector> getPath() {
|
||||
return targetNavigator.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.bukkit.entity.Entity getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getTargetAsLocation() {
|
||||
return getTarget().getLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetType getTargetType() {
|
||||
return TargetType.ENTITY;
|
||||
}
|
||||
|
||||
private boolean hasLineOfSight() {
|
||||
return ((LivingEntity) handle).hasLineOfSight(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAggressive() {
|
||||
return aggro;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
targetNavigator.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MCTargetStrategy [target=" + target + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update() {
|
||||
if (target == null || !target.isValid()) {
|
||||
cancelReason = CancelReason.TARGET_DIED;
|
||||
return true;
|
||||
}
|
||||
if (target.getWorld() != handle.getWorld()) {
|
||||
cancelReason = CancelReason.TARGET_MOVED_WORLD;
|
||||
return true;
|
||||
}
|
||||
if (cancelReason != null) {
|
||||
return true;
|
||||
}
|
||||
if (!aggro && distanceSquared() <= parameters.distanceMargin()) {
|
||||
stop();
|
||||
return false;
|
||||
} else if (updateCounter == -1 || updateCounter++ > parameters.updatePathRate()) {
|
||||
targetNavigator.setPath();
|
||||
updateCounter = 0;
|
||||
}
|
||||
targetNavigator.update();
|
||||
|
||||
NMS.look(handle, target);
|
||||
if (aggro && canAttack()) {
|
||||
AttackStrategy strategy = parameters.attackStrategy();
|
||||
if (strategy != null && strategy.handle((LivingEntity) handle, (LivingEntity) getTarget())) {
|
||||
} else if (strategy != parameters.defaultAttackStrategy()) {
|
||||
parameters.defaultAttackStrategy().handle((LivingEntity) handle, (LivingEntity) getTarget());
|
||||
}
|
||||
attackTicks = parameters.attackDelayTicks();
|
||||
}
|
||||
if (attackTicks > 0) {
|
||||
attackTicks--;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private class AStarTargeter implements TargetNavigator {
|
||||
private int failureTimes = 0;
|
||||
private PathStrategy strategy;
|
||||
|
||||
@Override
|
||||
public Iterable<Vector> getPath() {
|
||||
return strategy.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPath() {
|
||||
setStrategy();
|
||||
strategy.update();
|
||||
CancelReason subReason = strategy.getCancelReason();
|
||||
if (subReason == CancelReason.STUCK) {
|
||||
if (failureTimes++ > 10) {
|
||||
cancelReason = strategy.getCancelReason();
|
||||
}
|
||||
} else {
|
||||
failureTimes = 0;
|
||||
cancelReason = strategy.getCancelReason();
|
||||
}
|
||||
}
|
||||
|
||||
private void setStrategy() {
|
||||
Location location = parameters.entityTargetLocationMapper().apply(target);
|
||||
if (location == null) {
|
||||
throw new IllegalStateException("mapper should not return null");
|
||||
}
|
||||
strategy = npc.isFlyable() ? new FlyingAStarNavigationStrategy(npc, location, parameters)
|
||||
: new AStarNavigationStrategy(npc, location, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
strategy.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
strategy.update();
|
||||
}
|
||||
}
|
||||
|
||||
public static interface TargetNavigator {
|
||||
Iterable<Vector> getPath();
|
||||
|
||||
void setPath();
|
||||
|
||||
void stop();
|
||||
|
||||
void update();
|
||||
}
|
||||
|
||||
static final AttackStrategy DEFAULT_ATTACK_STRATEGY = new AttackStrategy() {
|
||||
@Override
|
||||
public boolean handle(LivingEntity attacker, LivingEntity bukkitTarget) {
|
||||
NMS.attack(attacker, bukkitTarget);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private static final Location HANDLE_LOCATION = new Location(null, 0, 0, 0);
|
||||
|
||||
private static final Location TARGET_LOCATION = new Location(null, 0, 0, 0);
|
||||
}
|
7
main/java/net/citizensnpcs/npc/ai/NPCHolder.java
Normal file
7
main/java/net/citizensnpcs/npc/ai/NPCHolder.java
Normal file
@ -0,0 +1,7 @@
|
||||
package net.citizensnpcs.npc.ai;
|
||||
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
|
||||
public interface NPCHolder {
|
||||
public NPC getNPC();
|
||||
}
|
133
main/java/net/citizensnpcs/npc/ai/speech/Chat.java
Normal file
133
main/java/net/citizensnpcs/npc/ai/speech/Chat.java
Normal file
@ -0,0 +1,133 @@
|
||||
package net.citizensnpcs.npc.ai.speech;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.ai.speech.SpeechContext;
|
||||
import net.citizensnpcs.api.ai.speech.Talkable;
|
||||
import net.citizensnpcs.api.ai.speech.VocalChord;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
|
||||
import org.bukkit.entity.Entity;
|
||||
|
||||
public class Chat implements VocalChord {
|
||||
public final String VOCAL_CHORD_NAME = "chat";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return VOCAL_CHORD_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talk(SpeechContext context) {
|
||||
if (context.getTalker() == null)
|
||||
return;
|
||||
NPC npc = CitizensAPI.getNPCRegistry().getNPC(context.getTalker().getEntity());
|
||||
if (npc == null)
|
||||
return;
|
||||
|
||||
// chat to the world with CHAT_FORMAT and CHAT_RANGE settings
|
||||
if (!context.hasRecipients()) {
|
||||
String text = Setting.CHAT_FORMAT.asString().replace("<npc>", npc.getName()).replace("<text>",
|
||||
context.getMessage());
|
||||
talkToBystanders(npc, text, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assumed recipients at this point
|
||||
else if (context.size() <= 1) {
|
||||
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>",
|
||||
context.getMessage());
|
||||
String targetName = "";
|
||||
// For each recipient
|
||||
for (Talkable entity : context) {
|
||||
entity.talkTo(context, text, this);
|
||||
targetName = entity.getName();
|
||||
}
|
||||
// Check if bystanders hear targeted chat
|
||||
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean())
|
||||
return;
|
||||
// Format message with config setting and send to bystanders
|
||||
String bystanderText = Setting.CHAT_FORMAT_TO_BYSTANDERS.asString().replace("<npc>", npc.getName())
|
||||
.replace("<target>", targetName).replace("<text>", context.getMessage());
|
||||
talkToBystanders(npc, bystanderText, context);
|
||||
return;
|
||||
}
|
||||
|
||||
else { // Multiple recipients
|
||||
String text = Setting.CHAT_FORMAT_TO_TARGET.asString().replace("<npc>", npc.getName()).replace("<text>",
|
||||
context.getMessage());
|
||||
List<String> targetNames = new ArrayList<String>();
|
||||
// Talk to each recipient
|
||||
for (Talkable entity : context) {
|
||||
entity.talkTo(context, text, this);
|
||||
targetNames.add(entity.getName());
|
||||
}
|
||||
|
||||
if (!Setting.CHAT_BYSTANDERS_HEAR_TARGETED_CHAT.asBoolean())
|
||||
return;
|
||||
String targets = "";
|
||||
int max = Setting.CHAT_MAX_NUMBER_OF_TARGETS.asInt();
|
||||
String[] format = Setting.CHAT_MULTIPLE_TARGETS_FORMAT.asString().split("\\|");
|
||||
if (format.length != 4)
|
||||
Messaging.severe("npc.chat.options.multiple-targets-format invalid!");
|
||||
if (max == 1) {
|
||||
targets = format[0].replace("<target>", targetNames.get(0)) + format[3];
|
||||
} else if (max == 2 || targetNames.size() == 2) {
|
||||
if (targetNames.size() == 2) {
|
||||
targets = format[0].replace("<target>", targetNames.get(0))
|
||||
+ format[2].replace("<target>", targetNames.get(1));
|
||||
} else
|
||||
targets = format[0].replace("<target>", targetNames.get(0))
|
||||
+ format[1].replace("<target>", targetNames.get(1)) + format[3];
|
||||
} else if (max >= 3) {
|
||||
targets = format[0].replace("<target>", targetNames.get(0));
|
||||
|
||||
int x = 1;
|
||||
for (x = 1; x < max - 1; x++) {
|
||||
if (targetNames.size() - 1 == x)
|
||||
break;
|
||||
targets = targets + format[1].replace("<npc>", targetNames.get(x));
|
||||
}
|
||||
if (targetNames.size() == max) {
|
||||
targets = targets + format[2].replace("<npc>", targetNames.get(x));
|
||||
} else
|
||||
targets = targets + format[3];
|
||||
}
|
||||
|
||||
String bystanderText = Setting.CHAT_FORMAT_WITH_TARGETS_TO_BYSTANDERS.asString()
|
||||
.replace("<npc>", npc.getName()).replace("<targets>", targets)
|
||||
.replace("<text>", context.getMessage());
|
||||
talkToBystanders(npc, bystanderText, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void talkToBystanders(NPC npc, String text, SpeechContext context) {
|
||||
// Get list of nearby entities
|
||||
List<Entity> bystanderEntities = npc.getEntity().getNearbyEntities(Setting.CHAT_RANGE.asDouble(),
|
||||
Setting.CHAT_RANGE.asDouble(), Setting.CHAT_RANGE.asDouble());
|
||||
for (Entity bystander : bystanderEntities) {
|
||||
// Continue if a LivingEntity, which is compatible with
|
||||
// TalkableEntity
|
||||
boolean shouldTalk = true;
|
||||
// Exclude targeted recipients
|
||||
if (context.hasRecipients()) {
|
||||
for (Talkable target : context) {
|
||||
if (target.getEntity().equals(bystander)) {
|
||||
shouldTalk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Found a nearby LivingEntity, make it Talkable and
|
||||
// talkNear it if 'should_talk'
|
||||
if (shouldTalk) {
|
||||
new TalkableEntity(bystander).talkNear(context, text, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
92
main/java/net/citizensnpcs/npc/ai/speech/TalkableEntity.java
Normal file
92
main/java/net/citizensnpcs/npc/ai/speech/TalkableEntity.java
Normal file
@ -0,0 +1,92 @@
|
||||
package net.citizensnpcs.npc.ai.speech;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.ai.speech.SpeechContext;
|
||||
import net.citizensnpcs.api.ai.speech.Talkable;
|
||||
import net.citizensnpcs.api.ai.speech.VocalChord;
|
||||
import net.citizensnpcs.api.ai.speech.event.SpeechBystanderEvent;
|
||||
import net.citizensnpcs.api.ai.speech.event.SpeechTargetedEvent;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class TalkableEntity implements Talkable {
|
||||
Entity entity;
|
||||
|
||||
public TalkableEntity(Entity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
public TalkableEntity(NPC npc) {
|
||||
entity = npc.getEntity();
|
||||
}
|
||||
|
||||
public TalkableEntity(Player player) {
|
||||
entity = player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to compare a LivingEntity to this TalkableEntity
|
||||
*
|
||||
* @return 0 if the Entities are the same, 1 if they are not, -1 if the object compared is not a valid LivingEntity
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Object o) {
|
||||
// If not living entity, return -1
|
||||
if (!(o instanceof Entity)) {
|
||||
return -1;
|
||||
// If NPC and matches, return 0
|
||||
} else if (CitizensAPI.getNPCRegistry().isNPC((Entity) o) && CitizensAPI.getNPCRegistry().isNPC(entity)
|
||||
&& CitizensAPI.getNPCRegistry().getNPC((Entity) o).getUniqueId()
|
||||
.equals(CitizensAPI.getNPCRegistry().getNPC(entity).getUniqueId())) {
|
||||
return 0;
|
||||
} else if (entity.equals(o)) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
if (CitizensAPI.getNPCRegistry().isNPC(entity)) {
|
||||
return CitizensAPI.getNPCRegistry().getNPC(entity).getName();
|
||||
} else if (entity instanceof Player) {
|
||||
return ((Player) entity).getName();
|
||||
} else {
|
||||
return entity.getType().name().replace("_", " ");
|
||||
}
|
||||
}
|
||||
|
||||
private void talk(String message) {
|
||||
if (entity instanceof Player && !CitizensAPI.getNPCRegistry().isNPC(entity))
|
||||
Messaging.send((Player) entity, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talkNear(SpeechContext context, String text, VocalChord vocalChord) {
|
||||
SpeechBystanderEvent event = new SpeechBystanderEvent(this, context, text, vocalChord);
|
||||
Bukkit.getServer().getPluginManager().callEvent(event);
|
||||
if (event.isCancelled())
|
||||
return;
|
||||
talk(event.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talkTo(SpeechContext context, String text, VocalChord vocalChord) {
|
||||
SpeechTargetedEvent event = new SpeechTargetedEvent(this, context, text, vocalChord);
|
||||
Bukkit.getServer().getPluginManager().callEvent(event);
|
||||
if (event.isCancelled())
|
||||
return;
|
||||
talk(event.getMessage());
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
107
main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java
Normal file
107
main/java/net/citizensnpcs/npc/profile/ProfileFetchThread.java
Normal file
@ -0,0 +1,107 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Thread used to fetch profiles from the Mojang servers.
|
||||
*
|
||||
* <p>
|
||||
* Maintains a cache of profiles so that no profile is ever requested more than once during a single server session.
|
||||
* </p>
|
||||
*
|
||||
* @see ProfileFetcher
|
||||
*/
|
||||
class ProfileFetchThread implements Runnable {
|
||||
private final ProfileFetcher profileFetcher = new ProfileFetcher();
|
||||
private final Deque<ProfileRequest> queue = new ArrayDeque<ProfileRequest>();
|
||||
private final Map<String, ProfileRequest> requested = new HashMap<String, ProfileRequest>(35);
|
||||
private final Object sync = new Object(); // sync for queue & requested fields
|
||||
|
||||
ProfileFetchThread() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a profile.
|
||||
*
|
||||
* @param name
|
||||
* The name of the player the profile belongs to.
|
||||
* @param handler
|
||||
* Optional handler to handle result fetch result. Handler always invoked from the main thread.
|
||||
*
|
||||
* @see ProfileFetcher#fetch
|
||||
*/
|
||||
void fetch(String name, @Nullable ProfileFetchHandler handler) {
|
||||
Preconditions.checkNotNull(name);
|
||||
|
||||
name = name.toLowerCase();
|
||||
ProfileRequest request;
|
||||
|
||||
synchronized (sync) {
|
||||
request = requested.get(name);
|
||||
if (request == null) {
|
||||
request = new ProfileRequest(name, handler);
|
||||
queue.add(request);
|
||||
requested.put(name, request);
|
||||
return;
|
||||
} else if (request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
queue.add(request);
|
||||
}
|
||||
}
|
||||
|
||||
if (handler != null) {
|
||||
|
||||
if (request.getResult() == ProfileFetchResult.PENDING
|
||||
|| request.getResult() == ProfileFetchResult.TOO_MANY_REQUESTS) {
|
||||
addHandler(request, handler);
|
||||
} else {
|
||||
sendResult(handler, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
List<ProfileRequest> requests;
|
||||
|
||||
synchronized (sync) {
|
||||
if (queue.isEmpty())
|
||||
return;
|
||||
|
||||
requests = new ArrayList<ProfileRequest>(queue);
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
profileFetcher.fetchRequests(requests);
|
||||
}
|
||||
|
||||
private static void addHandler(final ProfileRequest request, final ProfileFetchHandler handler) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
request.addHandler(handler);
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
|
||||
private static void sendResult(final ProfileFetchHandler handler, final ProfileRequest request) {
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handler.onResult(request);
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
}
|
167
main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal file
167
main/java/net/citizensnpcs/npc/profile/ProfileFetcher.java
Normal file
@ -0,0 +1,167 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.mojang.authlib.Agent;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.GameProfileRepository;
|
||||
import com.mojang.authlib.ProfileLookupCallback;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
/**
|
||||
* Fetches game profiles that include skin data from Mojang servers.
|
||||
*
|
||||
* @see ProfileFetchThread
|
||||
*/
|
||||
public class ProfileFetcher {
|
||||
ProfileFetcher() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more profiles.
|
||||
*
|
||||
* @param requests
|
||||
* The profile requests.
|
||||
*/
|
||||
void fetchRequests(final Collection<ProfileRequest> requests) {
|
||||
Preconditions.checkNotNull(requests);
|
||||
|
||||
final GameProfileRepository repo = NMS.getGameProfileRepository();
|
||||
|
||||
String[] playerNames = new String[requests.size()];
|
||||
|
||||
int i = 0;
|
||||
for (ProfileRequest request : requests) {
|
||||
playerNames[i++] = request.getPlayerName();
|
||||
}
|
||||
|
||||
repo.findProfilesByNames(playerNames, Agent.MINECRAFT, new ProfileLookupCallback() {
|
||||
@Override
|
||||
public void onProfileLookupFailed(GameProfile profile, Exception e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug(
|
||||
"Profile lookup for player '" + profile.getName() + "' failed2: " + getExceptionMsg(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
|
||||
ProfileRequest request = findRequest(profile.getName(), requests);
|
||||
if (request == null)
|
||||
return;
|
||||
|
||||
if (isProfileNotFound(e)) {
|
||||
request.setResult(null, ProfileFetchResult.NOT_FOUND);
|
||||
} else if (isTooManyRequests(e)) {
|
||||
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||
} else {
|
||||
request.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProfileLookupSucceeded(final GameProfile profile) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Fetched profile " + profile.getId() + " for player " + profile.getName());
|
||||
}
|
||||
|
||||
ProfileRequest request = findRequest(profile.getName(), requests);
|
||||
if (request == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
request.setResult(NMS.fillProfileProperties(profile, true), ProfileFetchResult.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug(
|
||||
"Profile lookup for player '" + profile.getName() + "' failed2: " + getExceptionMsg(e));
|
||||
Messaging.debug(Throwables.getStackTraceAsString(e));
|
||||
}
|
||||
|
||||
if (isTooManyRequests(e)) {
|
||||
request.setResult(null, ProfileFetchResult.TOO_MANY_REQUESTS);
|
||||
} else {
|
||||
request.setResult(null, ProfileFetchResult.FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a profile.
|
||||
*
|
||||
* @param name
|
||||
* The name of the player the profile belongs to.
|
||||
* @param handler
|
||||
* Optional handler to handle the result. Handler always invoked from the main thread.
|
||||
*/
|
||||
public static void fetch(String name, @Nullable ProfileFetchHandler handler) {
|
||||
Preconditions.checkNotNull(name);
|
||||
|
||||
if (PROFILE_THREAD == null) {
|
||||
initThread();
|
||||
}
|
||||
PROFILE_THREAD.fetch(name, handler);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProfileRequest findRequest(String name, Collection<ProfileRequest> requests) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
for (ProfileRequest request : requests) {
|
||||
if (request.getPlayerName().equals(name)) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getExceptionMsg(Exception e) {
|
||||
return Throwables.getRootCause(e).getMessage();
|
||||
}
|
||||
|
||||
private static void initThread() {
|
||||
if (THREAD_TASK != null) {
|
||||
THREAD_TASK.cancel();
|
||||
}
|
||||
|
||||
PROFILE_THREAD = new ProfileFetchThread();
|
||||
THREAD_TASK = Bukkit.getScheduler().runTaskTimerAsynchronously(CitizensAPI.getPlugin(), PROFILE_THREAD, 21, 20);
|
||||
}
|
||||
|
||||
private static boolean isProfileNotFound(Exception e) {
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return (message != null && message.contains("did not find"))
|
||||
|| (cause != null && cause.contains("did not find"));
|
||||
}
|
||||
|
||||
private static boolean isTooManyRequests(Exception e) {
|
||||
|
||||
String message = e.getMessage();
|
||||
String cause = e.getCause() != null ? e.getCause().getMessage() : null;
|
||||
|
||||
return (message != null && message.contains("too many requests"))
|
||||
|| (cause != null && cause.contains("too many requests"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all queued and cached requests.
|
||||
*/
|
||||
public static void reset() {
|
||||
initThread();
|
||||
}
|
||||
|
||||
private static ProfileFetchThread PROFILE_THREAD;
|
||||
private static BukkitTask THREAD_TASK;
|
||||
}
|
127
main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal file
127
main/java/net/citizensnpcs/npc/profile/ProfileRequest.java
Normal file
@ -0,0 +1,127 @@
|
||||
package net.citizensnpcs.npc.profile;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
|
||||
/**
|
||||
* Stores basic information about a single profile used to request profiles from the Mojang servers.
|
||||
*
|
||||
* <p>
|
||||
* Also stores the result of the request.
|
||||
* </p>
|
||||
*/
|
||||
public class ProfileRequest {
|
||||
private Deque<ProfileFetchHandler> handlers;
|
||||
private final String playerName;
|
||||
private GameProfile profile;
|
||||
private volatile ProfileFetchResult result = ProfileFetchResult.PENDING;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param playerName
|
||||
* The name of the player whose profile is being requested.
|
||||
* @param handler
|
||||
* Optional handler to handle the result for the profile. Handler always invoked from the main thread.
|
||||
*/
|
||||
ProfileRequest(String playerName, @Nullable ProfileFetchHandler handler) {
|
||||
Preconditions.checkNotNull(playerName);
|
||||
|
||||
this.playerName = playerName;
|
||||
|
||||
if (handler != null) {
|
||||
addHandler(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one time result handler.
|
||||
*
|
||||
* <p>
|
||||
* Handler is always invoked from the main thread.
|
||||
* </p>
|
||||
*
|
||||
* @param handler
|
||||
* The result handler.
|
||||
*/
|
||||
public void addHandler(ProfileFetchHandler handler) {
|
||||
Preconditions.checkNotNull(handler);
|
||||
|
||||
if (result != ProfileFetchResult.PENDING) {
|
||||
handler.onResult(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handlers == null)
|
||||
handlers = new ArrayDeque<ProfileFetchHandler>();
|
||||
|
||||
handlers.addLast(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the player the requested profile belongs to.
|
||||
*/
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the game profile that was requested.
|
||||
*
|
||||
* @return The game profile or null if the profile has not been retrieved yet or there was an error while retrieving
|
||||
* the profile.
|
||||
*/
|
||||
@Nullable
|
||||
public GameProfile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result of the profile fetch.
|
||||
*/
|
||||
public ProfileFetchResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to set the profile result.
|
||||
*
|
||||
* <p>
|
||||
* Can be invoked from any thread, always executes on the main thread.
|
||||
* </p>
|
||||
*
|
||||
* @param profile
|
||||
* The profile. Null if there was an error.
|
||||
* @param result
|
||||
* The result of the request.
|
||||
*/
|
||||
void setResult(final @Nullable GameProfile profile, final ProfileFetchResult result) {
|
||||
if (!CitizensAPI.hasImplementation())
|
||||
return;
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ProfileRequest.this.profile = profile;
|
||||
ProfileRequest.this.result = result;
|
||||
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
while (!handlers.isEmpty()) {
|
||||
handlers.removeFirst().onResult(ProfileRequest.this);
|
||||
}
|
||||
|
||||
handlers = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
341
main/java/net/citizensnpcs/npc/skin/Skin.java
Normal file
341
main/java/net/citizensnpcs/npc/skin/Skin.java
Normal file
@ -0,0 +1,341 @@
|
||||
package net.citizensnpcs.npc.skin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.event.DespawnReason;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetchHandler;
|
||||
import net.citizensnpcs.npc.profile.ProfileFetcher;
|
||||
import net.citizensnpcs.npc.profile.ProfileRequest;
|
||||
|
||||
/**
|
||||
* Stores data for a single skin.
|
||||
*/
|
||||
public class Skin {
|
||||
private int fetchRetries = -1;
|
||||
private boolean hasFetched;
|
||||
private volatile boolean isValid = true;
|
||||
private final Map<SkinnableEntity, Void> pending = new WeakHashMap<SkinnableEntity, Void>(15);
|
||||
private BukkitTask retryTask;
|
||||
private volatile Property skinData;
|
||||
private volatile UUID skinId;
|
||||
private final String skinName;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param skinName
|
||||
* The name of the player the skin belongs to.
|
||||
* @param forceUpdate
|
||||
*/
|
||||
Skin(String skinName) {
|
||||
this.skinName = skinName.toLowerCase();
|
||||
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.containsKey(this.skinName))
|
||||
throw new IllegalArgumentException("There is already a skin named " + skinName);
|
||||
|
||||
CACHE.put(this.skinName, this);
|
||||
}
|
||||
|
||||
fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the skin data to the specified skinnable entity.
|
||||
*
|
||||
* <p>
|
||||
* If invoked before the skin data is ready, the skin is retrieved and the skin is automatically applied to the
|
||||
* entity at a later time.
|
||||
* </p>
|
||||
*
|
||||
* @param entity
|
||||
* The skinnable entity.
|
||||
*
|
||||
* @return True if skin was applied, false if the data is being retrieved.
|
||||
*/
|
||||
public boolean apply(SkinnableEntity entity) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
NPC npc = entity.getNPC();
|
||||
|
||||
// Use npc cached skin if available.
|
||||
// If npc requires latest skin, cache is used for faster
|
||||
// availability until the latest skin can be loaded.
|
||||
String cachedName = npc.data().get(CACHED_SKIN_UUID_NAME_METADATA);
|
||||
String texture = npc.data().get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA, "cache");
|
||||
if (this.skinName.equals(cachedName) && !texture.equals("cache")) {
|
||||
Property localData = new Property("textures", texture,
|
||||
npc.data().<String> get(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA));
|
||||
setNPCTexture(entity, localData);
|
||||
|
||||
// check if NPC prefers to use cached skin over the latest skin.
|
||||
if (!entity.getNPC().data().get(NPC.PLAYER_SKIN_USE_LATEST, Setting.NPC_SKIN_USE_LATEST.asBoolean())) {
|
||||
// cache preferred
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSkinData()) {
|
||||
if (hasFetched) {
|
||||
return true;
|
||||
} else {
|
||||
pending.put(entity, null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setNPCSkinData(entity, skinName, skinId, skinData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the skin data to the specified skinnable entity and respawn the NPC.
|
||||
*
|
||||
* @param entity
|
||||
* The skinnable entity.
|
||||
*/
|
||||
public void applyAndRespawn(SkinnableEntity entity) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
if (!apply(entity))
|
||||
return;
|
||||
|
||||
NPC npc = entity.getNPC();
|
||||
|
||||
if (npc.isSpawned()) {
|
||||
npc.despawn(DespawnReason.PENDING_RESPAWN);
|
||||
npc.spawn(npc.getStoredLocation());
|
||||
}
|
||||
}
|
||||
|
||||
private void fetch() {
|
||||
final int maxRetries = Setting.MAX_NPC_SKIN_RETRIES.asInt();
|
||||
if (maxRetries > -1 && fetchRetries >= maxRetries) {
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Reached max skin fetch retries for '" + skinName + "'");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ProfileFetcher.fetch(this.skinName, new ProfileFetchHandler() {
|
||||
@Override
|
||||
public void onResult(ProfileRequest request) {
|
||||
hasFetched = true;
|
||||
|
||||
switch (request.getResult()) {
|
||||
case NOT_FOUND:
|
||||
isValid = false;
|
||||
break;
|
||||
case TOO_MANY_REQUESTS:
|
||||
if (maxRetries == 0) {
|
||||
break;
|
||||
}
|
||||
fetchRetries++;
|
||||
long delay = Setting.NPC_SKIN_RETRY_DELAY.asLong();
|
||||
retryTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fetch();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
if (Messaging.isDebugging()) {
|
||||
Messaging.debug("Retrying skin fetch for '" + skinName + "' in " + delay + " ticks.");
|
||||
}
|
||||
break;
|
||||
case SUCCESS:
|
||||
GameProfile profile = request.getProfile();
|
||||
setData(profile);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the player the skin belongs to.
|
||||
*
|
||||
* @return The skin ID or null if it has not been retrieved yet or the skin is invalid.
|
||||
*/
|
||||
@Nullable
|
||||
public UUID getSkinId() {
|
||||
return skinId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the skin.
|
||||
*/
|
||||
public String getSkinName() {
|
||||
return skinName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the skin data has been retrieved.
|
||||
*/
|
||||
public boolean hasSkinData() {
|
||||
return skinData != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the skin is valid.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private void setData(@Nullable GameProfile profile) {
|
||||
if (profile == null) {
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profile.getName().toLowerCase().equals(skinName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"GameProfile name (" + profile.getName() + ") and " + "skin name (" + skinName + ") do not match.");
|
||||
}
|
||||
|
||||
skinId = profile.getId();
|
||||
skinData = Iterables.getFirst(profile.getProperties().get("textures"), null);
|
||||
|
||||
List<SkinnableEntity> entities = new ArrayList<SkinnableEntity>(pending.keySet());
|
||||
for (SkinnableEntity entity : entities) {
|
||||
applyAndRespawn(entity);
|
||||
}
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached skins.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
synchronized (CACHE) {
|
||||
for (Skin skin : CACHE.values()) {
|
||||
skin.pending.clear();
|
||||
if (skin.retryTask != null) {
|
||||
skin.retryTask.cancel();
|
||||
}
|
||||
}
|
||||
CACHE.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a skin for a skinnable entity.
|
||||
*
|
||||
* <p>
|
||||
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
|
||||
* </p>
|
||||
*
|
||||
* @param entity
|
||||
* The skinnable entity.
|
||||
*/
|
||||
public static Skin get(SkinnableEntity entity) {
|
||||
return get(entity, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a skin for a skinnable entity.
|
||||
*
|
||||
* <p>
|
||||
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
|
||||
* </p>
|
||||
*
|
||||
* @param entity
|
||||
* The skinnable entity.
|
||||
* @param forceUpdate
|
||||
* if the skin should be checked via the cache
|
||||
*/
|
||||
public static Skin get(SkinnableEntity entity, boolean forceUpdate) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
String skinName = entity.getSkinName().toLowerCase();
|
||||
return get(skinName, forceUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player skin.
|
||||
*
|
||||
* <p>
|
||||
* If a Skin instance does not exist, a new one is created and the skin data is automatically fetched.
|
||||
* </p>
|
||||
*
|
||||
* @param skinName
|
||||
* The name of the skin.
|
||||
*/
|
||||
public static Skin get(String skinName, boolean forceUpdate) {
|
||||
Preconditions.checkNotNull(skinName);
|
||||
|
||||
skinName = skinName.toLowerCase();
|
||||
|
||||
Skin skin;
|
||||
synchronized (CACHE) {
|
||||
skin = CACHE.get(skinName);
|
||||
}
|
||||
|
||||
if (skin == null) {
|
||||
skin = new Skin(skinName);
|
||||
} else if (forceUpdate) {
|
||||
skin.fetch();
|
||||
}
|
||||
|
||||
return skin;
|
||||
}
|
||||
|
||||
private static void setNPCSkinData(SkinnableEntity entity, String skinName, UUID skinId, Property skinProperty) {
|
||||
NPC npc = entity.getNPC();
|
||||
|
||||
// cache skins for faster initial skin availability and
|
||||
// for use when the latest skin is not required.
|
||||
npc.data().setPersistent(CACHED_SKIN_UUID_NAME_METADATA, skinName);
|
||||
npc.data().setPersistent(CACHED_SKIN_UUID_METADATA, skinId.toString());
|
||||
if (skinProperty.getValue() != null) {
|
||||
npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA, skinProperty.getValue());
|
||||
npc.data().setPersistent(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA, skinProperty.getSignature());
|
||||
setNPCTexture(entity, skinProperty);
|
||||
} else {
|
||||
npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_METADATA);
|
||||
npc.data().remove(NPC.PLAYER_SKIN_TEXTURE_PROPERTIES_SIGN_METADATA);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNPCTexture(SkinnableEntity entity, Property skinProperty) {
|
||||
GameProfile profile = entity.getProfile();
|
||||
|
||||
// don't set property if already set since this sometimes causes
|
||||
// packet errors that disconnect the client.
|
||||
Property current = Iterables.getFirst(profile.getProperties().get("textures"), null);
|
||||
if (current != null && current.getValue().equals(skinProperty.getValue())
|
||||
&& (current.getSignature() != null && current.getSignature().equals(skinProperty.getSignature()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
profile.getProperties().removeAll("textures"); // ensure client does not crash due to duplicate properties.
|
||||
profile.getProperties().put("textures", skinProperty);
|
||||
}
|
||||
|
||||
private static final Map<String, Skin> CACHE = new HashMap<String, Skin>(20);
|
||||
public static final String CACHED_SKIN_UUID_METADATA = "cached-skin-uuid";
|
||||
public static final String CACHED_SKIN_UUID_NAME_METADATA = "cached-skin-uuid-name";
|
||||
}
|
255
main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal file
255
main/java/net/citizensnpcs/npc/skin/SkinPacketTracker.java
Normal file
@ -0,0 +1,255 @@
|
||||
package net.citizensnpcs.npc.skin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import net.citizensnpcs.Settings;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
/**
|
||||
* Handles and synchronizes add and remove packets for Player type NPC's in order to properly apply the NPC skin.
|
||||
*
|
||||
* <p>
|
||||
* Used as one instance per NPC entity.
|
||||
* </p>
|
||||
*/
|
||||
public class SkinPacketTracker {
|
||||
private final SkinnableEntity entity;
|
||||
private final Map<UUID, PlayerEntry> inProgress = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
|
||||
|
||||
private boolean isRemoved;
|
||||
private Skin skin;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param entity
|
||||
* The skinnable entity the instance belongs to.
|
||||
*/
|
||||
public SkinPacketTracker(SkinnableEntity entity) {
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
this.entity = entity;
|
||||
this.skin = Skin.get(entity);
|
||||
|
||||
if (LISTENER == null) {
|
||||
LISTENER = new PlayerListener();
|
||||
Bukkit.getPluginManager().registerEvents(LISTENER, CitizensAPI.getPlugin());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NPC skin.
|
||||
*/
|
||||
public Skin getSkin() {
|
||||
return skin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the tracker that a remove packet has been sent to the specified player.
|
||||
*
|
||||
* @param playerId
|
||||
* The ID of the player.
|
||||
*/
|
||||
void notifyRemovePacketCancelled(UUID playerId) {
|
||||
inProgress.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the tracker that a remove packet has been sent to the specified player.
|
||||
*
|
||||
* @param playerId
|
||||
* The ID of the player.
|
||||
*/
|
||||
void notifyRemovePacketSent(UUID playerId) {
|
||||
PlayerEntry entry = inProgress.get(playerId);
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (entry.removeCount == 0)
|
||||
return;
|
||||
|
||||
entry.removeCount -= 1;
|
||||
if (entry.removeCount == 0) {
|
||||
inProgress.remove(playerId);
|
||||
} else {
|
||||
scheduleRemovePacket(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that the NPC skin has been changed.
|
||||
*/
|
||||
public void notifySkinChange(boolean forceUpdate) {
|
||||
this.skin = Skin.get(entity, forceUpdate);
|
||||
skin.applyAndRespawn(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when the NPC entity is removed.
|
||||
*
|
||||
* <p>
|
||||
* Sends remove packets to all players.
|
||||
* </p>
|
||||
*/
|
||||
public void onRemoveNPC() {
|
||||
isRemoved = true;
|
||||
|
||||
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
|
||||
|
||||
for (Player player : players) {
|
||||
|
||||
if (player.hasMetadata("NPC"))
|
||||
continue;
|
||||
|
||||
// send packet now and later to ensure removal from player list
|
||||
NMS.sendTabListRemove(player, entity.getBukkitEntity());
|
||||
TAB_LIST_REMOVER.sendPacket(player, entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when the NPC entity is spawned.
|
||||
*/
|
||||
public void onSpawnNPC() {
|
||||
isRemoved = false;
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!entity.getNPC().isSpawned())
|
||||
return;
|
||||
|
||||
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
|
||||
updateNearbyViewers(viewDistance);
|
||||
}
|
||||
}.runTaskLater(CitizensAPI.getPlugin(), 20);
|
||||
}
|
||||
|
||||
private void scheduleRemovePacket(final PlayerEntry entry) {
|
||||
if (isRemoved)
|
||||
return;
|
||||
|
||||
entry.removeTask = Bukkit.getScheduler().runTaskLater(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (shouldRemoveFromTabList()) {
|
||||
TAB_LIST_REMOVER.sendPacket(entry.player, entity);
|
||||
}
|
||||
}
|
||||
}, PACKET_DELAY_REMOVE);
|
||||
}
|
||||
|
||||
private void scheduleRemovePacket(PlayerEntry entry, int count) {
|
||||
if (!shouldRemoveFromTabList())
|
||||
return;
|
||||
|
||||
entry.removeCount = count;
|
||||
scheduleRemovePacket(entry);
|
||||
}
|
||||
|
||||
private boolean shouldRemoveFromTabList() {
|
||||
return entity.getNPC().data().get("removefromtablist", Settings.Setting.DISABLE_TABLIST.asBoolean());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send skin related packets to all nearby players within the specified block radius.
|
||||
*
|
||||
* @param radius
|
||||
* The radius.
|
||||
*/
|
||||
public void updateNearbyViewers(double radius) {
|
||||
radius *= radius;
|
||||
|
||||
org.bukkit.World world = entity.getBukkitEntity().getWorld();
|
||||
Player from = entity.getBukkitEntity();
|
||||
Location location = from.getLocation();
|
||||
|
||||
for (Player player : world.getPlayers()) {
|
||||
if (player == null || player.hasMetadata("NPC"))
|
||||
continue;
|
||||
|
||||
player.getLocation(CACHE_LOCATION);
|
||||
if (!player.canSee(from) || !location.getWorld().equals(CACHE_LOCATION.getWorld()))
|
||||
continue;
|
||||
|
||||
if (location.distanceSquared(CACHE_LOCATION) > radius)
|
||||
continue;
|
||||
|
||||
updateViewer(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send skin related packets to a player.
|
||||
*
|
||||
* @param player
|
||||
* The player.
|
||||
*/
|
||||
public void updateViewer(final Player player) {
|
||||
Preconditions.checkNotNull(player);
|
||||
|
||||
if (isRemoved || player.hasMetadata("NPC"))
|
||||
return;
|
||||
|
||||
PlayerEntry entry = inProgress.get(player.getUniqueId());
|
||||
if (entry != null) {
|
||||
entry.cancel();
|
||||
} else {
|
||||
entry = new PlayerEntry(player);
|
||||
}
|
||||
|
||||
TAB_LIST_REMOVER.cancelPackets(player, entity);
|
||||
|
||||
inProgress.put(player.getUniqueId(), entry);
|
||||
skin.apply(entity);
|
||||
NMS.sendTabListAdd(player, entity.getBukkitEntity());
|
||||
|
||||
scheduleRemovePacket(entry, 2);
|
||||
}
|
||||
|
||||
private class PlayerEntry {
|
||||
Player player;
|
||||
int removeCount;
|
||||
BukkitTask removeTask;
|
||||
|
||||
PlayerEntry(Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
// cancel previous packet tasks so they do not interfere with
|
||||
// new tasks
|
||||
void cancel() {
|
||||
if (removeTask != null)
|
||||
removeTask.cancel();
|
||||
removeCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PlayerListener implements Listener {
|
||||
@EventHandler
|
||||
private void onPlayerQuit(PlayerQuitEvent event) {
|
||||
// this also causes any entries in the "inProgress" field to
|
||||
// be removed.
|
||||
TAB_LIST_REMOVER.cancelPackets(event.getPlayer());
|
||||
}
|
||||
}
|
||||
|
||||
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
|
||||
private static PlayerListener LISTENER;
|
||||
private static final int PACKET_DELAY_REMOVE = 1;
|
||||
private static final TabListRemover TAB_LIST_REMOVER = new TabListRemover();
|
||||
}
|
481
main/java/net/citizensnpcs/npc/skin/SkinUpdateTracker.java
Normal file
481
main/java/net/citizensnpcs/npc/skin/SkinUpdateTracker.java
Normal file
@ -0,0 +1,481 @@
|
||||
package net.citizensnpcs.npc.skin;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import net.citizensnpcs.Settings;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.npc.NPCRegistry;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
/**
|
||||
* Tracks skin updates for players.
|
||||
*
|
||||
* @see net.citizensnpcs.EventListen
|
||||
*/
|
||||
public class SkinUpdateTracker {
|
||||
|
||||
private final Map<SkinnableEntity, Void> navigating = new WeakHashMap<SkinnableEntity, Void>(25);
|
||||
private final NPCRegistry npcRegistry;
|
||||
private final Map<UUID, PlayerTracker> playerTrackers = new HashMap<UUID, PlayerTracker>(
|
||||
Bukkit.getMaxPlayers() / 2);
|
||||
private final Map<String, NPCRegistry> registries;
|
||||
private final NPCNavigationUpdater updater = new NPCNavigationUpdater();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param npcRegistry
|
||||
* The primary citizens registry.
|
||||
* @param registries
|
||||
* Map of other registries.
|
||||
*/
|
||||
public SkinUpdateTracker(NPCRegistry npcRegistry, Map<String, NPCRegistry> registries) {
|
||||
Preconditions.checkNotNull(npcRegistry);
|
||||
Preconditions.checkNotNull(registries);
|
||||
|
||||
this.npcRegistry = npcRegistry;
|
||||
this.registries = registries;
|
||||
|
||||
updater.runTaskTimer(CitizensAPI.getPlugin(), 1, 1);
|
||||
new NPCNavigationTracker().runTaskTimer(CitizensAPI.getPlugin(), 3, 7);
|
||||
}
|
||||
|
||||
// determines if a player is near a skinnable entity and, if checkFov set, if the
|
||||
// skinnable entity is within the players field of view.
|
||||
private boolean canSee(Player player, SkinnableEntity skinnable, boolean checkFov) {
|
||||
Player entity = skinnable.getBukkitEntity();
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
if (!player.canSee(entity))
|
||||
return false;
|
||||
|
||||
if (!player.getWorld().equals(entity.getWorld()))
|
||||
return false;
|
||||
|
||||
Location playerLoc = player.getLocation(CACHE_LOCATION);
|
||||
Location skinLoc = entity.getLocation(NPC_LOCATION);
|
||||
|
||||
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
|
||||
viewDistance *= viewDistance;
|
||||
|
||||
if (playerLoc.distanceSquared(skinLoc) > viewDistance)
|
||||
return false;
|
||||
|
||||
// see if the NPC is within the players field of view
|
||||
if (checkFov) {
|
||||
double deltaX = skinLoc.getX() - playerLoc.getX();
|
||||
double deltaZ = skinLoc.getZ() - playerLoc.getZ();
|
||||
double angle = Math.atan2(deltaX, deltaZ);
|
||||
float skinYaw = Util.clampYaw(-(float) Math.toDegrees(angle));
|
||||
float playerYaw = Util.clampYaw(playerLoc.getYaw());
|
||||
float upperBound = Util.clampYaw(playerYaw + FIELD_OF_VIEW);
|
||||
float lowerBound = Util.clampYaw(playerYaw - FIELD_OF_VIEW);
|
||||
if (upperBound == -180.0 && playerYaw > 0) {
|
||||
upperBound = 0;
|
||||
}
|
||||
boolean hasMoved;
|
||||
if (playerYaw - 90 < -180 || playerYaw + 90 > 180) {
|
||||
hasMoved = skinYaw > lowerBound && skinYaw < upperBound;
|
||||
} else {
|
||||
hasMoved = skinYaw < lowerBound || skinYaw > upperBound;
|
||||
}
|
||||
return hasMoved;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Iterable<NPC> getAllNPCs() {
|
||||
return Iterables.filter(Iterables.concat(npcRegistry, Iterables.concat(registries.values())),
|
||||
Predicates.notNull());
|
||||
}
|
||||
|
||||
private List<SkinnableEntity> getNearbyNPCs(Player player, boolean reset, boolean checkFov) {
|
||||
List<SkinnableEntity> results = new ArrayList<SkinnableEntity>();
|
||||
PlayerTracker tracker = getTracker(player, reset);
|
||||
for (NPC npc : getAllNPCs()) {
|
||||
|
||||
SkinnableEntity skinnable = getSkinnable(npc);
|
||||
if (skinnable == null)
|
||||
continue;
|
||||
|
||||
// if checking field of view, don't add skins that have already been updated for FOV
|
||||
if (checkFov && tracker.fovVisibleSkins.contains(skinnable))
|
||||
continue;
|
||||
|
||||
if (canSee(player, skinnable, checkFov)) {
|
||||
results.add(skinnable);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// get all navigating skinnable NPC's within the players FOV that have not been "seen" yet
|
||||
private void getNewVisibleNavigating(Player player, Collection<SkinnableEntity> output) {
|
||||
PlayerTracker tracker = getTracker(player, false);
|
||||
|
||||
for (SkinnableEntity skinnable : navigating.keySet()) {
|
||||
|
||||
// make sure player hasn't already been updated to prevent excessive tab list flashing
|
||||
// while NPC's are navigating and to reduce the number of times #canSee is invoked.
|
||||
if (tracker.fovVisibleSkins.contains(skinnable))
|
||||
continue;
|
||||
|
||||
if (canSee(player, skinnable, true))
|
||||
output.add(skinnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private SkinnableEntity getSkinnable(NPC npc) {
|
||||
Entity entity = npc.getEntity();
|
||||
if (entity == null)
|
||||
return null;
|
||||
|
||||
return entity instanceof SkinnableEntity ? (SkinnableEntity) entity : null;
|
||||
}
|
||||
|
||||
// get a players tracker, create new one if not exists.
|
||||
private PlayerTracker getTracker(Player player, boolean reset) {
|
||||
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
|
||||
if (tracker == null) {
|
||||
tracker = new PlayerTracker(player);
|
||||
playerTrackers.put(player.getUniqueId(), tracker);
|
||||
} else if (reset) {
|
||||
tracker.hardReset(player);
|
||||
}
|
||||
return tracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when an NPC is despawned.
|
||||
*
|
||||
* @param npc
|
||||
* The despawned NPC.
|
||||
*/
|
||||
public void onNPCDespawn(NPC npc) {
|
||||
Preconditions.checkNotNull(npc);
|
||||
SkinnableEntity skinnable = getSkinnable(npc);
|
||||
if (skinnable == null)
|
||||
return;
|
||||
|
||||
navigating.remove(skinnable);
|
||||
|
||||
for (PlayerTracker tracker : playerTrackers.values()) {
|
||||
tracker.fovVisibleSkins.remove(skinnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when an NPC begins navigating.
|
||||
*
|
||||
* @param npc
|
||||
* The navigating NPC.
|
||||
*/
|
||||
public void onNPCNavigationBegin(NPC npc) {
|
||||
Preconditions.checkNotNull(npc);
|
||||
SkinnableEntity skinnable = getSkinnable(npc);
|
||||
if (skinnable == null)
|
||||
return;
|
||||
|
||||
navigating.put(skinnable, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when an NPC finishes navigating.
|
||||
*
|
||||
* @param npc
|
||||
* The finished NPC.
|
||||
*/
|
||||
public void onNPCNavigationComplete(NPC npc) {
|
||||
Preconditions.checkNotNull(npc);
|
||||
SkinnableEntity skinnable = getSkinnable(npc);
|
||||
if (skinnable == null)
|
||||
return;
|
||||
|
||||
navigating.remove(skinnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when an NPC is spawned.
|
||||
*
|
||||
* @param npc
|
||||
* The spawned NPC.
|
||||
*/
|
||||
public void onNPCSpawn(NPC npc) {
|
||||
Preconditions.checkNotNull(npc);
|
||||
SkinnableEntity skinnable = getSkinnable(npc);
|
||||
if (skinnable == null)
|
||||
return;
|
||||
|
||||
// reset nearby players in case they are not looking at the NPC when it spawns.
|
||||
resetNearbyPlayers(skinnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke when a player moves.
|
||||
*
|
||||
* @param player
|
||||
* The player that moved.
|
||||
*/
|
||||
public void onPlayerMove(Player player) {
|
||||
Preconditions.checkNotNull(player);
|
||||
PlayerTracker updateTracker = playerTrackers.get(player.getUniqueId());
|
||||
if (updateTracker == null)
|
||||
return;
|
||||
|
||||
if (!updateTracker.shouldUpdate(player))
|
||||
return;
|
||||
|
||||
updatePlayer(player, 10, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a player from the tracker.
|
||||
*
|
||||
* <p>
|
||||
* Used when the player logs out.
|
||||
* </p>
|
||||
*
|
||||
* @param playerId
|
||||
* The ID of the player.
|
||||
*/
|
||||
public void removePlayer(UUID playerId) {
|
||||
Preconditions.checkNotNull(playerId);
|
||||
playerTrackers.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all players currently being tracked.
|
||||
*
|
||||
* <p>
|
||||
* Used when Citizens is reloaded.
|
||||
* </p>
|
||||
*/
|
||||
public void reset() {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.hasMetadata("NPC"))
|
||||
continue;
|
||||
|
||||
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
|
||||
if (tracker == null)
|
||||
continue;
|
||||
|
||||
tracker.hardReset(player);
|
||||
}
|
||||
}
|
||||
|
||||
// hard reset players near a skinnable NPC
|
||||
private void resetNearbyPlayers(SkinnableEntity skinnable) {
|
||||
Entity entity = skinnable.getBukkitEntity();
|
||||
if (entity == null || !entity.isValid())
|
||||
return;
|
||||
|
||||
double viewDistance = Settings.Setting.NPC_SKIN_VIEW_DISTANCE.asDouble();
|
||||
viewDistance *= viewDistance;
|
||||
Location location = entity.getLocation(NPC_LOCATION);
|
||||
List<Player> players = entity.getWorld().getPlayers();
|
||||
for (Player player : players) {
|
||||
if (player.hasMetadata("NPC"))
|
||||
continue;
|
||||
Location ploc = player.getLocation(CACHE_LOCATION);
|
||||
if (ploc.getWorld() != location.getWorld())
|
||||
continue;
|
||||
double distanceSquared = ploc.distanceSquared(location);
|
||||
if (distanceSquared > viewDistance)
|
||||
continue;
|
||||
|
||||
PlayerTracker tracker = playerTrackers.get(player.getUniqueId());
|
||||
if (tracker != null) {
|
||||
tracker.hardReset(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a player with skin related packets from nearby skinnable NPC's.
|
||||
*
|
||||
* @param player
|
||||
* The player to update.
|
||||
* @param delay
|
||||
* The delay before sending the packets.
|
||||
* @param reset
|
||||
* True to hard reset the players tracking info, otherwise false.
|
||||
*/
|
||||
public void updatePlayer(final Player player, long delay, final boolean reset) {
|
||||
if (player.hasMetadata("NPC"))
|
||||
return;
|
||||
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<SkinnableEntity> visible = getNearbyNPCs(player, reset, false);
|
||||
for (SkinnableEntity skinnable : visible) {
|
||||
skinnable.getSkinTracker().updateViewer(player);
|
||||
}
|
||||
}
|
||||
}.runTaskLater(CitizensAPI.getPlugin(), delay);
|
||||
}
|
||||
|
||||
// update players when the NPC navigates into their field of view
|
||||
private class NPCNavigationTracker extends BukkitRunnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (navigating.isEmpty() || playerTrackers.isEmpty())
|
||||
return;
|
||||
|
||||
List<SkinnableEntity> nearby = new ArrayList<SkinnableEntity>(10);
|
||||
Collection<? extends Player> players = Bukkit.getOnlinePlayers();
|
||||
|
||||
for (Player player : players) {
|
||||
if (player.hasMetadata("NPC"))
|
||||
continue;
|
||||
|
||||
getNewVisibleNavigating(player, nearby);
|
||||
|
||||
for (SkinnableEntity skinnable : nearby) {
|
||||
PlayerTracker tracker = getTracker(player, false);
|
||||
tracker.fovVisibleSkins.add(skinnable);
|
||||
updater.queue.offer(new UpdateInfo(player, skinnable));
|
||||
}
|
||||
|
||||
nearby.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updates players. Repeating task used to schedule updates without
|
||||
// causing excessive scheduling.
|
||||
private class NPCNavigationUpdater extends BukkitRunnable {
|
||||
Queue<UpdateInfo> queue = new ArrayDeque<UpdateInfo>(20);
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!queue.isEmpty()) {
|
||||
UpdateInfo info = queue.remove();
|
||||
info.entity.getSkinTracker().updateViewer(info.player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks player location and yaw to determine when the player should be updated
|
||||
// with nearby skins.
|
||||
private class PlayerTracker {
|
||||
final Set<SkinnableEntity> fovVisibleSkins = new HashSet<SkinnableEntity>(20);
|
||||
boolean hasMoved;
|
||||
final Location location = new Location(null, 0, 0, 0);
|
||||
float lowerBound;
|
||||
int rotationCount;
|
||||
float startYaw;
|
||||
float upperBound;
|
||||
|
||||
PlayerTracker(Player player) {
|
||||
hardReset(player);
|
||||
}
|
||||
|
||||
// reset all
|
||||
void hardReset(Player player) {
|
||||
this.hasMoved = false;
|
||||
this.rotationCount = 0;
|
||||
this.lowerBound = this.upperBound = this.startYaw = 0;
|
||||
this.fovVisibleSkins.clear();
|
||||
reset(player);
|
||||
}
|
||||
|
||||
// resets initial yaw and location to the players current location and yaw.
|
||||
void reset(Player player) {
|
||||
player.getLocation(this.location);
|
||||
if (rotationCount < 3) {
|
||||
float rotationDegrees = Settings.Setting.NPC_SKIN_ROTATION_UPDATE_DEGREES.asFloat();
|
||||
float yaw = Util.clampYaw(this.location.getYaw());
|
||||
this.startYaw = yaw;
|
||||
this.upperBound = Util.clampYaw(yaw + rotationDegrees);
|
||||
this.lowerBound = Util.clampYaw(yaw - rotationDegrees);
|
||||
if (upperBound == -180.0 && startYaw > 0) {
|
||||
upperBound = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean shouldUpdate(Player player) {
|
||||
Location currentLoc = player.getLocation(CACHE_LOCATION);
|
||||
|
||||
if (!hasMoved) {
|
||||
hasMoved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rotationCount < 3) {
|
||||
float yaw = Util.clampYaw(currentLoc.getYaw());
|
||||
boolean hasRotated;
|
||||
if (startYaw - 90 < -180 || startYaw + 90 > 180) {
|
||||
hasRotated = yaw > lowerBound && yaw < upperBound;
|
||||
} else {
|
||||
hasRotated = yaw < lowerBound || yaw > upperBound;
|
||||
}
|
||||
|
||||
// update the first 3 times the player rotates. helps load skins around player
|
||||
// after the player logs/teleports.
|
||||
if (hasRotated) {
|
||||
rotationCount++;
|
||||
reset(player);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure player is in same world
|
||||
if (!currentLoc.getWorld().equals(this.location.getWorld())) {
|
||||
reset(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
// update every time a player moves a certain distance
|
||||
double distance = currentLoc.distanceSquared(this.location);
|
||||
if (distance > MOVEMENT_SKIN_UPDATE_DISTANCE) {
|
||||
reset(player);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateInfo {
|
||||
SkinnableEntity entity;
|
||||
Player player;
|
||||
|
||||
UpdateInfo(Player player, SkinnableEntity entity) {
|
||||
this.player = player;
|
||||
this.entity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
|
||||
private static final float FIELD_OF_VIEW = 70f;
|
||||
private static final int MOVEMENT_SKIN_UPDATE_DISTANCE = 50 * 50;
|
||||
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
|
||||
}
|
59
main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java
Normal file
59
main/java/net/citizensnpcs/npc/skin/SkinnableEntity.java
Normal file
@ -0,0 +1,59 @@
|
||||
package net.citizensnpcs.npc.skin;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
import net.citizensnpcs.npc.ai.NPCHolder;
|
||||
|
||||
/**
|
||||
* Interface for player entities that are skinnable.
|
||||
*/
|
||||
public interface SkinnableEntity extends NPCHolder {
|
||||
|
||||
/**
|
||||
* Get the bukkit entity.
|
||||
*/
|
||||
Player getBukkitEntity();
|
||||
|
||||
/**
|
||||
* Get entity game profile.
|
||||
*/
|
||||
GameProfile getProfile();
|
||||
|
||||
/**
|
||||
* Get the name of the player whose skin the NPC uses.
|
||||
*/
|
||||
String getSkinName();
|
||||
|
||||
/**
|
||||
* Get the entities skin packet tracker.
|
||||
*/
|
||||
SkinPacketTracker getSkinTracker();
|
||||
|
||||
/**
|
||||
* Set the bit flags that represent the skin layer parts visibility.
|
||||
*
|
||||
* <p>
|
||||
* Setting the skin flags automatically updates the NPC skin.
|
||||
* </p>
|
||||
*
|
||||
* @param flags
|
||||
* The bit flags.
|
||||
*/
|
||||
void setSkinFlags(byte flags);
|
||||
|
||||
/**
|
||||
* Set the name of the player whose skin the NPC uses.
|
||||
*
|
||||
* <p>
|
||||
* Setting the skin name automatically updates and respawn the NPC.
|
||||
* </p>
|
||||
*
|
||||
* @param name
|
||||
* The skin name.
|
||||
*/
|
||||
void setSkinName(String name);
|
||||
|
||||
void setSkinName(String skinName, boolean forceUpdate);
|
||||
}
|
158
main/java/net/citizensnpcs/npc/skin/TabListRemover.java
Normal file
158
main/java/net/citizensnpcs/npc/skin/TabListRemover.java
Normal file
@ -0,0 +1,158 @@
|
||||
package net.citizensnpcs.npc.skin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import net.citizensnpcs.Settings;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
/**
|
||||
* Sends remove packets in batch per player.
|
||||
*
|
||||
* <p>
|
||||
* Collects entities to remove and sends them all to the player in a single packet.
|
||||
* </p>
|
||||
*/
|
||||
public class TabListRemover {
|
||||
private final Map<UUID, PlayerEntry> pending = new HashMap<UUID, PlayerEntry>(Bukkit.getMaxPlayers() / 2);
|
||||
|
||||
TabListRemover() {
|
||||
Bukkit.getScheduler().runTaskTimer(CitizensAPI.getPlugin(), new Sender(), 2, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel packets pending to be sent to the specified player.
|
||||
*
|
||||
* @param player
|
||||
* The player.
|
||||
*/
|
||||
public void cancelPackets(Player player) {
|
||||
Preconditions.checkNotNull(player);
|
||||
|
||||
PlayerEntry entry = pending.remove(player.getUniqueId());
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
for (SkinnableEntity entity : entry.toRemove) {
|
||||
entity.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel packets pending to be sent to the specified player for the specified skinnable entity.
|
||||
*
|
||||
* @param player
|
||||
* The player.
|
||||
* @param skinnable
|
||||
* The skinnable entity.
|
||||
*/
|
||||
public void cancelPackets(Player player, SkinnableEntity skinnable) {
|
||||
Preconditions.checkNotNull(player);
|
||||
Preconditions.checkNotNull(skinnable);
|
||||
|
||||
PlayerEntry entry = pending.get(player.getUniqueId());
|
||||
if (entry == null)
|
||||
return;
|
||||
|
||||
if (entry.toRemove.remove(skinnable)) {
|
||||
skinnable.getSkinTracker().notifyRemovePacketCancelled(player.getUniqueId());
|
||||
}
|
||||
|
||||
if (entry.toRemove.isEmpty())
|
||||
pending.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
private PlayerEntry getEntry(Player player) {
|
||||
|
||||
PlayerEntry entry = pending.get(player.getUniqueId());
|
||||
if (entry == null) {
|
||||
entry = new PlayerEntry(player);
|
||||
pending.put(player.getUniqueId(), entry);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a remove packet to the specified player for the specified skinnable entity.
|
||||
*
|
||||
* @param player
|
||||
* The player to send the packet to.
|
||||
* @param entity
|
||||
* The entity to remove.
|
||||
*/
|
||||
public void sendPacket(Player player, SkinnableEntity entity) {
|
||||
Preconditions.checkNotNull(player);
|
||||
Preconditions.checkNotNull(entity);
|
||||
|
||||
PlayerEntry entry = getEntry(player);
|
||||
|
||||
entry.toRemove.add(entity);
|
||||
}
|
||||
|
||||
private class PlayerEntry {
|
||||
Player player;
|
||||
Set<SkinnableEntity> toRemove = new HashSet<SkinnableEntity>(25);
|
||||
|
||||
PlayerEntry(Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
}
|
||||
|
||||
private class Sender implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
int maxPacketEntries = Settings.Setting.MAX_PACKET_ENTRIES.asInt();
|
||||
|
||||
Iterator<Map.Entry<UUID, PlayerEntry>> entryIterator = pending.entrySet().iterator();
|
||||
while (entryIterator.hasNext()) {
|
||||
|
||||
Map.Entry<UUID, PlayerEntry> mapEntry = entryIterator.next();
|
||||
PlayerEntry entry = mapEntry.getValue();
|
||||
|
||||
int listSize = Math.min(maxPacketEntries, entry.toRemove.size());
|
||||
boolean sendAll = listSize == entry.toRemove.size();
|
||||
|
||||
List<SkinnableEntity> skinnableList = new ArrayList<SkinnableEntity>(listSize);
|
||||
|
||||
int i = 0;
|
||||
Iterator<SkinnableEntity> skinIterator = entry.toRemove.iterator();
|
||||
while (skinIterator.hasNext()) {
|
||||
|
||||
if (i >= maxPacketEntries)
|
||||
break;
|
||||
|
||||
SkinnableEntity skinnable = skinIterator.next();
|
||||
skinnableList.add(skinnable);
|
||||
|
||||
skinIterator.remove();
|
||||
i++;
|
||||
}
|
||||
|
||||
if (entry.player.isOnline())
|
||||
NMS.sendTabListRemove(entry.player, skinnableList);
|
||||
|
||||
// notify skin trackers that a remove packet has been sent to a player
|
||||
for (SkinnableEntity entity : skinnableList) {
|
||||
entity.getSkinTracker().notifyRemovePacketSent(entry.player.getUniqueId());
|
||||
}
|
||||
|
||||
if (sendAll)
|
||||
entryIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
main/java/net/citizensnpcs/trait/Age.java
Normal file
69
main/java/net/citizensnpcs/trait/Age.java
Normal file
@ -0,0 +1,69 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Ageable;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
@TraitName("age")
|
||||
public class Age extends Trait implements Toggleable {
|
||||
@Persist
|
||||
private int age = 0;
|
||||
private Ageable ageable;
|
||||
@Persist
|
||||
private boolean locked = true;
|
||||
|
||||
public Age() {
|
||||
super("age");
|
||||
}
|
||||
|
||||
public void describe(CommandSender sender) {
|
||||
Messaging.sendTr(sender, Messages.AGE_TRAIT_DESCRIPTION, npc.getName(), age, locked);
|
||||
}
|
||||
|
||||
private boolean isAgeable() {
|
||||
return ageable != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Ageable) {
|
||||
Ageable entity = (Ageable) npc.getEntity();
|
||||
entity.setAge(age);
|
||||
entity.setAgeLock(locked);
|
||||
ageable = entity;
|
||||
} else
|
||||
ageable = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!locked && isAgeable()) {
|
||||
age = ageable.getAge();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
if (isAgeable()) {
|
||||
ageable.setAge(age);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
locked = !locked;
|
||||
if (isAgeable())
|
||||
ageable.setAgeLock(locked);
|
||||
return locked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Age{age=" + age + ",locked=" + locked + "}";
|
||||
}
|
||||
}
|
87
main/java/net/citizensnpcs/trait/Anchors.java
Normal file
87
main/java/net/citizensnpcs/trait/Anchors.java
Normal file
@ -0,0 +1,87 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Anchor;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
@TraitName("anchors")
|
||||
public class Anchors extends Trait {
|
||||
private final List<Anchor> anchors = new ArrayList<Anchor>();
|
||||
|
||||
public Anchors() {
|
||||
super("anchors");
|
||||
}
|
||||
|
||||
public boolean addAnchor(String name, Location location) {
|
||||
Anchor newAnchor = new Anchor(name, location);
|
||||
if (anchors.contains(newAnchor))
|
||||
return false;
|
||||
anchors.add(newAnchor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void checkWorld(WorldLoadEvent event) {
|
||||
for (Anchor anchor : anchors)
|
||||
if (!anchor.isLoaded())
|
||||
anchor.load();
|
||||
}
|
||||
|
||||
public Anchor getAnchor(String name) {
|
||||
for (Anchor anchor : anchors)
|
||||
if (anchor.getName().equalsIgnoreCase(name))
|
||||
return anchor;
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Anchor> getAnchors() {
|
||||
return anchors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
for (DataKey sub : key.getRelative("list").getIntegerSubKeys()) {
|
||||
String[] parts = sub.getString("").split(";");
|
||||
Location location;
|
||||
try {
|
||||
location = new Location(Bukkit.getServer().getWorld(parts[1]), Double.valueOf(parts[2]),
|
||||
Double.valueOf(parts[3]), Double.valueOf(parts[4]));
|
||||
anchors.add(new Anchor(parts[0], location));
|
||||
} catch (NumberFormatException e) {
|
||||
Messaging.logTr(Messages.SKIPPING_INVALID_ANCHOR, sub.name(), e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
// Invalid world/location/etc. Still enough data to build an
|
||||
// unloaded anchor
|
||||
anchors.add(new Anchor(parts[0], sub.getString("").split(";", 2)[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeAnchor(Anchor anchor) {
|
||||
if (anchors.contains(anchor)) {
|
||||
anchors.remove(anchor);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.removeKey("list");
|
||||
for (int i = 0; i < anchors.size(); i++)
|
||||
key.setString("list." + String.valueOf(i), anchors.get(i).stringValue());
|
||||
}
|
||||
|
||||
}
|
108
main/java/net/citizensnpcs/trait/ArmorStandTrait.java
Normal file
108
main/java/net/citizensnpcs/trait/ArmorStandTrait.java
Normal file
@ -0,0 +1,108 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.ArmorStand;
|
||||
import org.bukkit.util.EulerAngle;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("armorstandtrait")
|
||||
public class ArmorStandTrait extends Trait {
|
||||
@Persist
|
||||
private EulerAngle body;
|
||||
@Persist
|
||||
private boolean gravity = true;
|
||||
@Persist
|
||||
private boolean hasarms = true;
|
||||
@Persist
|
||||
private boolean hasbaseplate = true;
|
||||
@Persist
|
||||
private EulerAngle head;
|
||||
@Persist
|
||||
private EulerAngle leftArm;
|
||||
@Persist
|
||||
private EulerAngle leftLeg;
|
||||
@Persist
|
||||
private boolean marker;
|
||||
@Persist
|
||||
private EulerAngle rightArm;
|
||||
@Persist
|
||||
private EulerAngle rightLeg;
|
||||
@Persist
|
||||
private boolean small;
|
||||
@Persist
|
||||
private boolean visible = true;
|
||||
|
||||
public ArmorStandTrait() {
|
||||
super("armorstandtrait");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof ArmorStand) {
|
||||
ArmorStand entity = (ArmorStand) npc.getEntity();
|
||||
if (leftArm != null) {
|
||||
entity.setLeftArmPose(leftArm);
|
||||
}
|
||||
if (leftLeg != null) {
|
||||
entity.setLeftLegPose(leftLeg);
|
||||
}
|
||||
if (rightArm != null) {
|
||||
entity.setRightArmPose(rightArm);
|
||||
}
|
||||
if (rightLeg != null) {
|
||||
entity.setRightLegPose(rightLeg);
|
||||
}
|
||||
if (body != null) {
|
||||
entity.setBodyPose(body);
|
||||
}
|
||||
if (head != null) {
|
||||
entity.setHeadPose(head);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (npc.getEntity() instanceof ArmorStand) {
|
||||
ArmorStand entity = (ArmorStand) npc.getEntity();
|
||||
body = entity.getBodyPose();
|
||||
leftArm = entity.getLeftArmPose();
|
||||
leftLeg = entity.getLeftLegPose();
|
||||
rightArm = entity.getRightArmPose();
|
||||
rightLeg = entity.getRightLegPose();
|
||||
head = entity.getHeadPose();
|
||||
entity.setVisible(visible);
|
||||
entity.setGravity(gravity);
|
||||
entity.setArms(hasarms);
|
||||
entity.setBasePlate(hasbaseplate);
|
||||
entity.setSmall(small);
|
||||
entity.setMarker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
public void setGravity(boolean gravity) {
|
||||
this.gravity = gravity;
|
||||
}
|
||||
|
||||
public void setHasArms(boolean arms) {
|
||||
this.hasarms = arms;
|
||||
}
|
||||
|
||||
public void setHasBaseplate(boolean baseplate) {
|
||||
this.hasbaseplate = baseplate;
|
||||
}
|
||||
|
||||
public void setMarker(boolean marker) {
|
||||
this.marker = marker;
|
||||
}
|
||||
|
||||
public void setSmall(boolean small) {
|
||||
this.small = small;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
74
main/java/net/citizensnpcs/trait/BossBarTrait.java
Normal file
74
main/java/net/citizensnpcs/trait/BossBarTrait.java
Normal file
@ -0,0 +1,74 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.boss.BarColor;
|
||||
import org.bukkit.boss.BarFlag;
|
||||
import org.bukkit.boss.BossBar;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
@TraitName("bossbar")
|
||||
public class BossBarTrait extends Trait {
|
||||
@Persist("color")
|
||||
private BarColor color = null;
|
||||
@Persist("flags")
|
||||
private List<BarFlag> flags = Lists.newArrayList();
|
||||
@Persist("title")
|
||||
private String title = null;
|
||||
@Persist("visible")
|
||||
private boolean visible = true;
|
||||
|
||||
public BossBarTrait() {
|
||||
super("bossbar");
|
||||
}
|
||||
|
||||
private boolean isBoss(Entity entity) {
|
||||
return entity.getType() == EntityType.ENDER_DRAGON || entity.getType() == EntityType.WITHER
|
||||
|| entity.getType() == EntityType.GUARDIAN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!npc.isSpawned() || !isBoss(npc.getEntity()))
|
||||
return;
|
||||
BossBar bar = NMS.getBossBar(npc.getEntity());
|
||||
bar.setVisible(visible);
|
||||
if (color != null) {
|
||||
bar.setColor(color);
|
||||
}
|
||||
if (title != null) {
|
||||
bar.setTitle(title);
|
||||
}
|
||||
for (BarFlag flag : BarFlag.values()) {
|
||||
bar.removeFlag(flag);
|
||||
}
|
||||
for (BarFlag flag : flags) {
|
||||
bar.addFlag(flag);
|
||||
}
|
||||
}
|
||||
|
||||
public void setColor(BarColor color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public void setFlags(Collection<BarFlag> flags) {
|
||||
this.flags = Lists.newArrayList(flags);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
}
|
367
main/java/net/citizensnpcs/trait/Controllable.java
Normal file
367
main/java/net/citizensnpcs/trait/Controllable.java
Normal file
@ -0,0 +1,367 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EnderDragon;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.command.CommandConfigurable;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.event.NPCRightClickEvent;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.trait.trait.Owner;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
@TraitName("controllable")
|
||||
public class Controllable extends Trait implements Toggleable, CommandConfigurable {
|
||||
private MovementController controller = new GroundController();
|
||||
@Persist
|
||||
private boolean enabled = true;
|
||||
private EntityType explicitType;
|
||||
@Persist("owner_required")
|
||||
private boolean ownerRequired;
|
||||
|
||||
public Controllable() {
|
||||
super("controllable");
|
||||
}
|
||||
|
||||
public Controllable(boolean enabled) {
|
||||
this();
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(CommandContext args) {
|
||||
if (args.hasFlag('f')) {
|
||||
explicitType = EntityType.BLAZE;
|
||||
} else if (args.hasFlag('g')) {
|
||||
explicitType = EntityType.OCELOT;
|
||||
} else if (args.hasFlag('o')) {
|
||||
explicitType = EntityType.UNKNOWN;
|
||||
} else if (args.hasFlag('r')) {
|
||||
explicitType = null;
|
||||
} else if (args.hasValueFlag("explicittype")) {
|
||||
explicitType = Util.matchEntityType(args.getFlag("explicittype"));
|
||||
}
|
||||
|
||||
if (npc.isSpawned()) {
|
||||
loadController();
|
||||
}
|
||||
}
|
||||
|
||||
private void enterOrLeaveVehicle(Player player) {
|
||||
List<Entity> passengers = NMS.getPassengers(player);
|
||||
if (passengers.size() > 0) {
|
||||
if (passengers.contains(player)) {
|
||||
player.leaveVehicle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ownerRequired && !npc.getTrait(Owner.class).isOwnedBy(player)) {
|
||||
return;
|
||||
}
|
||||
NMS.mount(npc.getEntity(), player);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
if (key.keyExists("explicittype")) {
|
||||
explicitType = Util.matchEntityType(key.getString("explicittype"));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadController() {
|
||||
EntityType type = npc.getEntity().getType();
|
||||
if (explicitType != null)
|
||||
type = explicitType;
|
||||
Class<? extends MovementController> clazz = controllerTypes.get(type);
|
||||
if (clazz == null) {
|
||||
controller = new GroundController();
|
||||
return;
|
||||
}
|
||||
Constructor<? extends MovementController> innerConstructor = null;
|
||||
try {
|
||||
innerConstructor = clazz.getConstructor(Controllable.class);
|
||||
innerConstructor.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
if (innerConstructor == null) {
|
||||
controller = clazz.newInstance();
|
||||
} else
|
||||
controller = innerConstructor.newInstance(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
controller = new GroundController();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean mount(Player toMount) {
|
||||
Entity passenger = npc.getEntity().getPassenger();
|
||||
if (passenger != null && passenger != toMount) {
|
||||
return false;
|
||||
}
|
||||
enterOrLeaveVehicle(toMount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (!npc.isSpawned() || !enabled)
|
||||
return;
|
||||
Action performed = event.getAction();
|
||||
if (NMS.getPassengers(npc.getEntity()).contains(npc.getEntity()))
|
||||
return;
|
||||
switch (performed) {
|
||||
case RIGHT_CLICK_BLOCK:
|
||||
case RIGHT_CLICK_AIR:
|
||||
controller.rightClick(event);
|
||||
break;
|
||||
case LEFT_CLICK_BLOCK:
|
||||
case LEFT_CLICK_AIR:
|
||||
controller.leftClick(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRightClick(NPCRightClickEvent event) {
|
||||
if (!enabled || !npc.isSpawned() || !event.getNPC().equals(npc))
|
||||
return;
|
||||
controller.rightClickEntity(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
loadController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!enabled || !npc.isSpawned())
|
||||
return;
|
||||
List<Entity> passengers = NMS.getPassengers(npc.getEntity());
|
||||
if (passengers.size() == 0 || !(passengers.get(0) instanceof Player))
|
||||
return;
|
||||
controller.run((Player) passengers.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
if (explicitType == null) {
|
||||
key.removeKey("explicittype");
|
||||
} else {
|
||||
key.setString("explicittype", explicitType.name());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
private void setMountedYaw(Entity entity) {
|
||||
if (entity instanceof EnderDragon || !Setting.USE_BOAT_CONTROLS.asBoolean())
|
||||
return; // EnderDragon handles this separately
|
||||
Location loc = entity.getLocation();
|
||||
Vector vel = entity.getVelocity();
|
||||
double tX = loc.getX() + vel.getX();
|
||||
double tZ = loc.getZ() + vel.getZ();
|
||||
if (loc.getX() > tZ) {
|
||||
loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ))) + 180F);
|
||||
} else if (loc.getZ() < tZ) {
|
||||
loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ))));
|
||||
}
|
||||
entity.teleport(loc);
|
||||
NMS.setHeadYaw(entity, loc.getYaw());
|
||||
}
|
||||
|
||||
public void setOwnerRequired(boolean ownerRequired) {
|
||||
this.ownerRequired = ownerRequired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
enabled = !enabled;
|
||||
if (!enabled && NMS.getPassengers(npc.getEntity()).size() > 0) {
|
||||
NMS.getPassengers(npc.getEntity()).get(0).leaveVehicle();
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
private double updateHorizontalSpeed(Entity handle, Entity passenger, double speed, float speedMod) {
|
||||
Vector hvel = handle.getVelocity();
|
||||
double oldSpeed = Math.sqrt(hvel.getX() * hvel.getX() + hvel.getZ() * hvel.getZ());
|
||||
double angle = Math.toRadians(passenger.getLocation().getYaw() - NMS.getVerticalMovement(passenger) * 45.0F);
|
||||
hvel = hvel.add(new Vector(speedMod * -Math.sin(angle) * NMS.getHorizontalMovement(passenger) * 0.05, 0,
|
||||
speedMod * Math.cos(angle) * NMS.getHorizontalMovement(passenger) * 0.05));
|
||||
handle.setVelocity(hvel);
|
||||
|
||||
double newSpeed = Math.sqrt(hvel.getX() * hvel.getX() + hvel.getZ() * hvel.getZ());
|
||||
if (newSpeed > oldSpeed && speed < 0.35D) {
|
||||
return (float) Math.min(0.35D, (speed + ((0.35D - speed) / 35.0D)));
|
||||
} else {
|
||||
return (float) Math.max(0.07D, (speed - ((speed - 0.07D) / 35.0D)));
|
||||
}
|
||||
}
|
||||
|
||||
public class GroundController implements MovementController {
|
||||
private int jumpTicks = 0;
|
||||
private double speed = 0.07D;
|
||||
|
||||
@Override
|
||||
public void leftClick(PlayerInteractEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClick(PlayerInteractEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClickEntity(NPCRightClickEvent event) {
|
||||
enterOrLeaveVehicle(event.getClicker());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Player rider) {
|
||||
boolean onGround = NMS.isOnGround(npc.getEntity());
|
||||
float speedMod = npc.getNavigator().getDefaultParameters()
|
||||
.modifiedSpeed((onGround ? GROUND_SPEED : AIR_SPEED));
|
||||
speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, speedMod);
|
||||
|
||||
boolean shouldJump = NMS.shouldJump(rider);
|
||||
if (shouldJump) {
|
||||
if (onGround && jumpTicks == 0) {
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(JUMP_VELOCITY));
|
||||
jumpTicks = 10;
|
||||
}
|
||||
} else {
|
||||
jumpTicks = 0;
|
||||
}
|
||||
jumpTicks = Math.max(0, jumpTicks - 1);
|
||||
setMountedYaw(npc.getEntity());
|
||||
}
|
||||
|
||||
private static final float AIR_SPEED = 1.5F;
|
||||
private static final float GROUND_SPEED = 4F;
|
||||
private static final float JUMP_VELOCITY = 0.6F;
|
||||
}
|
||||
|
||||
public class LookAirController implements MovementController {
|
||||
private boolean paused = false;
|
||||
|
||||
@Override
|
||||
public void leftClick(PlayerInteractEvent event) {
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClick(PlayerInteractEvent event) {
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClickEntity(NPCRightClickEvent event) {
|
||||
enterOrLeaveVehicle(event.getClicker());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Player rider) {
|
||||
if (paused) {
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001));
|
||||
return;
|
||||
}
|
||||
Vector dir = rider.getEyeLocation().getDirection();
|
||||
dir.multiply(npc.getNavigator().getDefaultParameters().speedModifier());
|
||||
npc.getEntity().setVelocity(dir);
|
||||
setMountedYaw(npc.getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
public static interface MovementController {
|
||||
void leftClick(PlayerInteractEvent event);
|
||||
|
||||
void rightClick(PlayerInteractEvent event);
|
||||
|
||||
void rightClickEntity(NPCRightClickEvent event);
|
||||
|
||||
void run(Player rider);
|
||||
}
|
||||
|
||||
public class PlayerInputAirController implements MovementController {
|
||||
private boolean paused = false;
|
||||
private double speed;
|
||||
|
||||
@Override
|
||||
public void leftClick(PlayerInteractEvent event) {
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClick(PlayerInteractEvent event) {
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(-0.3F));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rightClickEntity(NPCRightClickEvent event) {
|
||||
enterOrLeaveVehicle(event.getClicker());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Player rider) {
|
||||
if (paused) {
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001F));
|
||||
return;
|
||||
}
|
||||
|
||||
speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, 1F);
|
||||
boolean shouldJump = NMS.shouldJump(rider);
|
||||
if (shouldJump) {
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.3F));
|
||||
}
|
||||
npc.getEntity().setVelocity(npc.getEntity().getVelocity().multiply(new Vector(1, 0.98, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerControllerType(EntityType type, Class<? extends MovementController> clazz) {
|
||||
controllerTypes.put(type, clazz);
|
||||
}
|
||||
|
||||
private static final Map<EntityType, Class<? extends MovementController>> controllerTypes = Maps
|
||||
.newEnumMap(EntityType.class);
|
||||
|
||||
static {
|
||||
controllerTypes.put(EntityType.BAT, PlayerInputAirController.class);
|
||||
controllerTypes.put(EntityType.BLAZE, PlayerInputAirController.class);
|
||||
controllerTypes.put(EntityType.ENDER_DRAGON, PlayerInputAirController.class);
|
||||
controllerTypes.put(EntityType.GHAST, PlayerInputAirController.class);
|
||||
controllerTypes.put(EntityType.WITHER, PlayerInputAirController.class);
|
||||
controllerTypes.put(EntityType.UNKNOWN, LookAirController.class);
|
||||
}
|
||||
}
|
37
main/java/net/citizensnpcs/trait/CurrentLocation.java
Normal file
37
main/java/net/citizensnpcs/trait/CurrentLocation.java
Normal file
@ -0,0 +1,37 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("location")
|
||||
public class CurrentLocation extends Trait {
|
||||
@Persist(value = "", required = true)
|
||||
private Location location = new Location(null, 0, 0, 0);
|
||||
|
||||
public CurrentLocation() {
|
||||
super("location");
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return location.getWorld() == null ? null : location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!npc.isSpawned())
|
||||
return;
|
||||
location = npc.getEntity().getLocation(location);
|
||||
}
|
||||
|
||||
public void setLocation(Location loc) {
|
||||
this.location = loc.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CurrentLocation{" + location + "}";
|
||||
}
|
||||
}
|
41
main/java/net/citizensnpcs/trait/Gravity.java
Normal file
41
main/java/net/citizensnpcs/trait/Gravity.java
Normal file
@ -0,0 +1,41 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("gravity")
|
||||
public class Gravity extends Trait implements Toggleable {
|
||||
@Persist
|
||||
private boolean enabled;
|
||||
|
||||
public Gravity() {
|
||||
super("gravity");
|
||||
}
|
||||
|
||||
public void gravitate(boolean gravitate) {
|
||||
enabled = gravitate;
|
||||
}
|
||||
|
||||
public boolean hasGravity() {
|
||||
return !enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!npc.isSpawned())
|
||||
return;
|
||||
if (!enabled || npc.getNavigator().isNavigating())
|
||||
return;
|
||||
Vector vector = npc.getEntity().getVelocity();
|
||||
vector.setY(Math.max(0, vector.getY()));
|
||||
npc.getEntity().setVelocity(vector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
return enabled = !enabled;
|
||||
}
|
||||
}
|
105
main/java/net/citizensnpcs/trait/HorseModifiers.java
Normal file
105
main/java/net/citizensnpcs/trait/HorseModifiers.java
Normal file
@ -0,0 +1,105 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Horse;
|
||||
import org.bukkit.entity.Horse.Color;
|
||||
import org.bukkit.entity.Horse.Style;
|
||||
import org.bukkit.entity.Horse.Variant;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("horsemodifiers")
|
||||
public class HorseModifiers extends Trait {
|
||||
@Persist("armor")
|
||||
private ItemStack armor = null;
|
||||
@Persist("carryingChest")
|
||||
private boolean carryingChest;
|
||||
@Persist("color")
|
||||
private Color color = Color.CREAMY;
|
||||
@Persist("saddle")
|
||||
private ItemStack saddle = null;
|
||||
@Persist("style")
|
||||
private Style style = Style.NONE;
|
||||
@Persist("type")
|
||||
private Variant type = Variant.HORSE;
|
||||
|
||||
public HorseModifiers() {
|
||||
super("horsemodifiers");
|
||||
}
|
||||
|
||||
public ItemStack getArmor() {
|
||||
return armor;
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public ItemStack getSaddle() {
|
||||
return saddle;
|
||||
}
|
||||
|
||||
public Style getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
public Variant getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (npc.getEntity() instanceof Horse) {
|
||||
Horse horse = (Horse) npc.getEntity();
|
||||
saddle = horse.getInventory().getSaddle();
|
||||
armor = horse.getInventory().getArmor();
|
||||
}
|
||||
}
|
||||
|
||||
public void setArmor(ItemStack armor) {
|
||||
this.armor = armor;
|
||||
}
|
||||
|
||||
public void setCarryingChest(boolean carryingChest) {
|
||||
this.carryingChest = carryingChest;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setColor(Horse.Color color) {
|
||||
this.color = color;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setSaddle(ItemStack saddle) {
|
||||
this.saddle = saddle;
|
||||
}
|
||||
|
||||
public void setStyle(Horse.Style style) {
|
||||
this.style = style;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setType(Horse.Variant type) {
|
||||
this.type = type;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
private void updateModifiers() {
|
||||
if (npc.getEntity() instanceof Horse) {
|
||||
Horse horse = (Horse) npc.getEntity();
|
||||
horse.setCarryingChest(carryingChest);
|
||||
horse.setColor(color);
|
||||
horse.setStyle(style);
|
||||
horse.setVariant(type);
|
||||
horse.getInventory().setArmor(armor);
|
||||
horse.getInventory().setSaddle(saddle);
|
||||
}
|
||||
}
|
||||
}
|
139
main/java/net/citizensnpcs/trait/LookClose.java
Normal file
139
main/java/net/citizensnpcs/trait/LookClose.java
Normal file
@ -0,0 +1,139 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.command.CommandConfigurable;
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
@TraitName("lookclose")
|
||||
public class LookClose extends Trait implements Toggleable, CommandConfigurable {
|
||||
private boolean enabled = Setting.DEFAULT_LOOK_CLOSE.asBoolean();
|
||||
private Player lookingAt;
|
||||
private double range = Setting.DEFAULT_LOOK_CLOSE_RANGE.asDouble();
|
||||
private boolean realisticLooking = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
|
||||
|
||||
public LookClose() {
|
||||
super("lookclose");
|
||||
}
|
||||
|
||||
private boolean canSeeTarget() {
|
||||
return realisticLooking && npc.getEntity() instanceof LivingEntity
|
||||
? ((LivingEntity) npc.getEntity()).hasLineOfSight(lookingAt) : true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(CommandContext args) {
|
||||
range = args.getFlagDouble("range", range);
|
||||
range = args.getFlagDouble("r", range);
|
||||
realisticLooking = args.hasFlag('r');
|
||||
}
|
||||
|
||||
private void findNewTarget() {
|
||||
List<Entity> nearby = npc.getEntity().getNearbyEntities(range, range, range);
|
||||
Collections.sort(nearby, new Comparator<Entity>() {
|
||||
@Override
|
||||
public int compare(Entity o1, Entity o2) {
|
||||
Location l1 = o1.getLocation(CACHE_LOCATION);
|
||||
Location l2 = o2.getLocation(CACHE_LOCATION2);
|
||||
if (!NPC_LOCATION.getWorld().equals(l1.getWorld()) || !NPC_LOCATION.getWorld().equals(l2.getWorld())) {
|
||||
return -1;
|
||||
}
|
||||
return Double.compare(l1.distanceSquared(NPC_LOCATION), l2.distanceSquared(NPC_LOCATION));
|
||||
}
|
||||
});
|
||||
for (Entity entity : nearby) {
|
||||
if (entity.getType() != EntityType.PLAYER || ((Player) entity).getGameMode() == GameMode.SPECTATOR
|
||||
|| entity.getLocation(CACHE_LOCATION).getWorld() != NPC_LOCATION.getWorld()
|
||||
|| CitizensAPI.getNPCRegistry().getNPC(entity) != null)
|
||||
continue;
|
||||
lookingAt = (Player) entity;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasInvalidTarget() {
|
||||
if (lookingAt == null)
|
||||
return true;
|
||||
if (!lookingAt.isOnline() || lookingAt.getWorld() != npc.getEntity().getWorld()
|
||||
|| lookingAt.getLocation(PLAYER_LOCATION).distanceSquared(NPC_LOCATION) > range) {
|
||||
lookingAt = null;
|
||||
}
|
||||
return lookingAt == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
enabled = key.getBoolean("enabled", true);
|
||||
range = key.getDouble("range", range);
|
||||
realisticLooking = key.getBoolean("realisticlooking", key.getBoolean("realistic-looking"));
|
||||
}
|
||||
|
||||
public void lookClose(boolean lookClose) {
|
||||
enabled = lookClose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDespawn() {
|
||||
lookingAt = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!enabled || !npc.isSpawned() || npc.getNavigator().isNavigating())
|
||||
return;
|
||||
npc.getEntity().getLocation(NPC_LOCATION);
|
||||
if (hasInvalidTarget()) {
|
||||
findNewTarget();
|
||||
}
|
||||
if (lookingAt != null && canSeeTarget()) {
|
||||
Util.faceEntity(npc.getEntity(), lookingAt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.setBoolean("enabled", enabled);
|
||||
key.setDouble("range", range);
|
||||
key.setBoolean("realisticlooking", realisticLooking);
|
||||
}
|
||||
|
||||
public void setRange(int range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public void setRealisticLooking(boolean realistic) {
|
||||
this.realisticLooking = realistic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
enabled = !enabled;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LookClose{" + enabled + "}";
|
||||
}
|
||||
|
||||
private static final Location CACHE_LOCATION = new Location(null, 0, 0, 0);
|
||||
private static final Location CACHE_LOCATION2 = new Location(null, 0, 0, 0);
|
||||
private static final Location NPC_LOCATION = new Location(null, 0, 0, 0);
|
||||
private static final Location PLAYER_LOCATION = new Location(null, 0, 0, 0);
|
||||
}
|
37
main/java/net/citizensnpcs/trait/MountTrait.java
Normal file
37
main/java/net/citizensnpcs/trait/MountTrait.java
Normal file
@ -0,0 +1,37 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.npc.ai.NPCHolder;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
@TraitName("mounttrait")
|
||||
public class MountTrait extends Trait {
|
||||
@Persist("mountedon")
|
||||
private UUID mountedOn;
|
||||
|
||||
public MountTrait() {
|
||||
super("mounttrait");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!npc.isSpawned())
|
||||
return;
|
||||
if (mountedOn != null) {
|
||||
NPC other = CitizensAPI.getNPCRegistry().getByUniqueId(mountedOn);
|
||||
if (other != null && other.isSpawned()) {
|
||||
NMS.mount(other.getEntity(), npc.getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
if (NMS.getVehicle(npc.getEntity()) instanceof NPCHolder) {
|
||||
mountedOn = ((NPCHolder) NMS.getVehicle(npc.getEntity())).getNPC().getUniqueId();
|
||||
}
|
||||
}
|
||||
}
|
34
main/java/net/citizensnpcs/trait/NPCSkeletonType.java
Normal file
34
main/java/net/citizensnpcs/trait/NPCSkeletonType.java
Normal file
@ -0,0 +1,34 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Skeleton;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("skeletontype")
|
||||
public class NPCSkeletonType extends Trait {
|
||||
private Skeleton skeleton;
|
||||
@Persist
|
||||
private org.bukkit.entity.Skeleton.SkeletonType type = org.bukkit.entity.Skeleton.SkeletonType.NORMAL;
|
||||
|
||||
public NPCSkeletonType() {
|
||||
super("skeletontype");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
skeleton = npc.getEntity() instanceof Skeleton ? (Skeleton) npc.getEntity() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (skeleton != null) {
|
||||
skeleton.setSkeletonType(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void setType(org.bukkit.entity.Skeleton.SkeletonType type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
43
main/java/net/citizensnpcs/trait/OcelotModifiers.java
Normal file
43
main/java/net/citizensnpcs/trait/OcelotModifiers.java
Normal file
@ -0,0 +1,43 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Ocelot;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
@TraitName("ocelotmodifiers")
|
||||
public class OcelotModifiers extends Trait {
|
||||
@Persist("sitting")
|
||||
private boolean sitting;
|
||||
@Persist("type")
|
||||
private Ocelot.Type type = Ocelot.Type.WILD_OCELOT;
|
||||
|
||||
public OcelotModifiers() {
|
||||
super("ocelotmodifiers");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setSitting(boolean sit) {
|
||||
this.sitting = sit;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setType(Ocelot.Type type) {
|
||||
this.type = type;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
private void updateModifiers() {
|
||||
if (npc.getEntity() instanceof Ocelot) {
|
||||
Ocelot ocelot = (Ocelot) npc.getEntity();
|
||||
ocelot.setCatType(type);
|
||||
NMS.setSitting(ocelot, sitting);
|
||||
}
|
||||
}
|
||||
}
|
104
main/java/net/citizensnpcs/trait/Poses.java
Normal file
104
main/java/net/citizensnpcs/trait/Poses.java
Normal file
@ -0,0 +1,104 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.api.command.exception.CommandException;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.api.util.Paginator;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.Pose;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
@TraitName("poses")
|
||||
public class Poses extends Trait {
|
||||
private final Map<String, Pose> poses = Maps.newHashMap();
|
||||
|
||||
public Poses() {
|
||||
super("poses");
|
||||
}
|
||||
|
||||
public boolean addPose(String name, Location location) {
|
||||
name = name.toLowerCase();
|
||||
Pose newPose = new Pose(name, location.getPitch(), location.getYaw());
|
||||
if (poses.containsValue(newPose) || poses.containsKey(name))
|
||||
return false;
|
||||
poses.put(name, newPose);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void assumePose(float yaw, float pitch) {
|
||||
if (!npc.isSpawned())
|
||||
npc.spawn(npc.getTrait(CurrentLocation.class).getLocation());
|
||||
|
||||
Util.assumePose(npc.getEntity(), yaw, pitch);
|
||||
}
|
||||
|
||||
public void assumePose(Location location) {
|
||||
assumePose(location.getYaw(), location.getPitch());
|
||||
}
|
||||
|
||||
public void assumePose(String flag) {
|
||||
Pose pose = poses.get(flag.toLowerCase());
|
||||
assumePose(pose.getYaw(), pose.getPitch());
|
||||
}
|
||||
|
||||
public void describe(CommandSender sender, int page) throws CommandException {
|
||||
Paginator paginator = new Paginator().header("Pose");
|
||||
paginator.addLine("<e>Key: <a>ID <b>Name <c>Pitch/Yaw");
|
||||
int i = 0;
|
||||
for (Pose pose : poses.values()) {
|
||||
String line = "<a>" + i + "<b> " + pose.getName() + "<c> " + pose.getPitch() + "/" + pose.getYaw();
|
||||
paginator.addLine(line);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!paginator.sendPage(sender, page))
|
||||
throw new CommandException(Messages.COMMAND_PAGE_MISSING);
|
||||
}
|
||||
|
||||
public Pose getPose(String name) {
|
||||
for (Pose pose : poses.values())
|
||||
if (pose.getName().equalsIgnoreCase(name))
|
||||
return pose;
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasPose(String pose) {
|
||||
return poses.containsKey(pose.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
poses.clear();
|
||||
for (DataKey sub : key.getRelative("list").getIntegerSubKeys())
|
||||
try {
|
||||
String[] parts = sub.getString("").split(";");
|
||||
poses.put(parts[0], new Pose(parts[0], Float.valueOf(parts[1]), Float.valueOf(parts[2])));
|
||||
} catch (NumberFormatException e) {
|
||||
Messaging.logTr(Messages.SKIPPING_INVALID_POSE, sub.name(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removePose(String pose) {
|
||||
return poses.remove(pose.toLowerCase()) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.removeKey("list");
|
||||
int i = 0;
|
||||
for (Pose pose : poses.values()) {
|
||||
key.setString("list." + i, pose.stringValue());
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
36
main/java/net/citizensnpcs/trait/Powered.java
Normal file
36
main/java/net/citizensnpcs/trait/Powered.java
Normal file
@ -0,0 +1,36 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Creeper;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("powered")
|
||||
public class Powered extends Trait implements Toggleable {
|
||||
@Persist("")
|
||||
private boolean powered;
|
||||
|
||||
public Powered() {
|
||||
super("powered");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Creeper)
|
||||
((Creeper) npc.getEntity()).setPowered(powered);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
powered = !powered;
|
||||
if (npc.getEntity() instanceof Creeper)
|
||||
((Creeper) npc.getEntity()).setPowered(powered);
|
||||
return powered;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Powered{" + powered + "}";
|
||||
}
|
||||
}
|
31
main/java/net/citizensnpcs/trait/RabbitType.java
Normal file
31
main/java/net/citizensnpcs/trait/RabbitType.java
Normal file
@ -0,0 +1,31 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Rabbit;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("rabbittype")
|
||||
public class RabbitType extends Trait {
|
||||
private Rabbit rabbit;
|
||||
@Persist
|
||||
private Rabbit.Type type = Rabbit.Type.BROWN;
|
||||
|
||||
public RabbitType() {
|
||||
super("rabbittype");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
rabbit = npc.getEntity() instanceof Rabbit ? (Rabbit) npc.getEntity() : null;
|
||||
setType(type);
|
||||
}
|
||||
|
||||
public void setType(Rabbit.Type type) {
|
||||
this.type = type;
|
||||
if (rabbit != null && rabbit.isValid()) {
|
||||
rabbit.setRabbitType(type);
|
||||
}
|
||||
}
|
||||
}
|
49
main/java/net/citizensnpcs/trait/Saddle.java
Normal file
49
main/java/net/citizensnpcs/trait/Saddle.java
Normal file
@ -0,0 +1,49 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Pig;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("saddle")
|
||||
public class Saddle extends Trait implements Toggleable {
|
||||
private boolean pig;
|
||||
@Persist("")
|
||||
private boolean saddle;
|
||||
|
||||
public Saddle() {
|
||||
super("saddle");
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
|
||||
if (pig && npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getRightClicked())))
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Pig) {
|
||||
((Pig) npc.getEntity()).setSaddle(saddle);
|
||||
pig = true;
|
||||
} else
|
||||
pig = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
saddle = !saddle;
|
||||
if (pig)
|
||||
((Pig) npc.getEntity()).setSaddle(saddle);
|
||||
return saddle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Saddle{" + saddle + "}";
|
||||
}
|
||||
}
|
119
main/java/net/citizensnpcs/trait/ScriptTrait.java
Normal file
119
main/java/net/citizensnpcs/trait/ScriptTrait.java
Normal file
@ -0,0 +1,119 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import net.citizensnpcs.Citizens;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.scripting.CompileCallback;
|
||||
import net.citizensnpcs.api.scripting.Script;
|
||||
import net.citizensnpcs.api.scripting.ScriptFactory;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
|
||||
@TraitName("scripttrait")
|
||||
public class ScriptTrait extends Trait {
|
||||
@Persist
|
||||
public List<String> files = new ArrayList<String>();
|
||||
private final List<RunnableScript> runnableScripts = new ArrayList<RunnableScript>();
|
||||
|
||||
public ScriptTrait() {
|
||||
super("scripttrait");
|
||||
}
|
||||
|
||||
public void addScripts(List<String> scripts) {
|
||||
for (String f : scripts) {
|
||||
if (!files.contains(f) && validateFile(f)) {
|
||||
loadScript(f);
|
||||
files.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getScripts() {
|
||||
return files;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) {
|
||||
for (String file : files) {
|
||||
if (validateFile(file)) {
|
||||
loadScript(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadScript(final String file) {
|
||||
File f = new File(JavaPlugin.getPlugin(Citizens.class).getScriptFolder(), file);
|
||||
CitizensAPI.getScriptCompiler().compile(f).cache(true).withCallback(new CompileCallback() {
|
||||
@Override
|
||||
public void onScriptCompiled(String sourceDescriptor, ScriptFactory compiled) {
|
||||
final Script newInstance = compiled.newInstance();
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
newInstance.invoke("onLoad", npc);
|
||||
} catch (RuntimeException e) {
|
||||
if (!(e.getCause() instanceof NoSuchMethodException)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
runnableScripts.add(new RunnableScript(newInstance, file));
|
||||
}
|
||||
});
|
||||
}
|
||||
}).beginWithFuture();
|
||||
}
|
||||
|
||||
public void removeScripts(List<String> scripts) {
|
||||
files.removeAll(scripts);
|
||||
Iterator<RunnableScript> itr = runnableScripts.iterator();
|
||||
while (itr.hasNext()) {
|
||||
if (scripts.remove(itr.next().file)) {
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Iterator<RunnableScript> itr = runnableScripts.iterator();
|
||||
while (itr.hasNext()) {
|
||||
try {
|
||||
itr.next().script.invoke("run", npc);
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getCause() instanceof NoSuchMethodException) {
|
||||
itr.remove();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateFile(String file) {
|
||||
File f = new File(JavaPlugin.getPlugin(Citizens.class).getScriptFolder(), file);
|
||||
if (!f.exists() || !f.getParentFile().equals(JavaPlugin.getPlugin(Citizens.class).getScriptFolder())) {
|
||||
return false;
|
||||
}
|
||||
return CitizensAPI.getScriptCompiler().canCompile(f);
|
||||
}
|
||||
|
||||
private static class RunnableScript {
|
||||
String file;
|
||||
Script script;
|
||||
|
||||
public RunnableScript(Script script, String file) {
|
||||
this.script = script;
|
||||
this.file = file;
|
||||
}
|
||||
}
|
||||
}
|
55
main/java/net/citizensnpcs/trait/SheepTrait.java
Normal file
55
main/java/net/citizensnpcs/trait/SheepTrait.java
Normal 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;
|
||||
}
|
||||
}
|
263
main/java/net/citizensnpcs/trait/SkinLayers.java
Normal file
263
main/java/net/citizensnpcs/trait/SkinLayers.java
Normal file
@ -0,0 +1,263 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.npc.skin.SkinnableEntity;
|
||||
|
||||
@TraitName("skinlayers")
|
||||
public class SkinLayers extends Trait {
|
||||
@Persist("cape")
|
||||
private boolean cape = true;
|
||||
@Persist("hat")
|
||||
private boolean hat = true;
|
||||
@Persist("jacket")
|
||||
private boolean jacket = true;
|
||||
@Persist("left-pants")
|
||||
private boolean leftPants = true;
|
||||
@Persist("left-sleeve")
|
||||
private boolean leftSleeve = true;
|
||||
@Persist("right-pants")
|
||||
private boolean rightPants = true;
|
||||
@Persist("right-sleeve")
|
||||
private boolean rightSleeve = true;
|
||||
|
||||
public SkinLayers() {
|
||||
super("skinlayers");
|
||||
}
|
||||
|
||||
public SkinLayers hide() {
|
||||
cape = false;
|
||||
hat = false;
|
||||
jacket = false;
|
||||
leftSleeve = false;
|
||||
rightSleeve = false;
|
||||
leftPants = false;
|
||||
rightPants = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideCape() {
|
||||
cape = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideHat() {
|
||||
hat = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideJacket() {
|
||||
jacket = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideLeftPants() {
|
||||
leftPants = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideLeftSleeve() {
|
||||
leftSleeve = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hidePants() {
|
||||
leftPants = false;
|
||||
rightPants = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideRightPants() {
|
||||
rightPants = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideRightSleeve() {
|
||||
rightSleeve = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers hideSleeves() {
|
||||
leftSleeve = false;
|
||||
rightSleeve = false;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isVisible(Layer layer) {
|
||||
switch (layer) {
|
||||
case CAPE:
|
||||
return cape;
|
||||
case JACKET:
|
||||
return jacket;
|
||||
case LEFT_SLEEVE:
|
||||
return leftSleeve;
|
||||
case RIGHT_SLEEVE:
|
||||
return rightSleeve;
|
||||
case LEFT_PANTS:
|
||||
return leftPants;
|
||||
case RIGHT_PANTS:
|
||||
return rightPants;
|
||||
case HAT:
|
||||
return hat;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach() {
|
||||
setFlags();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
setFlags();
|
||||
}
|
||||
|
||||
private void setFlags() {
|
||||
if (!npc.isSpawned())
|
||||
return;
|
||||
|
||||
SkinnableEntity skinnable = npc.getEntity() instanceof SkinnableEntity ? (SkinnableEntity) npc.getEntity()
|
||||
: null;
|
||||
if (skinnable == null)
|
||||
return;
|
||||
|
||||
int flags = 0xFF;
|
||||
for (Layer layer : Layer.values()) {
|
||||
if (!isVisible(layer)) {
|
||||
flags &= ~layer.flag;
|
||||
}
|
||||
}
|
||||
skinnable.setSkinFlags((byte) flags);
|
||||
}
|
||||
|
||||
public SkinLayers setVisible(Layer layer, boolean isVisible) {
|
||||
switch (layer) {
|
||||
case CAPE:
|
||||
cape = isVisible;
|
||||
break;
|
||||
case JACKET:
|
||||
jacket = isVisible;
|
||||
break;
|
||||
case LEFT_SLEEVE:
|
||||
leftSleeve = isVisible;
|
||||
break;
|
||||
case RIGHT_SLEEVE:
|
||||
rightSleeve = isVisible;
|
||||
break;
|
||||
case LEFT_PANTS:
|
||||
leftPants = isVisible;
|
||||
break;
|
||||
case RIGHT_PANTS:
|
||||
rightPants = isVisible;
|
||||
break;
|
||||
case HAT:
|
||||
hat = isVisible;
|
||||
break;
|
||||
}
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers show() {
|
||||
cape = true;
|
||||
hat = true;
|
||||
jacket = true;
|
||||
leftSleeve = true;
|
||||
rightSleeve = true;
|
||||
leftPants = true;
|
||||
rightPants = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showCape() {
|
||||
cape = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showHat() {
|
||||
hat = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showJacket() {
|
||||
jacket = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showLeftPants() {
|
||||
leftPants = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showLeftSleeve() {
|
||||
leftSleeve = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showPants() {
|
||||
leftPants = true;
|
||||
rightPants = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showRightPants() {
|
||||
rightPants = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showRightSleeve() {
|
||||
rightSleeve = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
public SkinLayers showSleeves() {
|
||||
leftSleeve = true;
|
||||
rightSleeve = true;
|
||||
setFlags();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SkinLayers{cape:" + cape + ", hat:" + hat + ", jacket:" + jacket + ", leftSleeve:" + leftSleeve
|
||||
+ ", rightSleeve:" + rightSleeve + ", leftPants:" + leftPants + ", rightPants:" + rightPants + "}";
|
||||
}
|
||||
|
||||
public enum Layer {
|
||||
CAPE(0),
|
||||
HAT(6),
|
||||
JACKET(1),
|
||||
LEFT_PANTS(4),
|
||||
LEFT_SLEEVE(2),
|
||||
RIGHT_PANTS(5),
|
||||
RIGHT_SLEEVE(3);
|
||||
|
||||
final int flag;
|
||||
|
||||
Layer(int offset) {
|
||||
this.flag = 1 << offset;
|
||||
}
|
||||
}
|
||||
}
|
41
main/java/net/citizensnpcs/trait/SlimeSize.java
Normal file
41
main/java/net/citizensnpcs/trait/SlimeSize.java
Normal file
@ -0,0 +1,41 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Slime;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
@TraitName("slimesize")
|
||||
public class SlimeSize extends Trait {
|
||||
@Persist
|
||||
private int size = 3;
|
||||
private boolean slime;
|
||||
|
||||
public SlimeSize() {
|
||||
super("slimesize");
|
||||
}
|
||||
|
||||
public void describe(CommandSender sender) {
|
||||
Messaging.sendTr(sender, Messages.SIZE_DESCRIPTION, npc.getName(), size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (!(npc.getEntity() instanceof Slime)) {
|
||||
slime = false;
|
||||
return;
|
||||
}
|
||||
((Slime) npc.getEntity()).setSize(size);
|
||||
slime = true;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
if (slime)
|
||||
((Slime) npc.getEntity()).setSize(size);
|
||||
}
|
||||
}
|
5
main/java/net/citizensnpcs/trait/Toggleable.java
Normal file
5
main/java/net/citizensnpcs/trait/Toggleable.java
Normal file
@ -0,0 +1,5 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
public interface Toggleable {
|
||||
public boolean toggle();
|
||||
}
|
57
main/java/net/citizensnpcs/trait/VillagerProfession.java
Normal file
57
main/java/net/citizensnpcs/trait/VillagerProfession.java
Normal file
@ -0,0 +1,57 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Villager;
|
||||
import org.bukkit.entity.Villager.Profession;
|
||||
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
|
||||
@TraitName("profession")
|
||||
public class VillagerProfession extends Trait {
|
||||
private Profession profession = Profession.FARMER;
|
||||
|
||||
public VillagerProfession() {
|
||||
super("profession");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
try {
|
||||
profession = Profession.valueOf(key.getString(""));
|
||||
if (profession == Profession.NORMAL) {
|
||||
profession = Profession.FARMER;
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new NPCLoadException("Invalid profession.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Villager) {
|
||||
((Villager) npc.getEntity()).setProfession(profession);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.setString("", profession.name());
|
||||
}
|
||||
|
||||
public void setProfession(Profession profession) {
|
||||
if (profession == Profession.NORMAL) {
|
||||
profession = Profession.FARMER;
|
||||
}
|
||||
this.profession = profession;
|
||||
if (npc.getEntity() instanceof Villager) {
|
||||
((Villager) npc.getEntity()).setProfession(profession);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Profession{" + profession + "}";
|
||||
}
|
||||
}
|
38
main/java/net/citizensnpcs/trait/WitherTrait.java
Normal file
38
main/java/net/citizensnpcs/trait/WitherTrait.java
Normal file
@ -0,0 +1,38 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Wither;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.util.NMS;
|
||||
|
||||
@TraitName("withertrait")
|
||||
public class WitherTrait extends Trait {
|
||||
@Persist("charged")
|
||||
private boolean charged = false;
|
||||
|
||||
public WitherTrait() {
|
||||
super("withertrait");
|
||||
}
|
||||
|
||||
public boolean isCharged() {
|
||||
return charged;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (npc.getEntity() instanceof Wither) {
|
||||
Wither wither = (Wither) npc.getEntity();
|
||||
NMS.setWitherCharged(wither, charged);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCharged(boolean charged) {
|
||||
this.charged = charged;
|
||||
}
|
||||
}
|
66
main/java/net/citizensnpcs/trait/WolfModifiers.java
Normal file
66
main/java/net/citizensnpcs/trait/WolfModifiers.java
Normal file
@ -0,0 +1,66 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.entity.Wolf;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("wolfmodifiers")
|
||||
public class WolfModifiers extends Trait {
|
||||
@Persist("angry")
|
||||
private boolean angry;
|
||||
@Persist("collarColor")
|
||||
private DyeColor collarColor = DyeColor.RED;
|
||||
@Persist("sitting")
|
||||
private boolean sitting;
|
||||
@Persist("tamed")
|
||||
private boolean tamed;
|
||||
|
||||
public WolfModifiers() {
|
||||
super("wolfmodifiers");
|
||||
}
|
||||
|
||||
public DyeColor getCollarColor() {
|
||||
return collarColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setAngry(boolean angry) {
|
||||
this.angry = angry;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setCollarColor(DyeColor color) {
|
||||
this.collarColor = color;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setSitting(boolean sitting) {
|
||||
this.sitting = sitting;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
public void setTamed(boolean tamed) {
|
||||
this.tamed = tamed;
|
||||
updateModifiers();
|
||||
}
|
||||
|
||||
private void updateModifiers() {
|
||||
if (npc.getEntity() instanceof Wolf) {
|
||||
Wolf wolf = (Wolf) npc.getEntity();
|
||||
wolf.setCollarColor(collarColor);
|
||||
wolf.setSitting(sitting);
|
||||
wolf.setAngry(angry);
|
||||
if (angry) {
|
||||
wolf.setTarget(wolf);
|
||||
}
|
||||
wolf.setTamed(tamed);
|
||||
}
|
||||
}
|
||||
}
|
64
main/java/net/citizensnpcs/trait/WoolColor.java
Normal file
64
main/java/net/citizensnpcs/trait/WoolColor.java
Normal file
@ -0,0 +1,64 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.DyeColor;
|
||||
import org.bukkit.entity.Sheep;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.entity.SheepDyeWoolEvent;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
|
||||
@TraitName("woolcolor")
|
||||
public class WoolColor extends Trait {
|
||||
private DyeColor color = DyeColor.WHITE;
|
||||
boolean sheep = false;
|
||||
|
||||
public WoolColor() {
|
||||
super("woolcolor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
try {
|
||||
color = DyeColor.valueOf(key.getString(""));
|
||||
} catch (Exception ex) {
|
||||
color = DyeColor.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onSheepDyeWool(SheepDyeWoolEvent event) {
|
||||
if (npc.equals(CitizensAPI.getNPCRegistry().getNPC(event.getEntity())))
|
||||
event.setCancelled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Sheep) {
|
||||
((Sheep) npc.getEntity()).setColor(color);
|
||||
sheep = true;
|
||||
} else {
|
||||
sheep = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.setString("", color.name());
|
||||
}
|
||||
|
||||
public void setColor(DyeColor color) {
|
||||
this.color = color;
|
||||
if (sheep) {
|
||||
((Sheep) npc.getEntity()).setColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WoolColor{" + color.name() + "}";
|
||||
}
|
||||
}
|
58
main/java/net/citizensnpcs/trait/ZombieModifier.java
Normal file
58
main/java/net/citizensnpcs/trait/ZombieModifier.java
Normal file
@ -0,0 +1,58 @@
|
||||
package net.citizensnpcs.trait;
|
||||
|
||||
import org.bukkit.entity.Villager.Profession;
|
||||
import org.bukkit.entity.Zombie;
|
||||
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
|
||||
@TraitName("zombiemodifier")
|
||||
public class ZombieModifier extends Trait {
|
||||
@Persist
|
||||
private boolean baby;
|
||||
@Persist
|
||||
private Profession profession;
|
||||
@Persist
|
||||
private boolean villager;
|
||||
private boolean zombie;
|
||||
|
||||
public ZombieModifier() {
|
||||
super("zombiemodifier");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (npc.getEntity() instanceof Zombie) {
|
||||
((Zombie) npc.getEntity()).setVillager(villager);
|
||||
((Zombie) npc.getEntity()).setBaby(baby);
|
||||
((Zombie) npc.getEntity()).setVillagerProfession(profession);
|
||||
zombie = true;
|
||||
} else {
|
||||
zombie = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProfession(Profession profession) {
|
||||
this.profession = profession;
|
||||
if (zombie) {
|
||||
((Zombie) npc.getEntity()).setVillagerProfession(profession);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean toggleBaby() {
|
||||
baby = !baby;
|
||||
if (zombie) {
|
||||
((Zombie) npc.getEntity()).setBaby(baby);
|
||||
}
|
||||
return baby;
|
||||
}
|
||||
|
||||
public boolean toggleVillager() {
|
||||
villager = !villager;
|
||||
if (zombie) {
|
||||
((Zombie) npc.getEntity()).setVillager(villager);
|
||||
}
|
||||
return villager;
|
||||
}
|
||||
}
|
38
main/java/net/citizensnpcs/trait/text/PageChangePrompt.java
Normal file
38
main/java/net/citizensnpcs/trait/text/PageChangePrompt.java
Normal file
@ -0,0 +1,38 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.NumericPrompt;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PageChangePrompt extends NumericPrompt {
|
||||
private final Text text;
|
||||
|
||||
public PageChangePrompt(Text text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptValidatedInput(ConversationContext context, Number input) {
|
||||
Player player = (Player) context.getForWhom();
|
||||
if (!text.sendPage(player, input.intValue())) {
|
||||
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_PAGE);
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
return (Prompt) context.getSessionData("previous");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFailedValidationText(ConversationContext context, String input) {
|
||||
return ChatColor.RED + Messaging.tr(Messages.TEXT_EDITOR_INVALID_PAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
return Messaging.tr(Messages.TEXT_EDITOR_PAGE_PROMPT);
|
||||
}
|
||||
}
|
241
main/java/net/citizensnpcs/trait/text/Text.java
Normal file
241
main/java/net/citizensnpcs/trait/text/Text.java
Normal file
@ -0,0 +1,241 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.conversations.Conversation;
|
||||
import org.bukkit.conversations.ConversationAbandonedEvent;
|
||||
import org.bukkit.conversations.ConversationAbandonedListener;
|
||||
import org.bukkit.conversations.ConversationFactory;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.ai.speech.SpeechContext;
|
||||
import net.citizensnpcs.api.event.NPCRightClickEvent;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.api.util.Paginator;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.trait.Toggleable;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.Util;
|
||||
|
||||
@TraitName("text")
|
||||
public class Text extends Trait implements Runnable, Toggleable, Listener, ConversationAbandonedListener {
|
||||
private final Map<UUID, Date> cooldowns = Maps.newHashMap();
|
||||
private int currentIndex;
|
||||
private String itemInHandPattern = "default";
|
||||
private final Plugin plugin;
|
||||
private boolean randomTalker = Setting.DEFAULT_RANDOM_TALKER.asBoolean();
|
||||
private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble();
|
||||
private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean();
|
||||
private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean();
|
||||
private final List<String> text = new ArrayList<String>();
|
||||
|
||||
public Text() {
|
||||
super("text");
|
||||
this.plugin = CitizensAPI.getPlugin();
|
||||
}
|
||||
|
||||
void add(String string) {
|
||||
text.add(string);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void conversationAbandoned(ConversationAbandonedEvent event) {
|
||||
Bukkit.dispatchCommand((Player) event.getContext().getForWhom(), "npc text");
|
||||
}
|
||||
|
||||
void edit(int index, String newText) {
|
||||
text.set(index, newText);
|
||||
}
|
||||
|
||||
public Editor getEditor(final Player player) {
|
||||
final Conversation conversation = new ConversationFactory(plugin).addConversationAbandonedListener(this)
|
||||
.withLocalEcho(false).withEscapeSequence("/npc text").withEscapeSequence("exit").withModality(false)
|
||||
.withFirstPrompt(new TextStartPrompt(this)).buildConversation(player);
|
||||
return new Editor() {
|
||||
@Override
|
||||
public void begin() {
|
||||
Messaging.sendTr(player, Messages.TEXT_EDITOR_BEGIN);
|
||||
conversation.begin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
Messaging.sendTr(player, Messages.TEXT_EDITOR_END);
|
||||
conversation.abandon();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
boolean hasIndex(int index) {
|
||||
return index >= 0 && text.size() > index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
text.clear();
|
||||
// TODO: legacy, remove later
|
||||
for (DataKey sub : key.getIntegerSubKeys()) {
|
||||
text.add(sub.getString(""));
|
||||
}
|
||||
for (DataKey sub : key.getRelative("text").getIntegerSubKeys()) {
|
||||
text.add(sub.getString(""));
|
||||
}
|
||||
if (text.isEmpty()) {
|
||||
populateDefaultText();
|
||||
}
|
||||
|
||||
talkClose = key.getBoolean("talk-close", talkClose);
|
||||
realisticLooker = key.getBoolean("realistic-looking", realisticLooker);
|
||||
randomTalker = key.getBoolean("random-talker", randomTalker);
|
||||
range = key.getDouble("range", range);
|
||||
itemInHandPattern = key.getString("talkitem", itemInHandPattern);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRightClick(NPCRightClickEvent event) {
|
||||
if (!event.getNPC().equals(npc))
|
||||
return;
|
||||
String localPattern = itemInHandPattern.equals("default") ? Setting.TALK_ITEM.asString() : itemInHandPattern;
|
||||
if (Util.matchesItemInHand(event.getClicker(), localPattern) && !shouldTalkClose()) {
|
||||
sendText(event.getClicker());
|
||||
}
|
||||
}
|
||||
|
||||
private void populateDefaultText() {
|
||||
text.addAll(Setting.DEFAULT_TEXT.asList());
|
||||
}
|
||||
|
||||
void remove(int index) {
|
||||
text.remove(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!talkClose || !npc.isSpawned())
|
||||
return;
|
||||
List<Entity> nearby = npc.getEntity().getNearbyEntities(range, range, range);
|
||||
for (Entity search : nearby) {
|
||||
if (!(search instanceof Player) || ((Player) search).getGameMode() == GameMode.SPECTATOR)
|
||||
continue;
|
||||
Player player = (Player) search;
|
||||
// If the cooldown is not expired, do not send text
|
||||
Date cooldown = cooldowns.get(player.getUniqueId());
|
||||
if (cooldown != null) {
|
||||
if (!new Date().after(cooldown)) {
|
||||
return;
|
||||
}
|
||||
cooldowns.remove(player.getUniqueId());
|
||||
}
|
||||
if (!sendText(player))
|
||||
return;
|
||||
// Add a cooldown if the text was successfully sent
|
||||
Date wait = new Date();
|
||||
int secondsDelta = RANDOM.nextInt(Setting.TALK_CLOSE_MAXIMUM_COOLDOWN.asInt())
|
||||
+ Setting.TALK_CLOSE_MINIMUM_COOLDOWN.asInt();
|
||||
if (secondsDelta <= 0)
|
||||
return;
|
||||
long millisecondsDelta = TimeUnit.MILLISECONDS.convert(secondsDelta, TimeUnit.SECONDS);
|
||||
wait.setTime(wait.getTime() + millisecondsDelta);
|
||||
cooldowns.put(player.getUniqueId(), wait);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
key.setBoolean("talk-close", talkClose);
|
||||
key.setBoolean("random-talker", randomTalker);
|
||||
key.setBoolean("realistic-looking", realisticLooker);
|
||||
key.setDouble("range", range);
|
||||
key.setString("talkitem", itemInHandPattern);
|
||||
// TODO: legacy, remove later
|
||||
for (int i = 0; i < 100; i++)
|
||||
key.removeKey(String.valueOf(i));
|
||||
key.removeKey("text");
|
||||
for (int i = 0; i < text.size(); i++)
|
||||
key.setString("text." + String.valueOf(i), text.get(i));
|
||||
}
|
||||
|
||||
boolean sendPage(Player player, int page) {
|
||||
Paginator paginator = new Paginator().header(npc.getName() + "'s Text Entries");
|
||||
for (int i = 0; i < text.size(); i++)
|
||||
paginator.addLine("<a>" + i + " <7>- <e>" + text.get(i));
|
||||
|
||||
return paginator.sendPage(player, page);
|
||||
}
|
||||
|
||||
private boolean sendText(Player player) {
|
||||
if (!player.hasPermission("citizens.admin") && !player.hasPermission("citizens.npc.talk"))
|
||||
return false;
|
||||
if (text.size() == 0)
|
||||
return false;
|
||||
|
||||
int index = 0;
|
||||
if (randomTalker)
|
||||
index = RANDOM.nextInt(text.size());
|
||||
else {
|
||||
if (currentIndex > text.size() - 1)
|
||||
currentIndex = 0;
|
||||
index = currentIndex++;
|
||||
}
|
||||
|
||||
npc.getDefaultSpeechController().speak(new SpeechContext(text.get(index), player));
|
||||
return true;
|
||||
}
|
||||
|
||||
void setItemInHandPattern(String pattern) {
|
||||
itemInHandPattern = pattern;
|
||||
}
|
||||
|
||||
void setRange(double range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
boolean shouldTalkClose() {
|
||||
return talkClose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean toggle() {
|
||||
return (talkClose = !talkClose);
|
||||
}
|
||||
|
||||
boolean toggleRandomTalker() {
|
||||
return (randomTalker = !randomTalker);
|
||||
}
|
||||
|
||||
boolean toggleRealisticLooking() {
|
||||
return (realisticLooker = !realisticLooker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Text{talk-close=" + talkClose + ",text=");
|
||||
for (String line : text)
|
||||
builder.append(line + ",");
|
||||
builder.append("}");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static Random RANDOM = Util.getFastRandom();
|
||||
}
|
30
main/java/net/citizensnpcs/trait/text/TextAddPrompt.java
Normal file
30
main/java/net/citizensnpcs/trait/text/TextAddPrompt.java
Normal file
@ -0,0 +1,30 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.conversations.StringPrompt;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class TextAddPrompt extends StringPrompt {
|
||||
private final Text text;
|
||||
|
||||
public TextAddPrompt(Text text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptInput(ConversationContext context, String input) {
|
||||
text.add(input);
|
||||
Messaging.sendTr((Player) context.getForWhom(), Messages.TEXT_EDITOR_ADDED_ENTRY, input);
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
return ChatColor.GREEN + Messaging.tr(Messages.TEXT_EDITOR_ADD_PROMPT);
|
||||
}
|
||||
}
|
31
main/java/net/citizensnpcs/trait/text/TextEditPrompt.java
Normal file
31
main/java/net/citizensnpcs/trait/text/TextEditPrompt.java
Normal file
@ -0,0 +1,31 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.conversations.StringPrompt;
|
||||
|
||||
public class TextEditPrompt extends StringPrompt {
|
||||
private final Text text;
|
||||
|
||||
public TextEditPrompt(Text text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptInput(ConversationContext context, String input) {
|
||||
int index = (Integer) context.getSessionData("index");
|
||||
text.edit(index, input);
|
||||
Messaging.sendTr((CommandSender) context.getForWhom(), Messages.TEXT_EDITOR_EDITED_TEXT, index, input);
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
return ChatColor.GREEN + Messaging.tr(Messages.TEXT_EDITOR_EDIT_PROMPT);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
45
main/java/net/citizensnpcs/trait/text/TextRemovePrompt.java
Normal file
45
main/java/net/citizensnpcs/trait/text/TextRemovePrompt.java
Normal file
@ -0,0 +1,45 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.conversations.StringPrompt;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class TextRemovePrompt extends StringPrompt {
|
||||
private final Text text;
|
||||
|
||||
public TextRemovePrompt(Text text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptInput(ConversationContext context, String input) {
|
||||
Player player = (Player) context.getForWhom();
|
||||
try {
|
||||
int index = Integer.parseInt(input);
|
||||
if (!text.hasIndex(index)) {
|
||||
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INDEX, index);
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
text.remove(index);
|
||||
Messaging.sendTr(player, Messages.TEXT_EDITOR_REMOVED_ENTRY, index);
|
||||
return new TextStartPrompt(text);
|
||||
} catch (NumberFormatException ex) {
|
||||
if (input.equalsIgnoreCase("page")) {
|
||||
context.setSessionData("previous", this);
|
||||
return new PageChangePrompt(text);
|
||||
}
|
||||
}
|
||||
Messaging.sendErrorTr(player, Messages.TEXT_EDITOR_INVALID_INPUT);
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
text.sendPage(((Player) context.getForWhom()), 1);
|
||||
return Messaging.tr(Messages.TEXT_EDITOR_REMOVE_PROMPT);
|
||||
}
|
||||
}
|
69
main/java/net/citizensnpcs/trait/text/TextStartPrompt.java
Normal file
69
main/java/net/citizensnpcs/trait/text/TextStartPrompt.java
Normal file
@ -0,0 +1,69 @@
|
||||
package net.citizensnpcs.trait.text;
|
||||
|
||||
import net.citizensnpcs.Settings.Setting;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.conversations.ConversationContext;
|
||||
import org.bukkit.conversations.Prompt;
|
||||
import org.bukkit.conversations.StringPrompt;
|
||||
|
||||
public class TextStartPrompt extends StringPrompt {
|
||||
private final Text text;
|
||||
|
||||
public TextStartPrompt(Text text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Prompt acceptInput(ConversationContext context, String original) {
|
||||
String[] parts = ChatColor.stripColor(original.trim()).split(" ");
|
||||
String input = parts[0];
|
||||
CommandSender sender = (CommandSender) context.getForWhom();
|
||||
if (input.equalsIgnoreCase("add"))
|
||||
return new TextAddPrompt(text);
|
||||
else if (input.equalsIgnoreCase("edit"))
|
||||
return new TextEditStartPrompt(text);
|
||||
else if (input.equalsIgnoreCase("remove"))
|
||||
return new TextRemovePrompt(text);
|
||||
else if (input.equalsIgnoreCase("random"))
|
||||
Messaging.sendTr(sender, Messages.TEXT_EDITOR_RANDOM_TALKER_SET, text.toggleRandomTalker());
|
||||
else if (input.equalsIgnoreCase("realistic looking"))
|
||||
Messaging.sendTr(sender, Messages.TEXT_EDITOR_REALISTIC_LOOKING_SET, text.toggleRealisticLooking());
|
||||
else if (input.equalsIgnoreCase("close") || input.equalsIgnoreCase("talk-close"))
|
||||
Messaging.sendTr(sender, Messages.TEXT_EDITOR_CLOSE_TALKER_SET, text.toggle());
|
||||
else if (input.equalsIgnoreCase("range")) {
|
||||
try {
|
||||
double range = Math.min(Math.max(0, Double.parseDouble(parts[1])), Setting.MAX_TEXT_RANGE.asDouble());
|
||||
text.setRange(range);
|
||||
Messaging.sendTr(sender, Messages.TEXT_EDITOR_RANGE_SET, range);
|
||||
} catch (NumberFormatException e) {
|
||||
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_RANGE);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_RANGE);
|
||||
}
|
||||
} else if (input.equalsIgnoreCase("item")) {
|
||||
if (parts.length > 1) {
|
||||
text.setItemInHandPattern(parts[1]);
|
||||
Messaging.sendTr(sender, Messages.TEXT_EDITOR_SET_ITEM, parts[1]);
|
||||
}
|
||||
} else if (input.equalsIgnoreCase("help")) {
|
||||
context.setSessionData("said-text", false);
|
||||
Messaging.send(sender, getPromptText(context));
|
||||
} else
|
||||
Messaging.sendErrorTr(sender, Messages.TEXT_EDITOR_INVALID_EDIT_TYPE);
|
||||
|
||||
return new TextStartPrompt(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPromptText(ConversationContext context) {
|
||||
if (context.getSessionData("said-text") == Boolean.TRUE)
|
||||
return "";
|
||||
String text = Messaging.tr(Messages.TEXT_EDITOR_START_PROMPT);
|
||||
context.setSessionData("said-text", Boolean.TRUE);
|
||||
return text;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
113
main/java/net/citizensnpcs/trait/waypoint/Waypoint.java
Normal file
113
main/java/net/citizensnpcs/trait/waypoint/Waypoint.java
Normal file
@ -0,0 +1,113 @@
|
||||
package net.citizensnpcs.trait.waypoint;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.citizensnpcs.api.CitizensAPI;
|
||||
import net.citizensnpcs.api.npc.NPC;
|
||||
import net.citizensnpcs.api.persistence.Persist;
|
||||
import net.citizensnpcs.api.persistence.PersistenceLoader;
|
||||
import net.citizensnpcs.trait.waypoint.triggers.DelayTrigger;
|
||||
import net.citizensnpcs.trait.waypoint.triggers.WaypointTrigger;
|
||||
import net.citizensnpcs.trait.waypoint.triggers.WaypointTriggerRegistry;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
public class Waypoint {
|
||||
@Persist(required = true)
|
||||
private Location location;
|
||||
@Persist
|
||||
private List<WaypointTrigger> triggers;
|
||||
|
||||
public Waypoint() {
|
||||
}
|
||||
|
||||
public Waypoint(Location at) {
|
||||
location = at;
|
||||
}
|
||||
|
||||
public void addTrigger(WaypointTrigger trigger) {
|
||||
if (triggers == null)
|
||||
triggers = Lists.newArrayList();
|
||||
triggers.add(trigger);
|
||||
}
|
||||
|
||||
public double distance(Waypoint dest) {
|
||||
return location.distance(dest.location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Waypoint other = (Waypoint) obj;
|
||||
if (location == null) {
|
||||
if (other.location != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!location.equals(other.location)) {
|
||||
return false;
|
||||
}
|
||||
if (triggers == null) {
|
||||
if (other.triggers != null) {
|
||||
return false;
|
||||
}
|
||||
} else if (!triggers.equals(other.triggers)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Location getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<WaypointTrigger> getTriggers() {
|
||||
return triggers == null ? Collections.EMPTY_LIST : triggers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = prime + ((location == null) ? 0 : location.hashCode());
|
||||
return prime * result + ((triggers == null) ? 0 : triggers.hashCode());
|
||||
}
|
||||
|
||||
public void onReach(NPC npc) {
|
||||
if (triggers == null)
|
||||
return;
|
||||
runTriggers(npc, 0);
|
||||
}
|
||||
|
||||
private void runTriggers(final NPC npc, int start) {
|
||||
for (int i = start; i < triggers.size(); i++) {
|
||||
WaypointTrigger trigger = triggers.get(i);
|
||||
trigger.onWaypointReached(npc, location);
|
||||
if (!(trigger instanceof DelayTrigger))
|
||||
continue;
|
||||
int delay = ((DelayTrigger) trigger).getDelay();
|
||||
if (delay <= 0)
|
||||
continue;
|
||||
final int newStart = i;
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(CitizensAPI.getPlugin(), new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
runTriggers(npc, newStart);
|
||||
}
|
||||
}, delay);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
PersistenceLoader.registerPersistDelegate(WaypointTrigger.class, WaypointTriggerRegistry.class);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
137
main/java/net/citizensnpcs/trait/waypoint/Waypoints.java
Normal file
137
main/java/net/citizensnpcs/trait/waypoint/Waypoints.java
Normal file
@ -0,0 +1,137 @@
|
||||
package net.citizensnpcs.trait.waypoint;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import net.citizensnpcs.api.command.CommandContext;
|
||||
import net.citizensnpcs.api.exception.NPCLoadException;
|
||||
import net.citizensnpcs.api.persistence.PersistenceLoader;
|
||||
import net.citizensnpcs.api.trait.Trait;
|
||||
import net.citizensnpcs.api.trait.TraitName;
|
||||
import net.citizensnpcs.api.util.DataKey;
|
||||
import net.citizensnpcs.api.util.Messaging;
|
||||
import net.citizensnpcs.editor.Editor;
|
||||
import net.citizensnpcs.util.Messages;
|
||||
import net.citizensnpcs.util.StringHelper;
|
||||
|
||||
@TraitName("waypoints")
|
||||
public class Waypoints extends Trait {
|
||||
private WaypointProvider provider = new LinearWaypointProvider();
|
||||
private String providerName = "linear";
|
||||
|
||||
public Waypoints() {
|
||||
super("waypoints");
|
||||
}
|
||||
|
||||
private WaypointProvider create(Class<? extends WaypointProvider> clazz) {
|
||||
try {
|
||||
return clazz.newInstance();
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void describeProviders(CommandSender sender) {
|
||||
Messaging.sendTr(sender, Messages.AVAILABLE_WAYPOINT_PROVIDERS);
|
||||
for (String name : providers.keySet()) {
|
||||
Messaging.send(sender, " - " + StringHelper.wrap(name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link WaypointProvider}. May be null during initialisation.
|
||||
*
|
||||
* @return The current provider
|
||||
*/
|
||||
public WaypointProvider getCurrentProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current provider name
|
||||
*/
|
||||
public String getCurrentProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public Editor getEditor(CommandSender player, CommandContext args) {
|
||||
return provider.createEditor(player, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(DataKey key) throws NPCLoadException {
|
||||
provider = null;
|
||||
providerName = key.getString("provider", "linear");
|
||||
for (Entry<String, Class<? extends WaypointProvider>> entry : providers.entrySet()) {
|
||||
if (entry.getKey().equals(providerName)) {
|
||||
provider = create(entry.getValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (provider == null)
|
||||
return;
|
||||
PersistenceLoader.load(provider, key.getRelative(providerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSpawn() {
|
||||
if (provider != null) {
|
||||
provider.onSpawn(getNPC());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(DataKey key) {
|
||||
if (provider == null)
|
||||
return;
|
||||
PersistenceLoader.save(provider, key.getRelative(providerName));
|
||||
key.setString("provider", providerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link WaypointProvider} using the given name.
|
||||
*
|
||||
* @param name
|
||||
* The name of the waypoint provider, registered using {@link #registerWaypointProvider(Class, String)}
|
||||
* @return Whether the operation succeeded
|
||||
*/
|
||||
public boolean setWaypointProvider(String name) {
|
||||
name = name.toLowerCase();
|
||||
Class<? extends WaypointProvider> clazz = providers.get(name);
|
||||
if (provider != null) {
|
||||
provider.onRemove();
|
||||
}
|
||||
if (clazz == null || (provider = create(clazz)) == null)
|
||||
return false;
|
||||
providerName = name;
|
||||
if (npc != null && npc.isSpawned()) {
|
||||
provider.onSpawn(npc);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link WaypointProvider}, which can be subsequently used by NPCs.
|
||||
*
|
||||
* @param clazz
|
||||
* The class of the waypoint provider
|
||||
* @param name
|
||||
* The name of the waypoint provider
|
||||
*/
|
||||
public static void registerWaypointProvider(Class<? extends WaypointProvider> clazz, String name) {
|
||||
providers.put(name, clazz);
|
||||
}
|
||||
|
||||
private static final Map<String, Class<? extends WaypointProvider>> providers = Maps.newHashMap();
|
||||
|
||||
static {
|
||||
providers.put("linear", LinearWaypointProvider.class);
|
||||
providers.put("wander", WanderWaypointProvider.class);
|
||||
providers.put("guided", GuidedWaypointProvider.class);
|
||||
}
|
||||
}
|
@ -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
Block a user