TF-ProjectKorra/src/com/projectkorra/projectkorra/ability/CoreAbility.java
Christopher Martin 787b303c9f
1.9.1 (#1145)
## Additions
* Adds a built-in bending board sidebar to visualize bound abilities and cooldowns.
  * The board respects worlds where bending is disabled.
  * Players can use the command `/pk board` to toggle the visibility of their board.
  * Includes an API that community developers can use in BendingBoardManager.
  * Adds the `"Properties.BendingBoard"` config option to enable or disable the board server.
  * Adds language file configuration options to control BendingBoard visuals.
    * `"Board.Title"`
      * Controls the title at the top of the board.
      * Supports the standard Minecraft color codes.
    * `"Board.SelectionPrefix"`
      * Controls the prefix shown corresponding to your current hot bar slot.
      * Supports the standard Minecraft color codes.
    * `"Board.EmptySlot"`
      * Controls what is shown for empty slots.
      * Supports the standard Minecraft color codes.
      * `{slot_number}` can be used as a placeholder for the slot number.
    * `"Board.MiscSeparator"`
      * Controls the separation between hot bar binds and temporary cooldowns such as Combos.
      * Supports the standard Minecraft color codes.
* Adds support for KingdomsX version 1.10.19.1
* Adds ability permission check to passive abilities. They should now respect their `bending.ability.<ability name>` permissions.
* Adds `AbilityVelocityAffectEntityEvent`
  * A cancellable event that will fire whenever an ability would alter the velocity of an entity.
* Adds the `Abilities.Earth.EarthSmash.Shoot.CollisionRadius` configuration option
  * Sets the collision radius of shot EarthSmash.

## Fixes
* Fixes FireBlast going through liquids.
* Fixes duplication involving waterlogged containers.
* Fixes being able to not enter the name of a Preset when using the `/pk preset create <name>` command.
* Fixes getDayFactor() not being applied correctly and occasionally producing the wrong value.
* Fixes a rounding issue with some Fire ability damage configuration options.
* Fixes an error when attempting to start EarthGrab.
* Fixes PhaseChange error when melting snow.
* Fixes a memory/process leak in how cooldowns were removed.
  * A player's cooldowns could only be removed when they were online. If a player's cooldown expired while they weren't online, their cooldown would attempt to revert every tick until the player rejoined. This has been resolved so cooldowns can revert while a player is offline.
  * A side effect of this fix is that it is now possible for `PlayerCooldownChangeEvents` to fire while their corresponding Player is offline.
* Fixes an issue with `MultiAbilityManager#hasMultiAbilityBound` where it would return true if any MultiAbility is bound, not if the specified MultiAbility was bound.

## Misc Changes
* Updates Towny version to 0.96.2.0
* DensityShift sand blocks can now be used as a bendable source.
* Changes AvatarState so that its cooldown is applied when the ability ends instead of when it starts.
* Changes togglable abilities such as AvatarState, Illumination, and TremorSense to visually show when they are enabled in the BendingBoard and on BendingPreview in the same way as the ChiBlocking Stances.
* Updated the text of some ability descriptions and instructions.
* Adds new cache to PhaseChange to greatly improve the performance of water/ice updates.
2021-06-07 16:58:29 -07:00

1124 lines
39 KiB
Java

package com.projectkorra.projectkorra.ability;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.jar.JarFile;
import sun.reflect.ReflectionFactory;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import co.aikar.timings.lib.MCTiming;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import com.projectkorra.projectkorra.BendingPlayer;
import com.projectkorra.projectkorra.Element;
import com.projectkorra.projectkorra.Element.SubElement;
import com.projectkorra.projectkorra.Manager;
import com.projectkorra.projectkorra.ProjectKorra;
import com.projectkorra.projectkorra.ability.util.AbilityLoader;
import com.projectkorra.projectkorra.ability.util.AddonAbilityLoader;
import com.projectkorra.projectkorra.ability.util.Collision;
import com.projectkorra.projectkorra.ability.util.CollisionManager;
import com.projectkorra.projectkorra.ability.util.ComboManager;
import com.projectkorra.projectkorra.ability.util.MultiAbilityManager;
import com.projectkorra.projectkorra.ability.util.MultiAbilityManager.MultiAbilityInfo;
import com.projectkorra.projectkorra.ability.util.PassiveManager;
import com.projectkorra.projectkorra.attribute.Attribute;
import com.projectkorra.projectkorra.attribute.AttributeModifier;
import com.projectkorra.projectkorra.attribute.AttributePriority;
import com.projectkorra.projectkorra.configuration.ConfigManager;
import com.projectkorra.projectkorra.event.AbilityEndEvent;
import com.projectkorra.projectkorra.event.AbilityProgressEvent;
import com.projectkorra.projectkorra.event.AbilityStartEvent;
import com.projectkorra.projectkorra.util.FlightHandler;
import com.projectkorra.projectkorra.util.TimeUtil;
/**
* CoreAbility provides default implementation of an Ability, including methods
* to control the life cycle of a specific instance. CoreAbility also provides a
* system to load CoreAbilities within a {@link JavaPlugin}, or located in an
* external {@link JarFile}.
* <p>
* For {@link CollisionManager} and {@link Collision}, a CoreAbility may need to
* override {@link #isCollidable()}, {@link #getCollisionRadius()},
* {@link #handleCollision(Collision)}, and {@link #getLocations()}.
*
* @see #start()
* @see #progress()
* @see #remove()
* @see #registerAddonAbilities(String)
* @see #registerPluginAbilities(JavaPlugin, String)
*/
public abstract class CoreAbility implements Ability {
private static final Set<CoreAbility> INSTANCES = Collections.newSetFromMap(new ConcurrentHashMap<CoreAbility, Boolean>());
private static final Map<Class<? extends CoreAbility>, Map<UUID, Map<Integer, CoreAbility>>> INSTANCES_BY_PLAYER = new ConcurrentHashMap<>();
private static final Map<Class<? extends CoreAbility>, Set<CoreAbility>> INSTANCES_BY_CLASS = new ConcurrentHashMap<>();
private static final Map<String, CoreAbility> ABILITIES_BY_NAME = new ConcurrentSkipListMap<>(); // preserves ordering.
private static final Map<Class<? extends CoreAbility>, CoreAbility> ABILITIES_BY_CLASS = new ConcurrentHashMap<>();
private static final double DEFAULT_COLLISION_RADIUS = 0.3;
private static final List<String> ADDON_PLUGINS = new ArrayList<>();
private static final Map<Class<? extends CoreAbility>, Map<String, Field>> ATTRIBUTE_FIELDS = new HashMap<>();
private static int idCounter;
private static long currentTick;
protected Player player;
protected BendingPlayer bPlayer;
protected FlightHandler flightHandler;
private final Map<String, Map<AttributePriority, Set<Pair<Number, AttributeModifier>>>> attributeModifiers = new HashMap<>();
private final Map<String, Object> attributeValues = new HashMap<>();
private boolean started;
private boolean removed;
private boolean hidden;
private int id;
private long startTime;
private long startTick;
private boolean attributesModified;
static {
idCounter = Integer.MIN_VALUE;
}
/**
* The default constructor is needed to create a fake instance of each
* CoreAbility via reflection in {@link #registerAbilities()}. More
* specifically, {@link #registerPluginAbilities} calls
* getDeclaredConstructor which is only usable with a public default
* constructor. Reflection lets us create a list of all of the plugin's
* abilities when the plugin first loads.
*
* @see #ABILITIES_BY_NAME
* @see #getAbility(String)
*/
public CoreAbility() {
for (final Field field : this.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Attribute.class)) {
final Attribute attribute = field.getAnnotation(Attribute.class);
if (!ATTRIBUTE_FIELDS.containsKey(this.getClass())) {
ATTRIBUTE_FIELDS.put(this.getClass(), new HashMap<>());
}
ATTRIBUTE_FIELDS.get(this.getClass()).put(attribute.value(), field);
}
}
}
/**
* Creates a new CoreAbility instance but does not start it.
*
* @param player the non-null player that created this instance
* @see #start()
*/
public CoreAbility(final Player player) {
if (player == null || !this.isEnabled()) {
return;
}
this.player = player;
this.bPlayer = BendingPlayer.getBendingPlayer(player);
this.flightHandler = Manager.getManager(FlightHandler.class);
this.startTime = System.currentTimeMillis();
this.started = false;
this.id = CoreAbility.idCounter;
if (idCounter == Integer.MAX_VALUE) {
idCounter = Integer.MIN_VALUE;
} else {
idCounter++;
}
}
/**
* Causes the ability to begin updating every tick by calling
* {@link #progress()} until {@link #remove()} is called. This method cannot
* be overridden, and any code that needs to be performed before start
* should be handled in the constructor.
*
* @see #getStartTime()
* @see #isStarted()
* @see #isRemoved()
*/
public void start() {
if (this.player == null || !this.isEnabled()) {
return;
}
final AbilityStartEvent event = new AbilityStartEvent(this);
Bukkit.getServer().getPluginManager().callEvent(event);
if (event.isCancelled()) {
this.remove();
return;
}
this.started = true;
this.startTime = System.currentTimeMillis();
this.startTick = getCurrentTick();
final Class<? extends CoreAbility> clazz = this.getClass();
final UUID uuid = this.player.getUniqueId();
if (!INSTANCES_BY_PLAYER.containsKey(clazz)) {
INSTANCES_BY_PLAYER.put(clazz, new ConcurrentHashMap<UUID, Map<Integer, CoreAbility>>());
}
if (!INSTANCES_BY_PLAYER.get(clazz).containsKey(uuid)) {
INSTANCES_BY_PLAYER.get(clazz).put(uuid, new ConcurrentHashMap<Integer, CoreAbility>());
}
if (!INSTANCES_BY_CLASS.containsKey(clazz)) {
INSTANCES_BY_CLASS.put(clazz, Collections.newSetFromMap(new ConcurrentHashMap<CoreAbility, Boolean>()));
}
INSTANCES_BY_PLAYER.get(clazz).get(uuid).put(this.id, this);
INSTANCES_BY_CLASS.get(clazz).add(this);
INSTANCES.add(this);
}
/**
* Causes this CoreAbility instance to be removed, and {@link #progress}
* will no longer be called every tick. If this method is overridden then
* the new method must call <b>super.remove()</b>.
*
* {@inheritDoc}
*
* @see #isRemoved()
*/
@Override
public void remove() {
if (this.player == null) {
return;
}
Bukkit.getServer().getPluginManager().callEvent(new AbilityEndEvent(this));
this.removed = true;
final Map<UUID, Map<Integer, CoreAbility>> classMap = INSTANCES_BY_PLAYER.get(this.getClass());
if (classMap != null) {
final Map<Integer, CoreAbility> playerMap = classMap.get(this.player.getUniqueId());
if (playerMap != null) {
playerMap.remove(this.id);
if (playerMap.size() == 0) {
classMap.remove(this.player.getUniqueId());
}
}
if (classMap.size() == 0) {
INSTANCES_BY_PLAYER.remove(this.getClass());
}
}
if (INSTANCES_BY_CLASS.containsKey(this.getClass())) {
INSTANCES_BY_CLASS.get(this.getClass()).remove(this);
}
INSTANCES.remove(this);
}
/**
* Causes {@link #progress()} to be called on every CoreAbility instance
* that has been started and has not been removed.
*/
public static void progressAll() {
for (final Set<CoreAbility> setAbils : INSTANCES_BY_CLASS.values()) {
for (final CoreAbility abil : setAbils) {
if (abil instanceof PassiveAbility) {
if (!((PassiveAbility) abil).isProgressable()) {
continue;
}
if (!abil.getPlayer().isOnline()) { // This has to be before isDead as isDead.
abil.remove(); // will return true if they are offline.
continue;
} else if (abil.getPlayer().isDead()) {
continue;
}
} else if (abil.getPlayer().isDead()) {
abil.remove();
continue;
} else if (!abil.getPlayer().isOnline()) {
abil.remove();
continue;
}
try {
if (!abil.attributesModified) {
abil.modifyAttributes();
abil.attributesModified = true;
}
try (MCTiming timing = ProjectKorra.timing(abil.getName()).startTiming()) {
abil.progress();
}
Bukkit.getServer().getPluginManager().callEvent(new AbilityProgressEvent(abil));
} catch (final Exception e) {
e.printStackTrace();
Bukkit.getLogger().severe(abil.toString());
try {
abil.getPlayer().sendMessage(ChatColor.YELLOW + "[" + new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(new Date()) + "] " + ChatColor.RED + "There was an error running " + abil.getName() + ". please notify the server owner describing exactly what you were doing at this moment");
} catch (final Exception me) {
Bukkit.getLogger().severe("unable to notify ability user of error");
}
try {
abil.remove();
} catch (final Exception re) {
Bukkit.getLogger().severe("unable to fully remove ability of above error");
}
}
}
}
currentTick++;
}
/**
* Removes every CoreAbility instance that has been started but not yet
* removed.
*/
public static void removeAll() {
for (final Set<CoreAbility> setAbils : INSTANCES_BY_CLASS.values()) {
for (final CoreAbility abil : setAbils) {
abil.remove();
}
}
for (final CoreAbility coreAbility : ABILITIES_BY_NAME.values()) {
if (coreAbility instanceof AddonAbility) {
final AddonAbility addon = (AddonAbility) coreAbility;
addon.stop();
}
}
}
/**
* Returns any T CoreAbility that has been started and not yet removed. May
* return null if no such ability exists.
*
* @param player the player that created the CoreAbility instance
* @param clazz the class of the type of CoreAbility
* @return a CoreAbility instance or null
*/
public static <T extends CoreAbility> T getAbility(final Player player, final Class<T> clazz) {
final Collection<T> abils = getAbilities(player, clazz);
if (abils.iterator().hasNext()) {
return abils.iterator().next();
}
return null;
}
/**
* Returns a "fake" instance for the CoreAbility represented by abilityName.
* This method does not look into CoreAbility instances that were created by
* Players, instead this method looks at the CoreAbilities that were created
* via Reflection by {@link #registerAbilities()} when the plugin was first
* loaded.
*
* <p>
* These "fake" instances have a null player, but methods such as
* {@link Ability#getName()}, and {@link Ability#getElement()} will still
* work, as will checking the type of the ability with instanceof.
*
* <p>
* CoreAbility coreAbil = getAbility(someString); <br>
* if (coreAbil instanceof FireAbility && coreAbil.isSneakAbility())
*
* @param abilityName the name of a loaded CoreAbility
* @return a "fake" CoreAbility instance, or null if no such ability exists
*/
public static CoreAbility getAbility(final String abilityName) {
return abilityName != null ? ABILITIES_BY_NAME.get(abilityName.toLowerCase()) : null;
}
/**
* Returns a "fake" instance for a CoreAbility with the specific class.
*
* @param clazz the class for the type of CoreAbility to be returned
* @return a "fake" CoreAbility instance or null if the ability doesn't exist or <b>isn't enabled</b>
*/
public static CoreAbility getAbility(final Class<? extends CoreAbility> clazz) {
return clazz != null ? ABILITIES_BY_CLASS.get(clazz) : null;
}
/**
* @return a list of "fake" instances for each ability that was loaded by
* {@link #registerAbilities()}
*/
public static ArrayList<CoreAbility> getAbilities() {
return new ArrayList<CoreAbility>(ABILITIES_BY_CLASS.values());
}
/**
* @return a list of "fake" instances for each ability that was loaded by
* {@link #registerAbilities()}
*/
public static ArrayList<CoreAbility> getAbilitiesByName() {
return new ArrayList<CoreAbility>(ABILITIES_BY_NAME.values());
}
/**
* Returns a Collection of all of the player created instances for a
* specific type of CoreAbility.
*
* @param clazz the class for the type of CoreAbilities
* @return a Collection of real instances
*/
public static <T extends CoreAbility> Collection<T> getAbilities(final Class<T> clazz) {
if (clazz == null || INSTANCES_BY_CLASS.get(clazz) == null || INSTANCES_BY_CLASS.get(clazz).size() == 0) {
return Collections.emptySet();
}
return (Collection<T>) CoreAbility.INSTANCES_BY_CLASS.get(clazz);
}
/**
* Returns a Collection of specific CoreAbility instances that were created
* by the specified player.
*
* @param player the player that created the instances
* @param clazz the class for the type of CoreAbilities
* @return a Collection of real instances
*/
public static <T extends CoreAbility> Collection<T> getAbilities(final Player player, final Class<T> clazz) {
if (player == null || clazz == null || INSTANCES_BY_PLAYER.get(clazz) == null || INSTANCES_BY_PLAYER.get(clazz).get(player.getUniqueId()) == null) {
return Collections.emptySet();
}
return (Collection<T>) INSTANCES_BY_PLAYER.get(clazz).get(player.getUniqueId()).values();
}
/**
* @return a Collection of all of the CoreAbilities that are currently
* alive. Do not modify this Collection.
*/
public static Collection<CoreAbility> getAbilitiesByInstances() {
return INSTANCES;
}
/**
* Returns an List of fake instances that were loaded by
* {@link #registerAbilities()} filtered by Element.
*
* @param element the Element of the loaded abilities
* @return a list of fake CoreAbility instances
*/
public static List<CoreAbility> getAbilitiesByElement(final Element element) {
final ArrayList<CoreAbility> abilities = new ArrayList<CoreAbility>();
if (element != null) {
for (final CoreAbility ability : getAbilities()) {
if (ability.getElement() == element) {
abilities.add(ability);
} else if (ability.getElement() instanceof SubElement) {
final Element parentElement = ((SubElement) ability.getElement()).getParentElement();
if (parentElement == element) {
abilities.add(ability);
}
}
}
}
return abilities;
}
/**
* CoreAbility keeps track of plugins that have registered abilities to use
* for bending reload purposes <br>
* <b>This isn't a simple list, external use isn't recommended</b>
*
* @return a list of entrys with the plugin name and path abilities can be
* found at
*/
public static List<String> getAddonPlugins() {
return ADDON_PLUGINS;
}
/**
* Returns true if the player has an active CoreAbility instance of type T.
*
* @param player the player that created the T instance
* @param clazz the class for the type of CoreAbility
*/
public static <T extends CoreAbility> boolean hasAbility(final Player player, final Class<T> clazz) {
return getAbility(player, clazz) != null;
}
/**
* Unloads the ability
*
* @param clazz Ability class to unload
*/
public static <T extends CoreAbility> void unloadAbility(final Class<T> clazz) {
if (!ABILITIES_BY_CLASS.containsKey(clazz)) {
return;
}
final String name = ABILITIES_BY_CLASS.get(clazz).getName();
for (final CoreAbility abil : INSTANCES) {
if (abil.getName() == name) {
abil.remove();
}
}
ABILITIES_BY_CLASS.remove(clazz);
ABILITIES_BY_NAME.remove(name);
ProjectKorra.log.info("Unloaded ability: " + name);
}
/**
* Returns a Set of all of the players that currently have an active
* instance of clazz.
*
* @param clazz the clazz for the type of CoreAbility
*/
public static Set<Player> getPlayers(final Class<? extends CoreAbility> clazz) {
final HashSet<Player> players = new HashSet<>();
if (clazz != null) {
final Map<UUID, Map<Integer, CoreAbility>> uuidMap = INSTANCES_BY_PLAYER.get(clazz);
if (uuidMap != null) {
for (final UUID uuid : uuidMap.keySet()) {
final Player uuidPlayer = Bukkit.getPlayer(uuid);
if (uuidPlayer != null) {
players.add(uuidPlayer);
}
}
}
}
return players;
}
/**
* Scans and loads plugin CoreAbilities, and Addon CoreAbilities that are
* located in a Jar file inside of the /ProjectKorra/Abilities/ folder.
*/
public static void registerAbilities() {
ABILITIES_BY_NAME.clear();
ABILITIES_BY_CLASS.clear();
registerPluginAbilities(ProjectKorra.plugin, "com.projectkorra");
registerAddonAbilities("/Abilities/");
}
/**
* Scans a JavaPlugin and registers CoreAbility class files. Does not work.
* Use {@link CoreAbility.registerPluginAbilities} instead!
*
* @param plugin a JavaPlugin containing CoreAbility class files
* @param packagePrefix a prefix of the package name, used to increase
* performance
* @see #getAbilities()
* @see #getAbility(String)
*/
@Deprecated
public static void legacyRegisterPluginAbilities(final JavaPlugin plugin, final String packagePrefix) {
final List<String> disabled = new ArrayList<String>(); // this way multiple classes with the same name only show once.
if (plugin == null) {
return;
}
final Class<? extends JavaPlugin> pluginClass = plugin.getClass();
final ClassLoader loader = pluginClass.getClassLoader();
final ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
try {
for (final ClassInfo info : ClassPath.from(loader).getAllClasses()) {
if (!info.getPackageName().startsWith(packagePrefix)) {
continue;
}
Class<?> clazz = null;
try {
clazz = info.load();
if (!CoreAbility.class.isAssignableFrom(clazz) || clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
continue;
}
final Constructor<?> objDef = CoreAbility.class.getDeclaredConstructor();
final Constructor<?> intConstr = rf.newConstructorForSerialization(clazz, objDef);
final CoreAbility ability = (CoreAbility) clazz.cast(intConstr.newInstance());
if (ability == null || ability.getName() == null) {
continue;
} else if (!ability.isEnabled() && !disabled.contains(ability.getName())) {
plugin.getLogger().info(ability.getName() + " is disabled");
disabled.add(ability.getName());
continue;
}
final String name = ability.getName();
ABILITIES_BY_NAME.put(ability.getName().toLowerCase(), ability);
ABILITIES_BY_CLASS.put(ability.getClass(), ability);
if (ability instanceof ComboAbility) {
final ComboAbility combo = (ComboAbility) ability;
if (combo.getCombination() != null) {
ComboManager.getComboAbilities().put(name, new ComboManager.ComboAbilityInfo(name, combo.getCombination(), combo));
ComboManager.getDescriptions().put(name, ability.getDescription());
ComboManager.getInstructions().put(name, ability.getInstructions());
String author = "";
if (ability instanceof AddonAbility) {
author = ((AddonAbility) ability).getAuthor();
}
ComboManager.getAuthors().put(name, author);
}
}
if (ability instanceof MultiAbility) {
final MultiAbility multiAbil = (MultiAbility) ability;
MultiAbilityManager.multiAbilityList.add(new MultiAbilityInfo(name, multiAbil.getMultiAbilities()));
}
if (ability instanceof PassiveAbility) {
ability.setHiddenAbility(true);
PassiveManager.getPassives().put(name, ability);
}
if (ability instanceof AddonAbility) {
final AddonAbility addon = (AddonAbility) ability;
addon.load();
}
} catch (final Exception e) {
e.printStackTrace();
} catch (final Error e) {
e.printStackTrace();
}
}
} catch (final IOException e) {
e.printStackTrace();
}
}
/**
* Scans a JavaPlugin and registers CoreAbility class files.
*
* @param plugin a JavaPlugin containing CoreAbility class files
* @param packageBase a prefix of the package name, used to increase
* performance
* @see #getAbilities()
* @see #getAbility(String)
*/
public static void registerPluginAbilities(final JavaPlugin plugin, final String packageBase) {
final AbilityLoader<CoreAbility> abilityLoader = new AbilityLoader<CoreAbility>(plugin, packageBase);
final List<CoreAbility> loadedAbilities = abilityLoader.load(CoreAbility.class, CoreAbility.class);
final String entry = plugin.getName() + "::" + packageBase;
if (!ADDON_PLUGINS.contains(entry)) {
ADDON_PLUGINS.add(entry);
}
for (final CoreAbility coreAbil : loadedAbilities) {
if (!coreAbil.isEnabled()) {
plugin.getLogger().info(coreAbil.getName() + " is disabled");
continue;
}
final String name = coreAbil.getName();
if (name == null) {
plugin.getLogger().warning("Ability " + coreAbil.getClass().getName() + " has no name?");
continue;
}
try {
ABILITIES_BY_NAME.put(name.toLowerCase(), coreAbil);
ABILITIES_BY_CLASS.put(coreAbil.getClass(), coreAbil);
if (coreAbil instanceof ComboAbility) {
final ComboAbility combo = (ComboAbility) coreAbil;
if (combo.getCombination() != null) {
ComboManager.getComboAbilities().put(name, new ComboManager.ComboAbilityInfo(name, combo.getCombination(), combo));
ComboManager.getDescriptions().put(name, coreAbil.getDescription());
ComboManager.getInstructions().put(name, coreAbil.getInstructions());
}
}
if (coreAbil instanceof MultiAbility) {
final MultiAbility multiAbil = (MultiAbility) coreAbil;
MultiAbilityManager.multiAbilityList.add(new MultiAbilityInfo(name, multiAbil.getMultiAbilities()));
}
if (coreAbil instanceof PassiveAbility) {
PassiveAbility passive = (PassiveAbility) coreAbil;
coreAbil.setHiddenAbility(true);
PassiveManager.getPassives().put(name, coreAbil);
if (!PassiveManager.getPassiveClasses().containsKey(passive)) {
PassiveManager.getPassiveClasses().put(passive, coreAbil.getClass());
}
}
} catch (Exception | Error e) {
plugin.getLogger().warning("The ability " + coreAbil.getName() + " was not able to load, if this message shows again please remove it!");
e.printStackTrace();
ABILITIES_BY_NAME.remove(name.toLowerCase());
ABILITIES_BY_CLASS.remove(coreAbil.getClass());
}
}
}
/**
* Scans all of the Jar files inside of /ProjectKorra/folder and registers
* all of the CoreAbility class files that were found.
*
* @param folder the name of the folder to scan
* @see #getAbilities()
* @see #getAbility(String)
*/
public static void registerAddonAbilities(final String folder) {
final ProjectKorra plugin = ProjectKorra.plugin;
final File path = new File(plugin.getDataFolder().toString() + folder);
if (!path.exists()) {
path.mkdir();
return;
}
final AddonAbilityLoader<CoreAbility> abilityLoader = new AddonAbilityLoader<CoreAbility>(plugin, path);
final List<CoreAbility> loadedAbilities = abilityLoader.load(CoreAbility.class, CoreAbility.class);
for (final CoreAbility coreAbil : loadedAbilities) {
if (!(coreAbil instanceof AddonAbility)) {
plugin.getLogger().warning(coreAbil.getName() + " is an addon ability and must implement the AddonAbility interface");
continue;
} else if (!coreAbil.isEnabled()) {
plugin.getLogger().info(coreAbil.getName() + " is disabled");
continue;
}
final AddonAbility addon = (AddonAbility) coreAbil;
final String name = coreAbil.getName();
try {
addon.load();
ABILITIES_BY_NAME.put(name.toLowerCase(), coreAbil);
ABILITIES_BY_CLASS.put(coreAbil.getClass(), coreAbil);
if (coreAbil instanceof ComboAbility) {
final ComboAbility combo = (ComboAbility) coreAbil;
if (combo.getCombination() != null) {
ComboManager.getComboAbilities().put(name, new ComboManager.ComboAbilityInfo(name, combo.getCombination(), combo));
ComboManager.getDescriptions().put(name, coreAbil.getDescription());
ComboManager.getInstructions().put(name, coreAbil.getInstructions());
ComboManager.getAuthors().put(name, addon.getAuthor());
}
}
if (coreAbil instanceof MultiAbility) {
final MultiAbility multiAbil = (MultiAbility) coreAbil;
MultiAbilityManager.multiAbilityList.add(new MultiAbilityInfo(name, multiAbil.getMultiAbilities()));
}
if (coreAbil instanceof PassiveAbility) {
PassiveAbility passive = (PassiveAbility) coreAbil;
coreAbil.setHiddenAbility(true);
PassiveManager.getPassives().put(name, coreAbil);
if (!PassiveManager.getPassiveClasses().containsKey(passive)) {
PassiveManager.getPassiveClasses().put(passive, coreAbil.getClass());
}
}
} catch (Exception | Error e) {
plugin.getLogger().warning("The ability " + coreAbil.getName() + " was not able to load, if this message shows again please remove it!");
e.printStackTrace();
addon.stop();
ABILITIES_BY_NAME.remove(name.toLowerCase());
ABILITIES_BY_CLASS.remove(coreAbil.getClass());
}
}
}
public long getStartTime() {
return this.startTime;
}
public long getStartTick() {
return this.startTick;
}
public static long getCurrentTick() {
return currentTick;
}
public long getRunningTicks() {
return currentTick - this.startTick;
}
public boolean isStarted() {
return this.started;
}
public boolean isRemoved() {
return this.removed;
}
public BendingPlayer getBendingPlayer() {
return this.bPlayer;
}
public int getId() {
return this.id;
}
@Override
public boolean isHiddenAbility() {
return this.hidden;
}
public void setHiddenAbility(final boolean hidden) {
this.hidden = hidden;
}
@Override
public boolean isEnabled() {
if (this instanceof AddonAbility) {
return true;
}
String elementName = this.getElement().getName();
if (this.getElement() instanceof SubElement) {
elementName = ((SubElement) this.getElement()).getParentElement().getName();
}
String tag = null;
if (this instanceof PassiveAbility) {
tag = "Abilities." + elementName + ".Passive." + this.getName() + ".Enabled";
} else {
tag = "Abilities." + elementName + "." + this.getName() + ".Enabled";
}
if (getConfig().isBoolean(tag)) {
return getConfig().getBoolean(tag);
} else {
return true;
}
}
@Override
public String getInstructions() {
String elementName = this.getElement().getName();
if (this.getElement() instanceof SubElement) {
elementName = ((SubElement) this.getElement()).getParentElement().getName();
}
if (this instanceof ComboAbility) {
elementName = elementName + ".Combo";
}
return ConfigManager.languageConfig.get().contains("Abilities." + elementName + "." + this.getName() + ".Instructions") ? ConfigManager.languageConfig.get().getString("Abilities." + elementName + "." + this.getName() + ".Instructions") : "";
}
@Override
public String getDescription() {
String elementName = this.getElement().getName();
if (this.getElement() instanceof SubElement) {
elementName = ((SubElement) this.getElement()).getParentElement().getName();
}
if (this instanceof PassiveAbility) {
return ConfigManager.languageConfig.get().getString("Abilities." + elementName + ".Passive." + this.getName() + ".Description");
} else if (this instanceof ComboAbility) {
return ConfigManager.languageConfig.get().getString("Abilities." + elementName + ".Combo." + this.getName() + ".Description");
}
return ConfigManager.languageConfig.get().getString("Abilities." + elementName + "." + this.getName() + ".Description");
}
public String getMovePreview(final Player player) {
final BendingPlayer bPlayer = BendingPlayer.getBendingPlayer(player);
String displayedMessage = getMovePreviewWithoutCooldownTimer(player, false);
if (bPlayer.isOnCooldown(this)) {
final long cooldown = bPlayer.getCooldown(this.getName()) - System.currentTimeMillis();
displayedMessage += this.getElement().getColor() + " - " + TimeUtil.formatTime(cooldown);
}
return displayedMessage;
}
public String getMovePreviewWithoutCooldownTimer(final Player player, boolean forceCooldown) {
final BendingPlayer bPlayer = BendingPlayer.getBendingPlayer(player);
String displayedMessage = "";
if (forceCooldown || bPlayer.isOnCooldown(this)) {
displayedMessage = this.getElement().getColor() + "" + ChatColor.STRIKETHROUGH + this.getName();
} else {
boolean isActiveStance = bPlayer.getStance() != null && bPlayer.getStance().getName().equals(this.getName());
boolean isActiveAvatarState = bPlayer.isAvatarState() && this.getName().equals("AvatarState");
boolean isActiveIllumination = bPlayer.isIlluminating() && this.getName().equals("Illumination");
boolean isActiveTremorSense = bPlayer.isTremorSensing() && this.getName().equals("Tremorsense");
if (isActiveStance || isActiveAvatarState || isActiveIllumination || isActiveTremorSense) {
displayedMessage = this.getElement().getColor() + "" + ChatColor.UNDERLINE + this.getName();
} else {
displayedMessage = this.getElement().getColor() + this.getName();
}
}
return displayedMessage;
}
@Override
public Player getPlayer() {
return this.player;
}
/**
* Changes the player that owns this ability instance. Used for redirection
* and other abilities that change the player object.
*
* @param target The player who now controls the ability
*/
public void setPlayer(final Player target) {
if (target == this.player) {
return;
}
final Class<? extends CoreAbility> clazz = this.getClass();
// The mapping from player UUID to a map of the player's instances.
Map<UUID, Map<Integer, CoreAbility>> classMap = INSTANCES_BY_PLAYER.get(clazz);
if (classMap != null) {
// The map of AbilityId to Ability for the current player.
final Map<Integer, CoreAbility> playerMap = classMap.get(this.player.getUniqueId());
if (playerMap != null) {
// Remove the ability from the current player's map.
playerMap.remove(this.id);
if (playerMap.isEmpty()) {
// Remove the player's empty ability map from global instances map.
classMap.remove(this.player.getUniqueId());
}
}
if (classMap.isEmpty()) {
INSTANCES_BY_PLAYER.remove(this.getClass());
}
}
// Add a new map for the current ability if it doesn't exist in the global map.
if (!INSTANCES_BY_PLAYER.containsKey(clazz)) {
INSTANCES_BY_PLAYER.put(clazz, new ConcurrentHashMap<>());
}
classMap = INSTANCES_BY_PLAYER.get(clazz);
// Create an AbilityId to Ability map for the target player if it doesn't exist.
if (!classMap.containsKey(target.getUniqueId())) {
classMap.put(target.getUniqueId(), new ConcurrentHashMap<>());
}
// Add the current instance to the target player's ability map.
classMap.get(target.getUniqueId()).put(this.getId(), this);
this.player = target;
final BendingPlayer newBendingPlayer = BendingPlayer.getBendingPlayer(target);
if (newBendingPlayer != null) {
this.bPlayer = newBendingPlayer;
}
}
/**
* Used by the CollisionManager to check if two instances can collide with
* each other. For example, an EarthBlast is not collidable right when the
* person selects a source block, but it is collidable once the block begins
* traveling.
*
* @return true if the instance is currently collidable
* @see CollisionManager
*/
public boolean isCollidable() {
return true;
}
/**
* The radius for collision of the ability instance. Some circular abilities
* are better represented with 1 single Location with a small or large
* radius, such as AirShield, FireShield, EarthSmash, WaterManipulation,
* EarthBlast, etc. Some abilities consist of multiple Locations with small
* radiuses, such as AirSpout, WaterSpout, Torrent, RaiseEarth, AirSwipe,
* FireKick, etc.
*
* @return the radius for a location returned by {@link #getLocations()}
* @see CollisionManager
*/
public double getCollisionRadius() {
return DEFAULT_COLLISION_RADIUS;
}
/**
* Called when this ability instance collides with another. Some abilities
* may want advanced behavior on a Collision; e.g. FireCombos only remove
* the stream that was hit rather than the entire ability.
* <p>
* collision.getAbilitySecond() - the ability that we are colliding with
* collision.isRemovingFirst() - if this ability should be removed
* <p>
* This ability should only worry about itself because handleCollision will
* be called for the other ability instance as well.
*
* @param collision with data about the other ability instance
* @see CollisionManager
*/
public void handleCollision(final Collision collision) {
if (collision.isRemovingFirst()) {
this.remove();
}
}
/**
* A List of Locations used to represent the ability. Some abilities might
* just be 1 Location with a radius, while some might be multiple Locations
* with small radiuses.
*
* @return a List of the ability's locations
* @see CollisionManager
*/
public List<Location> getLocations() {
final ArrayList<Location> locations = new ArrayList<>();
locations.add(this.getLocation());
return locations;
}
public CoreAbility addAttributeModifier(final String attribute, final Number value, final AttributeModifier modification) {
return this.addAttributeModifier(attribute, value, modification, AttributePriority.MEDIUM);
}
public CoreAbility addAttributeModifier(final String attribute, final Number value, final AttributeModifier modificationType, final AttributePriority priority) {
Validate.notNull(attribute, "attribute cannot be null");
Validate.notNull(value, "value cannot be null");
Validate.notNull(modificationType, "modifierMethod cannot be null");
Validate.notNull(priority, "priority cannot be null");
Validate.isTrue(ATTRIBUTE_FIELDS.containsKey(this.getClass()) && ATTRIBUTE_FIELDS.get(this.getClass()).containsKey(attribute), "Attribute " + attribute + " is not a defined Attribute for " + this.getName());
if (!this.attributeModifiers.containsKey(attribute)) {
this.attributeModifiers.put(attribute, new HashMap<>());
}
if (!this.attributeModifiers.get(attribute).containsKey(priority)) {
this.attributeModifiers.get(attribute).put(priority, new HashSet<>());
}
this.attributeModifiers.get(attribute).get(priority).add(Pair.of(value, modificationType));
return this;
}
public CoreAbility setAttribute(final String attribute, final Object value) {
Validate.notNull(attribute, "attribute cannot be null");
Validate.notNull(value, "value cannot be null");
Validate.isTrue(ATTRIBUTE_FIELDS.containsKey(this.getClass()) && ATTRIBUTE_FIELDS.get(this.getClass()).containsKey(attribute), "Attribute " + attribute + " is not a defined Attribute for " + this.getName());
this.attributeValues.put(attribute, value);
return this;
}
private void modifyAttributes() {
for (final String attribute : this.attributeModifiers.keySet()) {
final Field field = ATTRIBUTE_FIELDS.get(this.getClass()).get(attribute);
final boolean accessibility = field.isAccessible();
field.setAccessible(true);
try {
for (final AttributePriority priority : AttributePriority.values()) {
if (this.attributeModifiers.get(attribute).containsKey(priority)) {
for (final Pair<Number, AttributeModifier> pair : this.attributeModifiers.get(attribute).get(priority)) {
final Object get = field.get(this);
Validate.isTrue(get instanceof Number, "The field " + field.getName() + " cannot algebraically be modified.");
final Number oldValue = (Number) field.get(this);
final Number newValue = pair.getRight().performModification(oldValue, pair.getLeft());
field.set(this, newValue);
}
}
}
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
} finally {
field.setAccessible(accessibility);
}
}
this.attributeValues.forEach((attribute, value) -> {
final Field field = ATTRIBUTE_FIELDS.get(this.getClass()).get(attribute);
final boolean accessibility = field.isAccessible();
field.setAccessible(true);
try {
field.set(this, value);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
} finally {
field.setAccessible(accessibility);
}
});
}
/**
* @return the current FileConfiguration for the plugin
*/
public static FileConfiguration getConfig() {
return ConfigManager.getConfig();
}
/**
* @return the language.yml for the plugin
*/
public static FileConfiguration getLanguageConfig() {
return ConfigManager.languageConfig.get();
}
/**
* Returns a String used to debug potential CoreAbility memory that can be
* caused by a developer forgetting to call {@link #remove()}
*/
public static String getDebugString() {
final StringBuilder sb = new StringBuilder();
int playerCounter = 0;
final HashMap<String, Integer> classCounter = new HashMap<>();
for (final Map<UUID, Map<Integer, CoreAbility>> map1 : INSTANCES_BY_PLAYER.values()) {
playerCounter++;
for (final Map<Integer, CoreAbility> map2 : map1.values()) {
for (final CoreAbility coreAbil : map2.values()) {
final String simpleName = coreAbil.getClass().getSimpleName();
if (classCounter.containsKey(simpleName)) {
classCounter.put(simpleName, classCounter.get(simpleName) + 1);
} else {
classCounter.put(simpleName, 1);
}
}
}
}
for (final Set<CoreAbility> set : INSTANCES_BY_CLASS.values()) {
for (final CoreAbility coreAbil : set) {
final String simpleName = coreAbil.getClass().getSimpleName();
if (classCounter.containsKey(simpleName)) {
classCounter.put(simpleName, classCounter.get(simpleName) + 1);
} else {
classCounter.put(simpleName, 1);
}
}
}
sb.append("Class->UUID's in memory: " + playerCounter + "\n");
sb.append("Abilities in memory:\n");
for (final String className : classCounter.keySet()) {
sb.append(className + ": " + classCounter.get(className) + "\n");
}
return sb.toString();
}
public static double getDefaultCollisionRadius() {
return DEFAULT_COLLISION_RADIUS;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}