diff --git a/pom.xml b/pom.xml index 3d937b95..75549fb5 100644 --- a/pom.xml +++ b/pom.xml @@ -172,7 +172,7 @@ - net.Indyuce.mmocore.lib + com.bekvon.bukkit.residence Residence 4.8.7.2 system @@ -180,7 +180,7 @@ - net.Indyuce.mmocore.lib + com.Zrips.CMI CMI 8.6.5.0 system @@ -188,7 +188,7 @@ - net.Indyuce.mmocore.lib + com.sainttx.holograms Holograms 2.9.1 system @@ -196,7 +196,7 @@ - net.Indyuce.mmocore.lib + com.gmail.filoghost HolographicDisplays 2.4.6 system @@ -204,7 +204,7 @@ - net.Indyuce.mmocore.lib + net.citizensnpcs Citizens 2.0.25 system @@ -212,7 +212,7 @@ - net.Indyuce.mmocore.lib + me.vagdedes.spartan SpartanAPI 1.0 system diff --git a/src/main/java/net/Indyuce/mmocore/MMOCore.java b/src/main/java/net/Indyuce/mmocore/MMOCore.java index 6705044c..63d35289 100644 --- a/src/main/java/net/Indyuce/mmocore/MMOCore.java +++ b/src/main/java/net/Indyuce/mmocore/MMOCore.java @@ -225,7 +225,7 @@ public class MMOCore extends LuminePlugin { } /* - * resource regeneration. must check if entity is dead otherwise regen will make + * Resource regeneration. Must check if entity is dead otherwise regen will make * the 'respawn' button glitched plus HURT entity effect bug */ new BukkitRunnable() { @@ -240,7 +240,7 @@ public class MMOCore extends LuminePlugin { }.runTaskTimer(MMOCore.plugin, 100, 20); /* - * clean active loot chests every 5 minutes. cannot register this runnable in + * Clean active loot chests every 5 minutes. Cannot register this runnable in * the loot chest manager because it is instanced when the plugin loads */ new BukkitRunnable() { @@ -255,7 +255,7 @@ public class MMOCore extends LuminePlugin { * Stamina Addon...This should prevent a couple error reports produced by people * not reading the installation guide... */ - if (Bukkit.getPluginManager().getPlugin("MMOItemsMana") != null) { + if (Bukkit.getPluginManager().getPlugin("MMOMana") != null) { getLogger().log(Level.SEVERE, ChatColor.DARK_RED + "MMOCore is not meant to be used with MMOItems ManaAndStamina"); getLogger().log(Level.SEVERE, ChatColor.DARK_RED + "Please read the installation guide!"); Bukkit.broadcastMessage(ChatColor.DARK_RED + "[MMOCore] MMOCore is not meant to be used with MMOItems ManaAndStamina"); @@ -297,20 +297,16 @@ public class MMOCore extends LuminePlugin { Bukkit.getPluginManager().registerEvents(new PlayerCollectStats(), this); /* - * initialize player data from all online players. this is very important to do + * Initialize player data from all online players. This is very important to do * that after registering all the professses otherwise the player datas can't * recognize what profess the player has and professes will be lost */ Bukkit.getOnlinePlayers().forEach(player -> dataProvider.getDataManager().setup(player.getUniqueId())); - /* - * load guild data after loading player data - */ + // load guild data after loading player data dataProvider.getGuildManager().load(); - /* - * commands - */ + // Command try { final Field bukkitCommandMap = Bukkit.getServer().getClass().getDeclaredField("commandMap"); diff --git a/src/main/java/net/Indyuce/mmocore/api/event/AsyncPlayerDataLoadEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/AsyncPlayerDataLoadEvent.java new file mode 100644 index 00000000..e2ff20fa --- /dev/null +++ b/src/main/java/net/Indyuce/mmocore/api/event/AsyncPlayerDataLoadEvent.java @@ -0,0 +1,27 @@ +package net.Indyuce.mmocore.api.event; + +import net.Indyuce.mmocore.api.player.PlayerData; +import org.bukkit.event.HandlerList; + +public class AsyncPlayerDataLoadEvent extends PlayerDataEvent { + private static final HandlerList handlers = new HandlerList(); + + /** + * Called when a player data is being loaded into the game. + * This event is called async. + * + * @param playerData Player data being loaded + */ + public AsyncPlayerDataLoadEvent(PlayerData playerData) { + super(playerData); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/net/Indyuce/mmocore/api/event/PlayerAttributeUseEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/PlayerAttributeUseEvent.java index eb9e63b4..45c53a4b 100644 --- a/src/main/java/net/Indyuce/mmocore/api/event/PlayerAttributeUseEvent.java +++ b/src/main/java/net/Indyuce/mmocore/api/event/PlayerAttributeUseEvent.java @@ -1,15 +1,29 @@ package net.Indyuce.mmocore.api.event; import net.Indyuce.mmocore.api.player.PlayerData; +import net.Indyuce.mmocore.api.player.attribute.PlayerAttribute; import org.bukkit.event.HandlerList; -public class PlayerAttributeUseEvent extends PlayerDataEvent{ +public class PlayerAttributeUseEvent extends PlayerDataEvent { private static final HandlerList handlers = new HandlerList(); - public PlayerAttributeUseEvent(PlayerData playerData) { + private final PlayerAttribute attribute; + + /** + * Called when a player increases an attribute using the attribute viewer GUI + * + * @param playerData PLayer increasing his attribute + * @param attribute Attribute being increased + */ + public PlayerAttributeUseEvent(PlayerData playerData, PlayerAttribute attribute) { super(playerData); + + this.attribute = attribute; } + public PlayerAttribute getAttribute() { + return attribute; + } @Override public HandlerList getHandlers() { diff --git a/src/main/java/net/Indyuce/mmocore/api/event/PlayerCombatEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/PlayerCombatEvent.java index 2f93b885..6bdde032 100644 --- a/src/main/java/net/Indyuce/mmocore/api/event/PlayerCombatEvent.java +++ b/src/main/java/net/Indyuce/mmocore/api/event/PlayerCombatEvent.java @@ -1,30 +1,36 @@ package net.Indyuce.mmocore.api.event; +import net.Indyuce.mmocore.api.player.PlayerData; import org.bukkit.event.HandlerList; -import net.Indyuce.mmocore.api.player.PlayerData; - public class PlayerCombatEvent extends PlayerDataEvent { - private static final HandlerList handlers = new HandlerList(); + private static final HandlerList handlers = new HandlerList(); - private final boolean enter; + private final boolean enter; - public PlayerCombatEvent(PlayerData playerData, boolean enter) { - super(playerData); + /** + * Called when a player either enters or leaves combat + * by dealing damage to, or being hit by another entity + * + * @param playerData Player interacting + * @param enter If the player is entering combt + */ + public PlayerCombatEvent(PlayerData playerData, boolean enter) { + super(playerData); - this.enter = enter; - } + this.enter = enter; + } - public boolean entersCombat() { - return enter; - } + public boolean entersCombat() { + return enter; + } - @Override - public HandlerList getHandlers() { - return handlers; - } + @Override + public HandlerList getHandlers() { + return handlers; + } - public static HandlerList getHandlerList() { - return handlers; - } + public static HandlerList getHandlerList() { + return handlers; + } } diff --git a/src/main/java/net/Indyuce/mmocore/api/event/PlayerDataLoadEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/PlayerDataLoadEvent.java index 69fa797e..96ca55a2 100644 --- a/src/main/java/net/Indyuce/mmocore/api/event/PlayerDataLoadEvent.java +++ b/src/main/java/net/Indyuce/mmocore/api/event/PlayerDataLoadEvent.java @@ -1,22 +1,30 @@ package net.Indyuce.mmocore.api.event; +import net.Indyuce.mmocore.api.player.PlayerData; import org.bukkit.event.HandlerList; -import net.Indyuce.mmocore.api.player.PlayerData; - +/** + * @deprecated Use {@link AsyncPlayerDataLoadEvent} instead + */ +@Deprecated public class PlayerDataLoadEvent extends PlayerDataEvent { - private static final HandlerList handlers = new HandlerList(); + private static final HandlerList handlers = new HandlerList(); - public PlayerDataLoadEvent(PlayerData playerData) { - super(playerData); - } + /** + * Called when a player data is being loaded into the game. + * + * @param playerData Player data being loaded + */ + public PlayerDataLoadEvent(PlayerData playerData) { + super(playerData); + } - @Override - public HandlerList getHandlers() { - return handlers; - } + @Override + public HandlerList getHandlers() { + return handlers; + } - public static HandlerList getHandlerList() { - return handlers; - } + public static HandlerList getHandlerList() { + return handlers; + } } diff --git a/src/main/java/net/Indyuce/mmocore/api/event/PlayerPostCastSkillEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/PlayerPostCastSkillEvent.java index 88e59905..8270e463 100644 --- a/src/main/java/net/Indyuce/mmocore/api/event/PlayerPostCastSkillEvent.java +++ b/src/main/java/net/Indyuce/mmocore/api/event/PlayerPostCastSkillEvent.java @@ -1,44 +1,48 @@ package net.Indyuce.mmocore.api.event; -import org.bukkit.event.HandlerList; - import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.api.skill.Skill.SkillInfo; import net.Indyuce.mmocore.api.skill.SkillResult; +import org.bukkit.event.HandlerList; public class PlayerPostCastSkillEvent extends PlayerDataEvent { - private static final HandlerList handlers = new HandlerList(); + private static final HandlerList handlers = new HandlerList(); - private final SkillInfo skill; - private final SkillResult result; - private final boolean successful; + private final SkillInfo skill; + private final SkillResult result; - public PlayerPostCastSkillEvent(PlayerData playerData, SkillInfo skill, SkillResult result, boolean successful) { - super(playerData); - - this.skill = skill; - this.result = result; - this.successful = successful; - } + /** + * Called right after a player casts a skill. + * + * @param playerData Player casting the skill + * @param skill Skill being cast + * @param result SKill casting result + */ + public PlayerPostCastSkillEvent(PlayerData playerData, SkillInfo skill, SkillResult result) { + super(playerData); - public SkillInfo getCast() { - return skill; - } + this.skill = skill; + this.result = result; + } - public SkillResult getResult() { - return result; - } - - public boolean wasSuccessful() { - return successful; - } + public SkillInfo getCast() { + return skill; + } - @Override - public HandlerList getHandlers() { - return handlers; - } + public SkillResult getResult() { + return result; + } - public static HandlerList getHandlerList() { - return handlers; - } + public boolean wasSuccessful() { + return result.isSuccessful(); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } } diff --git a/src/main/java/net/Indyuce/mmocore/api/event/PlayerPreCastSkillEvent.java b/src/main/java/net/Indyuce/mmocore/api/event/PlayerPreCastSkillEvent.java index b1665959..19f48876 100644 --- a/src/main/java/net/Indyuce/mmocore/api/event/PlayerPreCastSkillEvent.java +++ b/src/main/java/net/Indyuce/mmocore/api/event/PlayerPreCastSkillEvent.java @@ -1,44 +1,50 @@ package net.Indyuce.mmocore.api.event; -import org.bukkit.event.Cancellable; -import org.bukkit.event.HandlerList; - import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.api.skill.Skill.SkillInfo; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; public class PlayerPreCastSkillEvent extends PlayerDataEvent implements Cancellable { - private static final HandlerList handlers = new HandlerList(); + private static final HandlerList handlers = new HandlerList(); - private final SkillInfo skill; + private final SkillInfo skill; - private boolean cancelled; + private boolean cancelled; - public PlayerPreCastSkillEvent(PlayerData playerData, SkillInfo skill) { - super(playerData); - - this.skill = skill; - } + /** + * Called right before a player casts a skill. This occurs before + * checking for mana, stamina costs and ability cooldown. + * + * @param playerData Player casting the skill + * @param skill Skill being cast + */ + public PlayerPreCastSkillEvent(PlayerData playerData, SkillInfo skill) { + super(playerData); - public SkillInfo getCast() { - return skill; - } + this.skill = skill; + } - @Override - public boolean isCancelled() { - return cancelled; - } + public SkillInfo getCast() { + return skill; + } - @Override - public void setCancelled(boolean value) { - cancelled = value; - } + @Override + public boolean isCancelled() { + return cancelled; + } - @Override - public HandlerList getHandlers() { - return handlers; - } + @Override + public void setCancelled(boolean value) { + cancelled = value; + } - public static HandlerList getHandlerList() { - return handlers; - } + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } } diff --git a/src/main/java/net/Indyuce/mmocore/api/loot/LootChest.java b/src/main/java/net/Indyuce/mmocore/api/loot/LootChest.java index 4d242d58..ce489cdc 100644 --- a/src/main/java/net/Indyuce/mmocore/api/loot/LootChest.java +++ b/src/main/java/net/Indyuce/mmocore/api/loot/LootChest.java @@ -1,5 +1,7 @@ package net.Indyuce.mmocore.api.loot; +import net.Indyuce.mmocore.MMOCore; +import net.Indyuce.mmocore.manager.SoundManager; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; @@ -9,97 +11,98 @@ import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; -import net.Indyuce.mmocore.MMOCore; -import net.Indyuce.mmocore.manager.SoundManager; - public class LootChest { - private final ChestTier tier; - private final LootChestRegion region; - private final ReplacedBlock block; - private final BukkitRunnable effectRunnable; - private final long date = System.currentTimeMillis(); + private final ChestTier tier; + private final LootChestRegion region; + private final ReplacedBlock block; + private final BukkitRunnable effectRunnable; + private final long date = System.currentTimeMillis(); - /* - * instance generated when a loot chest is placed (as a bukkit block), and - * used to save the data of the block which has been replaced (can replace - * non-solid blocks) - */ - public LootChest(ChestTier tier, LootChestRegion region, Block block) { - this.tier = tier; - this.region = region; - this.block = new ReplacedBlock(block); - this.effectRunnable = tier.hasEffect() ? tier.getEffect().startNewRunnable(block.getLocation().add(.5, .5, .5)) : null; - } + /** + * Called when a loot chest is placed as a Bukkit block, and used + * to save the data of the block which has been replaced. + *

+ * A placed drop chest may only replace non solid blocks like grass + * or levels.. + */ + public LootChest(ChestTier tier, LootChestRegion region, Block block) { + this.tier = tier; + this.region = region; + this.block = new ReplacedBlock(block); + this.effectRunnable = tier.hasEffect() ? tier.getEffect().startNewRunnable(block.getLocation().add(.5, .5, .5)) : null; + } - public ChestTier getTier() { - return tier; - } + public ChestTier getTier() { + return tier; + } - public ReplacedBlock getBlock() { - return block; - } + public ReplacedBlock getBlock() { + return block; + } - public LootChestRegion getRegion() { - return region; - } + public LootChestRegion getRegion() { + return region; + } - public boolean hasPlayerNearby() { - for (Player player : block.loc.getWorld().getPlayers()) - if (player.getLocation().distanceSquared(block.loc) < 625) - return true; - return false; - } + public boolean hasPlayerNearby() { + for (Player player : block.loc.getWorld().getPlayers()) + if (player.getLocation().distanceSquared(block.loc) < 625) + return true; + return false; + } - public boolean shouldExpire() { - return System.currentTimeMillis() - date > MMOCore.plugin.configManager.lootChestExpireTime; - } + public boolean shouldExpire() { + return System.currentTimeMillis() - date > MMOCore.plugin.configManager.lootChestExpireTime; + } - public void unregister(boolean player) { + /** + * @param player If a player just the chest. It's set to false + * when a loot chest expires or when MMOCore disables. + */ + public void unregister(boolean player) { - /* - * if a player is responsible of closing the chest, close it with sound - */ - if (player) { - MMOCore.plugin.soundManager.play(block.loc.getBlock(), SoundManager.SoundEvent.CLOSE_LOOT_CHEST); - block.loc.getWorld().spawnParticle(Particle.CRIT, block.loc.clone().add(.5, .5, .5), 16, 0, 0, 0, .5); - MMOCore.plugin.lootChests.unregister(this); - } + // If a player is responsible of closing the chest, close it with sound + if (player) { + MMOCore.plugin.soundManager.play(block.loc.getBlock(), SoundManager.SoundEvent.CLOSE_LOOT_CHEST); + block.loc.getWorld().spawnParticle(Particle.CRIT, block.loc.clone().add(.5, .5, .5), 16, 0, 0, 0, .5); + MMOCore.plugin.lootChests.unregister(this); + } - /* - * must clean block inventory before replacing block otherwise loots fly - * off and accumulate on the ground (+during dev phase) - */ - else - ((Chest) block.loc.getBlock().getState()).getBlockInventory().clear(); + /* + * Must clean block inventory before replacing block otherwise loots fly + * off and accumulate on the ground (+during dev phase) + */ + else + ((Chest) block.loc.getBlock().getState()).getBlockInventory().clear(); - block.restore(); - if (effectRunnable != null) - effectRunnable.cancel(); - } + block.restore(); + if (effectRunnable != null) + effectRunnable.cancel(); + } - public static class ReplacedBlock { - private final Material material; - private final BlockData data; - private final Location loc; + public static class ReplacedBlock { + private final Material material; + private final BlockData data; + private final Location loc; - public ReplacedBlock(Block block) { - this.material = block.getType(); - this.data = block.getBlockData(); - this.loc = block.getLocation(); - } + public ReplacedBlock(Block block) { + this.material = block.getType(); + this.data = block.getBlockData(); + this.loc = block.getLocation(); + } - public Location getLocoation() { - return loc; - } + public Location getLocoation() { + return loc; + } - public boolean matches(Location loc) { - return this.loc.getWorld().equals(loc.getWorld()) && this.loc.getBlockX() == loc.getBlockX() && this.loc.getBlockY() == loc.getBlockY() - && this.loc.getBlockZ() == loc.getBlockZ(); - } + public boolean matches(Location loc) { + return this.loc.getWorld().equals(loc.getWorld()) && this.loc.getBlockX() == loc.getBlockX() && this.loc.getBlockY() == loc.getBlockY() + && this.loc.getBlockZ() == loc.getBlockZ(); + } - public void restore() { - loc.getBlock().setType(material); - loc.getBlock().setBlockData(data); - } - } + public void restore() { + loc.getBlock().setType(material); + loc.getBlock().setBlockData(data); + } + } } diff --git a/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java b/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java index d366de27..85892f65 100644 --- a/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java +++ b/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java @@ -43,758 +43,785 @@ import java.util.logging.Level; public class PlayerData extends OfflinePlayerData { - /* - * is updated everytime the player joins the server. it is kept when the - * player is offline so the plugin can use #isOnline to check if the player - * is online - */ - private final MMOPlayerData mmoData; - - /* - * 'profess' can be null, you need to retrieve the player class using the - * getProfess() method which should never return null if the plugin is - * configured right - */ - private PlayerClass profess; - private int level, experience, classPoints, skillPoints, attributePoints, attributeReallocationPoints;// skillReallocationPoints, - private double mana, stamina, stellium; - private Party party; - private Guild guild; - - private final PlayerQuests questData; - private final PlayerStats playerStats; - private final List friends = new ArrayList<>(); - private final Set waypoints = new HashSet<>(); - private final Map skills = new HashMap<>(); - private final List boundSkills = new ArrayList<>(); - private final PlayerProfessions collectSkills = new PlayerProfessions(this); - private final PlayerSkillData skillData = new PlayerSkillData(this); - private final PlayerAttributes attributes = new PlayerAttributes(this); - private final Map classSlots = new HashMap<>(); - - private long lastWaypoint, lastFriendRequest, actionBarTimeOut, lastLootChest; - - /* - * NON-FINAL player data stuff made public to facilitate field change - */ - public int skillGuiDisplayOffset; - public SkillCasting skillCasting; - public boolean nocd; - public CombatRunnable combat; - - private boolean fullyLoaded = false; - - public PlayerData(MMOPlayerData mmoData) { - super(mmoData.getUniqueId()); - - this.mmoData = mmoData; - this.playerStats = new PlayerStats(this); - this.questData = new PlayerQuests(this); - } - - /* - * update all references after /mmocore reload so there can be garbage - * collection with old plugin objects like class or skill instances. - */ - public void update() { - - try { - profess = profess == null ? null : MMOCore.plugin.classManager.get(profess.getId()); - } catch (NullPointerException exception) { - MMOCore.log(Level.SEVERE, "[Userdata] Could not find class " + getProfess().getId() + " while refreshing player data."); - } - - int j = 0; - while (j < boundSkills.size()) - try { - boundSkills.set(j, getProfess().getSkill(boundSkills.get(j).getSkill())); - j++; - } catch (NullPointerException npe1) { - boundSkills.remove(j); - try { - MMOCore.log(Level.SEVERE, "[Userdata] Could not find skill " + boundSkills.get(j).getSkill().getId() + " in class " - + getProfess().getId() + " while refreshing player data."); - } catch (NullPointerException npe2) { - MMOCore.log(Level.SEVERE, - "[Userdata] Could not find unidentified skill in class " + getProfess().getId() + " while refreshing player data."); - } - } - } - - public MMOPlayerData getMMOPlayerData() { - return mmoData; - } - - public List getFriends() { - return friends; - } - - public PlayerProfessions getCollectionSkills() { - return collectSkills; - } - - public PlayerQuests getQuestData() { - return questData; - } - - public Player getPlayer() { - return mmoData.getPlayer(); - } - - @Override - public long getLastLogin() { - return mmoData.getLastLogin(); - } - - public long getLastFriendRequest() { - return lastFriendRequest; - } - - @Override - public int getLevel() { - return Math.max(1, level); - } - - public Party getParty() { - return party; - } - - public boolean hasGuild() { - return guild != null; - } - - public Guild getGuild() { - return guild; - } - - public int getClassPoints() { - return classPoints; - } - - public int getSkillPoints() { - return skillPoints; - } - - /* - * returns level up needed in order to level up - */ - public int getLevelUpExperience() { - return getProfess().getExpCurve().getExperience(getLevel() + 1); - } - - // public int getSkillReallocationPoints() { - // return skillReallocationPoints; - // } - - public int getAttributePoints() { - return attributePoints; - } - - public int getAttributeReallocationPoints() { - return attributeReallocationPoints; - } - - public boolean isOnline() { - return mmoData.isOnline(); - } - - public boolean hasParty() { - return party != null; - } - - public boolean inGuild() { - return guild != null; - } - - // public boolean isOnline() { - // return player.isOnline(); - // } - - public void setLevel(int level) { - this.level = Math.max(1, level); - getStats().updateStats(); - } - - public void takeLevels(int value) { - this.level = Math.max(1, level - value); - getStats().updateStats(); - } - - public void giveLevels(int value, EXPSource source) { - int total = 0; - while (value-- > 0) - total += getProfess().getExpCurve().getExperience(getLevel() + value + 1); - giveExperience(total, source); - } - - public void setExperience(int value) { - experience = Math.max(0, value); - refreshVanillaExp(); - } - - public void refreshVanillaExp() { - if (MMOCore.plugin.configManager.overrideVanillaExp) { - if (!isOnline()) - return; - getPlayer().setLevel(getLevel()); - getPlayer().setExp(Math.max(0, Math.min(1, (float) experience / (float) getLevelUpExperience()))); - } - } - - // public void setSkillReallocationPoints(int value) { - // skillReallocationPoints = Math.max(0, value); - // } - - public void setAttributePoints(int value) { - attributePoints = Math.max(0, value); - } - - public void setAttributeReallocationPoints(int value) { - attributeReallocationPoints = Math.max(0, value); - } - - public void setSkillPoints(int value) { - skillPoints = Math.max(0, value); - } - - public void setClassPoints(int value) { - classPoints = Math.max(0, value); - } - - public boolean hasSavedClass(PlayerClass profess) { - return classSlots.containsKey(profess.getId()); - } - - public Set getSavedClasses() { - return classSlots.keySet(); - } - - public SavedClassInformation getClassInfo(PlayerClass profess) { - return getClassInfo(profess.getId()); - } - - public SavedClassInformation getClassInfo(String profess) { - return classSlots.get(profess); - } - - public void applyClassInfo(PlayerClass profess, SavedClassInformation info) { - classSlots.put(profess.getId(), info); - } - - public void unloadClassInfo(PlayerClass profess) { - classSlots.remove(profess.getId()); - } - - public Set getWaypoints() { - return waypoints; - } - - public boolean hasWaypoint(Waypoint waypoint) { - return waypoint.isDefault() || waypoints.contains(waypoint.getId()); - } - - public void unlockWaypoint(Waypoint waypoint) { - waypoints.add(waypoint.getId()); - } - - public long getWaypointCooldown() { - return Math.max(0, lastWaypoint + 5000 - System.currentTimeMillis()); - } - - /* - * handles the per-player loot chest cooldown. that is to reduce the risk of - * spawning multiple chests in a row around the same player which could - * break the gameplay - */ - public boolean canSpawnLootChest() { - return lastLootChest + MMOCore.plugin.configManager.lootChestPlayerCooldown < System.currentTimeMillis(); - } - - public void applyLootChestCooldown() { - lastLootChest = System.currentTimeMillis(); - } - - public void heal(double heal) { - if (!isOnline()) - return; - double newest = Math.max(0, Math.min(getPlayer().getHealth() + heal, getPlayer().getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue())); - if (getPlayer().getHealth() == newest) - return; - - PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.HEALTH, heal); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) - return; - - getPlayer().setHealth(newest); - } - - public void addFriend(UUID uuid) { - friends.add(uuid); - } - - @Override - public void removeFriend(UUID uuid) { - friends.remove(uuid); - } - - @Override - public boolean hasFriend(UUID uuid) { - return friends.contains(uuid); - } - - public void setParty(Party party) { - this.party = party; - } - - public void setGuild(Guild guild) { - this.guild = guild; - } - - public void log(Level level, String message) { - MMOCore.plugin.getLogger().log(level, "[Userdata:" + (isOnline() ? getPlayer().getName() : "Offline Player") + "] " + message); - } - - public void setLastFriendRequest(long ms) { - lastFriendRequest = Math.max(0, ms); - } - - public void sendFriendRequest(PlayerData target) { - if (!isOnline() || !target.isOnline()) - return; - setLastFriendRequest(System.currentTimeMillis()); - - FriendRequest request = new FriendRequest(this, target); - new ConfigMessage("friend-request").addPlaceholders("player", getPlayer().getName(), "uuid", request.getUniqueId().toString()) - .sendAsJSon(target.getPlayer()); - MMOCore.plugin.requestManager.registerRequest(request); - } - - public void warp(Waypoint waypoint) { - - /* - * this cooldown is only used internally to make sure the player is not - * spamming waypoints. there is no need to reset it when resetting the - * player waypoints data - */ - lastWaypoint = System.currentTimeMillis(); - - giveStellium(-waypoint.getStelliumCost()); - - if (!isOnline()) - return; - new BukkitRunnable() { - final int x = getPlayer().getLocation().getBlockX(); - final int y = getPlayer().getLocation().getBlockY(); - final int z = getPlayer().getLocation().getBlockZ(); - int t; - - public void run() { - if (!isOnline()) - return; - if (getPlayer().getLocation().getBlockX() != x || getPlayer().getLocation().getBlockY() != y - || getPlayer().getLocation().getBlockZ() != z) { - MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_CANCELLED); - MMOCore.plugin.configManager.getSimpleMessage("warping-canceled").send(getPlayer()); - giveStellium(waypoint.getStelliumCost()); - cancel(); - return; - } - - MMOCore.plugin.configManager.getSimpleMessage("warping-comencing", "left", "" + ((120 - t) / 20)).send(getPlayer()); - if (t++ >= 100) { - getPlayer().teleport(waypoint.getLocation()); - getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 20, 1, false, false)); - MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_TELEPORT); - cancel(); - return; - } - - MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_CHARGE, 1, (float) (t / Math.PI * .015 + .5)); - double r = Math.sin((double) t / 100 * Math.PI); - for (double j = 0; j < Math.PI * 2; j += Math.PI / 4) - getPlayer().getLocation().getWorld().spawnParticle(Particle.REDSTONE, - getPlayer().getLocation().add(Math.cos((double) t / 20 + j) * r, (double) t / 50, Math.sin((double) t / 20 + j) * r), 1, - new Particle.DustOptions(Color.PURPLE, 1.25f)); - } - }.runTaskTimer(MMOCore.plugin, 0, 1); - } - - public boolean hasReachedMaxLevel() { - return getProfess().getMaxLevel() > 0 && getLevel() >= getProfess().getMaxLevel(); - } - - public void giveExperience(int value, EXPSource source) { - giveExperience(value, source, null); - } - - /** - * Called when giving experience to a player - * - * @param value Experience to give the player - * @param source How the player earned experience - * @param hologramLocation Location used to display the hologram. If it's null, no - * hologram will be displayed - */ - public void giveExperience(int value, EXPSource source, @Nullable Location hologramLocation) { - if (hasReachedMaxLevel()) { - setExperience(0); - return; - } - - /* - * Handle experience hologram - */ - if (hologramLocation != null && isOnline() && MMOCore.plugin.hasHolograms()) - MMOCore.plugin.hologramSupport.displayIndicator(hologramLocation.add(.5, 1.5, .5), - MMOCore.plugin.configManager.getSimpleMessage("exp-hologram", "exp", "" + value).message(), getPlayer()); - - value = MMOCore.plugin.boosterManager.calculateExp(null, value); - value *= 1 + getStats().getStat(StatType.ADDITIONAL_EXPERIENCE) / 100; - - PlayerExperienceGainEvent event = new PlayerExperienceGainEvent(this, value, source); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) - return; - - experience += event.getExperience(); - - int oldLevel = level, needed; - while (experience >= (needed = getLevelUpExperience())) { - - if (hasReachedMaxLevel()) { - experience = 0; - break; - } - - experience -= needed; - level = getLevel() + 1; - } - - if (level > oldLevel) { - Bukkit.getPluginManager().callEvent(new PlayerLevelUpEvent(this, null, oldLevel, level)); - if (isOnline()) { - new ConfigMessage("level-up").addPlaceholders("level", "" + level).send(getPlayer()); - MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.LEVEL_UP); - new SmallParticleEffect(getPlayer(), Particle.SPELL_INSTANT); - } - getStats().updateStats(); - } - - refreshVanillaExp(); - } - - public int getExperience() { - return experience; - } - - @Override - public PlayerClass getProfess() { - return profess == null ? MMOCore.plugin.classManager.getDefaultClass() : profess; - } - - public void giveMana(double amount) { - double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_MANA), mana + amount)); - if (mana == newest) - return; - - PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.MANA, amount); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) - return; - - mana = newest; - } - - public void giveStamina(double amount) { - double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_STAMINA), stamina + amount)); - if (stamina == newest) - return; - - PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.STAMINA, amount); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) - return; - - stamina = newest; - - } - - public void giveStellium(double amount) { - double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_STELLIUM), stellium + amount)); - if (stellium == newest) - return; - - PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.STELLIUM, amount); - Bukkit.getPluginManager().callEvent(event); - if (event.isCancelled()) - return; - - stellium = newest; - } - - public double getMana() { - return mana; - } - - public double getStamina() { - return stamina; - } - - public double getStellium() { - return stellium; - } - - public PlayerStats getStats() { - return playerStats; - } - - public PlayerAttributes getAttributes() { - return attributes; - } - - public void setMana(double amount) { - mana = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_MANA))); - } - - public void setStamina(double amount) { - stamina = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_STAMINA))); - } - - public void setStellium(double amount) { - stellium = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_STELLIUM))); - } - - public boolean isFullyLoaded() { - return fullyLoaded; - } - - public void setFullyLoaded() { - this.fullyLoaded = true; - } - - public boolean isCasting() { - return skillCasting != null; - } - - /* - * returns if the action bar is not being used to display anything else and - * if the general info action bar can be displayed - */ - public boolean canSeeActionBar() { - return actionBarTimeOut < System.currentTimeMillis(); - } - - public void setActionBarTimeOut(long timeOut) { - actionBarTimeOut = System.currentTimeMillis() + (timeOut * 50); - } - - public void displayActionBar(String message) { - if (!isOnline()) - return; - setActionBarTimeOut(MMOCore.plugin.actionBarManager.getTimeOut()); - getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); - } - - @Deprecated - public void setAttribute(PlayerAttribute attribute, int value) { - setAttribute(attribute.getId(), value); - } - - @Deprecated - public void setAttribute(String id, int value) { - attributes.setBaseAttribute(id, value); - } - - @Deprecated - public Map mapAttributePoints() { - return getAttributes().mapPoints(); - } - - public int getSkillLevel(Skill skill) { - return skills.getOrDefault(skill.getId(), 1); - } - - public void setSkillLevel(Skill skill, int level) { - setSkillLevel(skill.getId(), level); - } - - public void setSkillLevel(String skill, int level) { - skills.put(skill, level); - } - - public void resetSkillLevel(String skill) { - skills.remove(skill); - } - - /* - * better use getProfess().findSkill(skill).isPresent() - */ - @Deprecated - public boolean hasSkillUnlocked(Skill skill) { - return getProfess().hasSkill(skill.getId()) && hasSkillUnlocked(getProfess().getSkill(skill.getId())); - } - - /* - * (bug fix) any skill, when the player has the right level is instantly - * unlocked, therefore one must NOT check if the player has unlocked the - * skill by checking if the skills map contains the skill id as key. this - * only checks if the player has spent any skill point. - */ - public boolean hasSkillUnlocked(SkillInfo info) { - return getLevel() >= info.getUnlockLevel(); - } - - public Map mapSkillLevels() { - return new HashMap<>(skills); - } - - public void giveClassPoints(int value) { - setClassPoints(classPoints + value); - } - - public void giveSkillPoints(int value) { - setSkillPoints(skillPoints + value); - } - - public void giveAttributePoints(int value) { - setAttributePoints(attributePoints + value); - } - - // public void giveSkillReallocationPoints(int value) { - // setSkillReallocationPoints(skillReallocationPoints + value); - // } - - public void giveAttributeReallocationPoints(int value) { - setAttributeReallocationPoints(attributeReallocationPoints + value); - } - - public PlayerSkillData getSkillData() { - return skillData; - } - - public void setClass(PlayerClass profess) { - this.profess = profess; - - // for (Iterator iterator = boundSkills.iterator(); - // iterator.hasNext();) - // if (!getProfess().hasSkill(iterator.next().getSkill())) - // iterator.remove(); - - getStats().updateStats(); - } - - public boolean hasSkillBound(int slot) { - return slot < boundSkills.size(); - } - - public SkillInfo getBoundSkill(int slot) { - return slot >= boundSkills.size() ? null : boundSkills.get(slot); - } - - public void setBoundSkill(int slot, SkillInfo skill) { - if (boundSkills.size() < 6) - boundSkills.add(skill); - else - boundSkills.set(slot, skill); - } - - public void unbindSkill(int slot) { - boundSkills.remove(slot); - } - - public List getBoundSkills() { - return boundSkills; - } - - public boolean isInCombat() { - return combat != null; - } - - public boolean canChooseSubclass() { - for (Subclass subclass : getProfess().getSubclasses()) - if (getLevel() >= subclass.getLevel()) - return true; - return false; - } - - public void updateCombat() { - if (isInCombat()) - combat.update(); - else - combat = new CombatRunnable(this); - } - - public SkillResult cast(Skill skill) { - return cast(getProfess().getSkill(skill)); - } - - public SkillResult cast(SkillInfo skill) { - - PlayerPreCastSkillEvent preEvent = new PlayerPreCastSkillEvent(this, skill); - Bukkit.getPluginManager().callEvent(preEvent); - if (preEvent.isCancelled()) - return new SkillResult(this, skill, CancelReason.OTHER); - - /* - * skill, mana stamina aand cooldown requirements are all calculated in - * the SkillResult instances. this cast(SkillResult) method only applies - * cooldown, reduces mana and/or stamina and send messages - */ - SkillResult cast = skill.getSkill().whenCast(this, skill); - if (!cast.isSuccessful()) { - if (!skill.getSkill().isPassive() && isOnline()) { - if (cast.getCancelReason() == CancelReason.LOCKED) - MMOCore.plugin.configManager.getSimpleMessage("not-unlocked-skill").send(getPlayer()); - - if (cast.getCancelReason() == CancelReason.MANA) - MMOCore.plugin.configManager.getSimpleMessage("casting.no-mana").send(getPlayer()); - - if (cast.getCancelReason() == CancelReason.STAMINA) - MMOCore.plugin.configManager.getSimpleMessage("casting.no-stamina").send(getPlayer()); - - if (cast.getCancelReason() == CancelReason.COOLDOWN) - MMOCore.plugin.configManager.getSimpleMessage("casting.on-cooldown").send(getPlayer()); - } - - PlayerPostCastSkillEvent postEvent = new PlayerPostCastSkillEvent(this, skill, cast, false); - Bukkit.getPluginManager().callEvent(postEvent); - return cast; - } - - if (!nocd) { - double flatCooldownReduction = Math.max(0, Math.min(1, getStats().getStat(StatType.COOLDOWN_REDUCTION) / 100)); - flatCooldownReduction *= flatCooldownReduction > 0 ? skill.getModifier("cooldown", getSkillLevel(skill.getSkill())) * 1000 : 0; - - skillData.setLastCast(cast.getSkill(), System.currentTimeMillis() - (long) flatCooldownReduction); - giveMana(-cast.getManaCost()); - giveStamina(-cast.getStaminaCost()); - } - - PlayerPostCastSkillEvent postEvent = new PlayerPostCastSkillEvent(this, skill, cast, true); - Bukkit.getPluginManager().callEvent(postEvent); - return cast; - } - - @Override - public int hashCode() { - return mmoData.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return obj instanceof PlayerData && ((PlayerData) obj).getUniqueId().equals(getUniqueId()); - } - - public static PlayerData get(OfflinePlayer player) { - return get(player.getUniqueId()); - } - - public static PlayerData get(UUID uuid) { - return MMOCore.plugin.dataProvider.getDataManager().get(uuid); - } - - public static Collection getAll() { - return MMOCore.plugin.dataProvider.getDataManager().getLoaded(); - } + /** + * Corresponds to the MythicLib player data. It is used to keep + * track of the Player instance corresponding to that player data, + * as well as other things like the last time the player logged in/out + */ + private final MMOPlayerData mmoData; + + /** + * Can be null, the {@link #getProfess()} method will return the + * player class, or the default one if this field is null. + */ + @Nullable + private PlayerClass profess; + private int level, experience, classPoints, skillPoints, attributePoints, attributeReallocationPoints;// skillReallocationPoints, + private double mana, stamina, stellium; + private Party party; + private Guild guild; + + private final PlayerQuests questData; + private final PlayerStats playerStats; + private final List friends = new ArrayList<>(); + private final Set waypoints = new HashSet<>(); + private final Map skills = new HashMap<>(); + private final List boundSkills = new ArrayList<>(); + private final PlayerProfessions collectSkills = new PlayerProfessions(this); + private final PlayerSkillData skillData = new PlayerSkillData(this); + private final PlayerAttributes attributes = new PlayerAttributes(this); + private final Map classSlots = new HashMap<>(); + + private long lastWaypoint, lastFriendRequest, actionBarTimeOut, lastLootChest; + + // NON-FINAL player data stuff made public to facilitate field change + public int skillGuiDisplayOffset; + public SkillCasting skillCasting; + public boolean nocd; + public CombatRunnable combat; + + /** + * Player data is stored in the data map before it's actually fully loaded + * so that external plugins don't necessarily have to listen to the PlayerDataLoadEvent. + */ + private boolean fullyLoaded = false; + + public PlayerData(MMOPlayerData mmoData) { + super(mmoData.getUniqueId()); + + this.mmoData = mmoData; + this.playerStats = new PlayerStats(this); + this.questData = new PlayerQuests(this); + } + + /** + * Update all references after /mmocore reload so there can be garbage + * collection with old plugin objects like class or skill instances. + */ + public void update() { + + try { + profess = profess == null ? null : MMOCore.plugin.classManager.get(profess.getId()); + } catch (NullPointerException exception) { + MMOCore.log(Level.SEVERE, "[Userdata] Could not find class " + getProfess().getId() + " while refreshing player data."); + } + + int j = 0; + while (j < boundSkills.size()) + try { + boundSkills.set(j, getProfess().getSkill(boundSkills.get(j).getSkill())); + j++; + } catch (NullPointerException npe1) { + boundSkills.remove(j); + try { + MMOCore.log(Level.SEVERE, "[Userdata] Could not find skill " + boundSkills.get(j).getSkill().getId() + " in class " + + getProfess().getId() + " while refreshing player data."); + } catch (NullPointerException npe2) { + MMOCore.log(Level.SEVERE, + "[Userdata] Could not find unidentified skill in class " + getProfess().getId() + " while refreshing player data."); + } + } + } + + public MMOPlayerData getMMOPlayerData() { + return mmoData; + } + + public List getFriends() { + return friends; + } + + public PlayerProfessions getCollectionSkills() { + return collectSkills; + } + + public PlayerQuests getQuestData() { + return questData; + } + + public Player getPlayer() { + return mmoData.getPlayer(); + } + + @Override + public long getLastLogin() { + return mmoData.getLastLogin(); + } + + public long getLastFriendRequest() { + return lastFriendRequest; + } + + @Override + public int getLevel() { + return Math.max(1, level); + } + + public Party getParty() { + return party; + } + + public boolean hasGuild() { + return guild != null; + } + + public Guild getGuild() { + return guild; + } + + public int getClassPoints() { + return classPoints; + } + + public int getSkillPoints() { + return skillPoints; + } + + /** + * @return Experience needed in order to reach next level + */ + public int getLevelUpExperience() { + return getProfess().getExpCurve().getExperience(getLevel() + 1); + } + + // public int getSkillReallocationPoints() { + // return skillReallocationPoints; + // } + + public int getAttributePoints() { + return attributePoints; + } + + public int getAttributeReallocationPoints() { + return attributeReallocationPoints; + } + + public boolean isOnline() { + return mmoData.isOnline(); + } + + public boolean hasParty() { + return party != null; + } + + public boolean inGuild() { + return guild != null; + } + + public void setLevel(int level) { + this.level = Math.max(1, level); + getStats().updateStats(); + } + + public void takeLevels(int value) { + this.level = Math.max(1, level - value); + getStats().updateStats(); + } + + public void giveLevels(int value, EXPSource source) { + int total = 0; + while (value-- > 0) + total += getProfess().getExpCurve().getExperience(getLevel() + value + 1); + giveExperience(total, source); + } + + public void setExperience(int value) { + experience = Math.max(0, value); + refreshVanillaExp(); + } + + /** + * Class experience can be displayed on the player's exp bar. + * This updates the exp bar to display the player class level and exp. + */ + public void refreshVanillaExp() { + if (!isOnline() || !MMOCore.plugin.configManager.overrideVanillaExp) + return; + + getPlayer().setLevel(getLevel()); + getPlayer().setExp(Math.max(0, Math.min(1, (float) experience / (float) getLevelUpExperience()))); + } + + // public void setSkillReallocationPoints(int value) { + // skillReallocationPoints = Math.max(0, value); + // } + + public void setAttributePoints(int value) { + attributePoints = Math.max(0, value); + } + + public void setAttributeReallocationPoints(int value) { + attributeReallocationPoints = Math.max(0, value); + } + + public void setSkillPoints(int value) { + skillPoints = Math.max(0, value); + } + + public void setClassPoints(int value) { + classPoints = Math.max(0, value); + } + + public boolean hasSavedClass(PlayerClass profess) { + return classSlots.containsKey(profess.getId()); + } + + public Set getSavedClasses() { + return classSlots.keySet(); + } + + public SavedClassInformation getClassInfo(PlayerClass profess) { + return getClassInfo(profess.getId()); + } + + public SavedClassInformation getClassInfo(String profess) { + return classSlots.get(profess); + } + + public void applyClassInfo(PlayerClass profess, SavedClassInformation info) { + classSlots.put(profess.getId(), info); + } + + public void unloadClassInfo(PlayerClass profess) { + classSlots.remove(profess.getId()); + } + + public Set getWaypoints() { + return waypoints; + } + + public boolean hasWaypoint(Waypoint waypoint) { + return waypoint.isDefault() || waypoints.contains(waypoint.getId()); + } + + public void unlockWaypoint(Waypoint waypoint) { + waypoints.add(waypoint.getId()); + } + + public long getWaypointCooldown() { + return Math.max(0, lastWaypoint + 5000 - System.currentTimeMillis()); + } + + /** + * Handles the per-player loot chest cooldown system. That is to + * reduce the rik of spawning multiple loot chests around the same + * player in a row, which could be game breaking. + * + * @return If a random chest can spawn around that player + */ + public boolean canSpawnLootChest() { + return lastLootChest + MMOCore.plugin.configManager.lootChestPlayerCooldown < System.currentTimeMillis(); + } + + public void applyLootChestCooldown() { + lastLootChest = System.currentTimeMillis(); + } + + public void heal(double heal) { + if (!isOnline()) + return; + double newest = Math.max(0, Math.min(getPlayer().getHealth() + heal, getPlayer().getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue())); + if (getPlayer().getHealth() == newest) + return; + + PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.HEALTH, heal); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) + return; + + getPlayer().setHealth(newest); + } + + public void addFriend(UUID uuid) { + friends.add(uuid); + } + + @Override + public void removeFriend(UUID uuid) { + friends.remove(uuid); + } + + @Override + public boolean hasFriend(UUID uuid) { + return friends.contains(uuid); + } + + public void setParty(Party party) { + this.party = party; + } + + public void setGuild(Guild guild) { + this.guild = guild; + } + + public void log(Level level, String message) { + MMOCore.plugin.getLogger().log(level, "[Userdata:" + (isOnline() ? getPlayer().getName() : "Offline Player") + "] " + message); + } + + public void setLastFriendRequest(long ms) { + lastFriendRequest = Math.max(0, ms); + } + + public void sendFriendRequest(PlayerData target) { + if (!isOnline() || !target.isOnline()) + return; + setLastFriendRequest(System.currentTimeMillis()); + + FriendRequest request = new FriendRequest(this, target); + new ConfigMessage("friend-request").addPlaceholders("player", getPlayer().getName(), "uuid", request.getUniqueId().toString()) + .sendAsJSon(target.getPlayer()); + MMOCore.plugin.requestManager.registerRequest(request); + } + + public void warp(Waypoint waypoint) { + if (!isOnline()) + return; + + /* + * This cooldown is only used internally to make sure the player is not + * spamming waypoints. There is no need to reset it when resetting the + * player waypoints data + */ + lastWaypoint = System.currentTimeMillis(); + + giveStellium(-waypoint.getStelliumCost()); + + new BukkitRunnable() { + final int x = getPlayer().getLocation().getBlockX(); + final int y = getPlayer().getLocation().getBlockY(); + final int z = getPlayer().getLocation().getBlockZ(); + int t; + + public void run() { + if (!isOnline()) + return; + if (getPlayer().getLocation().getBlockX() != x || getPlayer().getLocation().getBlockY() != y + || getPlayer().getLocation().getBlockZ() != z) { + MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_CANCELLED); + MMOCore.plugin.configManager.getSimpleMessage("warping-canceled").send(getPlayer()); + giveStellium(waypoint.getStelliumCost()); + cancel(); + return; + } + + MMOCore.plugin.configManager.getSimpleMessage("warping-comencing", "left", "" + ((120 - t) / 20)).send(getPlayer()); + if (t++ >= 100) { + getPlayer().teleport(waypoint.getLocation()); + getPlayer().addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 20, 1, false, false)); + MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_TELEPORT); + cancel(); + return; + } + + MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.WARP_CHARGE, 1, (float) (t / Math.PI * .015 + .5)); + double r = Math.sin((double) t / 100 * Math.PI); + for (double j = 0; j < Math.PI * 2; j += Math.PI / 4) + getPlayer().getLocation().getWorld().spawnParticle(Particle.REDSTONE, + getPlayer().getLocation().add(Math.cos((double) t / 20 + j) * r, (double) t / 50, Math.sin((double) t / 20 + j) * r), 1, + new Particle.DustOptions(Color.PURPLE, 1.25f)); + } + }.runTaskTimer(MMOCore.plugin, 0, 1); + } + + public boolean hasReachedMaxLevel() { + return getProfess().getMaxLevel() > 0 && getLevel() >= getProfess().getMaxLevel(); + } + + /** + * Gives experience without displaying an EXP hologram around the player + * + * @param value Experience to give the player + * @param source How the player earned experience + */ + public void giveExperience(int value, EXPSource source) { + giveExperience(value, source, null); + } + + /** + * Called when giving experience to a player + * + * @param value Experience to give the player + * @param source How the player earned experience + * @param hologramLocation Location used to display the hologram. If it's null, no + * hologram will be displayed + */ + public void giveExperience(int value, EXPSource source, @Nullable Location hologramLocation) { + if (hasReachedMaxLevel()) { + setExperience(0); + return; + } + + // Experience hologram + if (hologramLocation != null && isOnline() && MMOCore.plugin.hasHolograms()) + MMOCore.plugin.hologramSupport.displayIndicator(hologramLocation.add(.5, 1.5, .5), + MMOCore.plugin.configManager.getSimpleMessage("exp-hologram", "exp", "" + value).message(), getPlayer()); + + value = MMOCore.plugin.boosterManager.calculateExp(null, value); + value *= 1 + getStats().getStat(StatType.ADDITIONAL_EXPERIENCE) / 100; + + PlayerExperienceGainEvent event = new PlayerExperienceGainEvent(this, value, source); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) + return; + + experience += event.getExperience(); + + // Calculate the player's next level + int oldLevel = level, needed; + while (experience >= (needed = getLevelUpExperience())) { + + if (hasReachedMaxLevel()) { + experience = 0; + break; + } + + experience -= needed; + level = getLevel() + 1; + } + + if (level > oldLevel) { + Bukkit.getPluginManager().callEvent(new PlayerLevelUpEvent(this, null, oldLevel, level)); + if (isOnline()) { + new ConfigMessage("level-up").addPlaceholders("level", "" + level).send(getPlayer()); + MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.LEVEL_UP); + new SmallParticleEffect(getPlayer(), Particle.SPELL_INSTANT); + } + getStats().updateStats(); + } + + refreshVanillaExp(); + } + + public int getExperience() { + return experience; + } + + @Override + public PlayerClass getProfess() { + return profess == null ? MMOCore.plugin.classManager.getDefaultClass() : profess; + } + + public void giveMana(double amount) { + double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_MANA), mana + amount)); + if (mana == newest) + return; + + PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.MANA, amount); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) + return; + + mana = newest; + } + + public void giveStamina(double amount) { + double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_STAMINA), stamina + amount)); + if (stamina == newest) + return; + + PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.STAMINA, amount); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) + return; + + stamina = newest; + + } + + public void giveStellium(double amount) { + double newest = Math.max(0, Math.min(getStats().getStat(StatType.MAX_STELLIUM), stellium + amount)); + if (stellium == newest) + return; + + PlayerRegenResourceEvent event = new PlayerRegenResourceEvent(this, PlayerResource.STELLIUM, amount); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) + return; + + stellium = newest; + } + + public double getMana() { + return mana; + } + + public double getStamina() { + return stamina; + } + + public double getStellium() { + return stellium; + } + + public PlayerStats getStats() { + return playerStats; + } + + public PlayerAttributes getAttributes() { + return attributes; + } + + public void setMana(double amount) { + mana = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_MANA))); + } + + public void setStamina(double amount) { + stamina = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_STAMINA))); + } + + public void setStellium(double amount) { + stellium = Math.max(0, Math.min(amount, getStats().getStat(StatType.MAX_STELLIUM))); + } + + public boolean isFullyLoaded() { + return fullyLoaded; + } + + public void setFullyLoaded() { + this.fullyLoaded = true; + } + + public boolean isCasting() { + return skillCasting != null; + } + + /** + * @return If the action bar is not being used to display anything else + * i.e if the "general info" action bar can be displayed + */ + public boolean canSeeActionBar() { + return actionBarTimeOut < System.currentTimeMillis(); + } + + /** + * @param timeOut Delay during which the general info action bar + * will not be displayed to the player + */ + public void setActionBarTimeOut(long timeOut) { + actionBarTimeOut = System.currentTimeMillis() + (timeOut * 50); + } + + public void displayActionBar(String message) { + if (!isOnline()) + return; + + setActionBarTimeOut(MMOCore.plugin.actionBarManager.getTimeOut()); + getPlayer().spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); + } + + @Deprecated + public void setAttribute(PlayerAttribute attribute, int value) { + setAttribute(attribute.getId(), value); + } + + @Deprecated + public void setAttribute(String id, int value) { + attributes.setBaseAttribute(id, value); + } + + @Deprecated + public Map mapAttributePoints() { + return getAttributes().mapPoints(); + } + + public int getSkillLevel(Skill skill) { + return skills.getOrDefault(skill.getId(), 1); + } + + public void setSkillLevel(Skill skill, int level) { + setSkillLevel(skill.getId(), level); + } + + public void setSkillLevel(String skill, int level) { + skills.put(skill, level); + } + + public void resetSkillLevel(String skill) { + skills.remove(skill); + } + + /* + * better use getProfess().findSkill(skill).isPresent() + */ + @Deprecated + public boolean hasSkillUnlocked(Skill skill) { + return getProfess().hasSkill(skill.getId()) && hasSkillUnlocked(getProfess().getSkill(skill.getId())); + } + + /** + * Checks for the player's level and compares it to the skill unlock level. + *

+ * Any skill, when the player has the right level is instantly + * unlocked, therefore one must NOT check if the player has unlocked the + * skill by checking if the skills map contains the skill id as key. This + * only checks if the player has spent any skill point. + * + * @return If the player unlocked the skill + */ + public boolean hasSkillUnlocked(SkillInfo info) { + return getLevel() >= info.getUnlockLevel(); + } + + public Map mapSkillLevels() { + return new HashMap<>(skills); + } + + public void giveClassPoints(int value) { + setClassPoints(classPoints + value); + } + + public void giveSkillPoints(int value) { + setSkillPoints(skillPoints + value); + } + + public void giveAttributePoints(int value) { + setAttributePoints(attributePoints + value); + } + + // public void giveSkillReallocationPoints(int value) { + // setSkillReallocationPoints(skillReallocationPoints + value); + // } + + public void giveAttributeReallocationPoints(int value) { + setAttributeReallocationPoints(attributeReallocationPoints + value); + } + + public PlayerSkillData getSkillData() { + return skillData; + } + + public void setClass(PlayerClass profess) { + this.profess = profess; + + // for (Iterator iterator = boundSkills.iterator(); + // iterator.hasNext();) + // if (!getProfess().hasSkill(iterator.next().getSkill())) + // iterator.remove(); + + getStats().updateStats(); + } + + public boolean hasSkillBound(int slot) { + return slot < boundSkills.size(); + } + + public SkillInfo getBoundSkill(int slot) { + return slot >= boundSkills.size() ? null : boundSkills.get(slot); + } + + public void setBoundSkill(int slot, SkillInfo skill) { + if (boundSkills.size() < 6) + boundSkills.add(skill); + else + boundSkills.set(slot, skill); + } + + public void unbindSkill(int slot) { + boundSkills.remove(slot); + } + + public List getBoundSkills() { + return boundSkills; + } + + public boolean isInCombat() { + return combat != null; + } + + /** + * Loops through all the subclasses available to the player and + * checks if they could potentially upgrade to one of these + * + * @return If the player can change its current class to + * a subclass + */ + public boolean canChooseSubclass() { + for (Subclass subclass : getProfess().getSubclasses()) + if (getLevel() >= subclass.getLevel()) + return true; + return false; + } + + /** + * Everytime a player does a combat action, like taking + * or dealing damage to an entity, this method is called. + */ + public void updateCombat() { + if (isInCombat()) + combat.update(); + else + combat = new CombatRunnable(this); + } + + public SkillResult cast(Skill skill) { + return cast(getProfess().getSkill(skill)); + } + + public SkillResult cast(SkillInfo skill) { + + PlayerPreCastSkillEvent preEvent = new PlayerPreCastSkillEvent(this, skill); + Bukkit.getPluginManager().callEvent(preEvent); + if (preEvent.isCancelled()) + return new SkillResult(this, skill, CancelReason.OTHER); + + // Check for mana/stamina/cooldown and cast skill + SkillResult cast = skill.getSkill().whenCast(this, skill); + + // Send failure messages + if (!cast.isSuccessful()) { + if (!skill.getSkill().isPassive() && isOnline()) { + if (cast.getCancelReason() == CancelReason.LOCKED) + MMOCore.plugin.configManager.getSimpleMessage("not-unlocked-skill").send(getPlayer()); + + if (cast.getCancelReason() == CancelReason.MANA) + MMOCore.plugin.configManager.getSimpleMessage("casting.no-mana").send(getPlayer()); + + if (cast.getCancelReason() == CancelReason.STAMINA) + MMOCore.plugin.configManager.getSimpleMessage("casting.no-stamina").send(getPlayer()); + + if (cast.getCancelReason() == CancelReason.COOLDOWN) + MMOCore.plugin.configManager.getSimpleMessage("casting.on-cooldown").send(getPlayer()); + } + + PlayerPostCastSkillEvent postEvent = new PlayerPostCastSkillEvent(this, skill, cast); + Bukkit.getPluginManager().callEvent(postEvent); + return cast; + } + + // Apply cooldown, mana and stamina costs + if (!nocd) { + double flatCooldownReduction = Math.max(0, Math.min(1, getStats().getStat(StatType.COOLDOWN_REDUCTION) / 100)); + flatCooldownReduction *= flatCooldownReduction > 0 ? skill.getModifier("cooldown", getSkillLevel(skill.getSkill())) * 1000 : 0; + + skillData.setLastCast(cast.getSkill(), System.currentTimeMillis() - (long) flatCooldownReduction); + giveMana(-cast.getManaCost()); + giveStamina(-cast.getStaminaCost()); + } + + PlayerPostCastSkillEvent postEvent = new PlayerPostCastSkillEvent(this, skill, cast); + Bukkit.getPluginManager().callEvent(postEvent); + return cast; + } + + @Override + public int hashCode() { + return mmoData.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof PlayerData && ((PlayerData) obj).getUniqueId().equals(getUniqueId()); + } + + public static PlayerData get(OfflinePlayer player) { + return get(player.getUniqueId()); + } + + public static PlayerData get(UUID uuid) { + return MMOCore.plugin.dataProvider.getDataManager().get(uuid); + } + + public static Collection getAll() { + return MMOCore.plugin.dataProvider.getDataManager().getLoaded(); + } } diff --git a/src/main/java/net/Indyuce/mmocore/gui/AttributeView.java b/src/main/java/net/Indyuce/mmocore/gui/AttributeView.java index 1943a3bb..a4862325 100644 --- a/src/main/java/net/Indyuce/mmocore/gui/AttributeView.java +++ b/src/main/java/net/Indyuce/mmocore/gui/AttributeView.java @@ -42,7 +42,7 @@ public class AttributeView extends EditableInventory { } public GeneratedInventory newInventory(PlayerData data) { - return new SkillViewerInventory(data, this); + return new AttributeViewerInventory(data, this); } public static class AttributeItem extends InventoryPlaceholderItem { @@ -74,8 +74,8 @@ public class AttributeView extends EditableInventory { } } - public class SkillViewerInventory extends GeneratedInventory { - public SkillViewerInventory(PlayerData playerData, EditableInventory editable) { + public class AttributeViewerInventory extends GeneratedInventory { + public AttributeViewerInventory(PlayerData playerData, EditableInventory editable) { super(playerData, editable); } @@ -130,7 +130,7 @@ public class AttributeView extends EditableInventory { MMOCore.plugin.configManager.getSimpleMessage("attribute-level-up", "attribute", attribute.getName(), "level", "" + ins.getBase()).send(player); MMOCore.plugin.soundManager.play(getPlayer(), SoundManager.SoundEvent.LEVEL_ATTRIBUTE); - PlayerAttributeUseEvent playerAttributeUseEvent = new PlayerAttributeUseEvent(playerData); + PlayerAttributeUseEvent playerAttributeUseEvent = new PlayerAttributeUseEvent(playerData, attribute); Bukkit.getServer().getPluginManager().callEvent(playerAttributeUseEvent); open(); diff --git a/src/main/java/net/Indyuce/mmocore/listener/PlayerListener.java b/src/main/java/net/Indyuce/mmocore/listener/PlayerListener.java index b76518fb..73c5eafc 100644 --- a/src/main/java/net/Indyuce/mmocore/listener/PlayerListener.java +++ b/src/main/java/net/Indyuce/mmocore/listener/PlayerListener.java @@ -1,6 +1,11 @@ package net.Indyuce.mmocore.listener; +import net.Indyuce.mmocore.MMOCore; +import net.Indyuce.mmocore.api.event.PlayerRegenResourceEvent; +import net.Indyuce.mmocore.api.player.PlayerData; +import net.Indyuce.mmocore.api.player.profess.resource.PlayerResource; +import net.Indyuce.mmocore.gui.api.PluginInventory; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.entity.Projectile; @@ -12,97 +17,71 @@ import org.bukkit.event.entity.EntityRegainHealthEvent; import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; - -import net.Indyuce.mmocore.MMOCore; -import net.Indyuce.mmocore.api.event.PlayerRegenResourceEvent; -import net.Indyuce.mmocore.api.player.PlayerData; -import net.Indyuce.mmocore.api.player.profess.resource.PlayerResource; -import net.Indyuce.mmocore.gui.api.PluginInventory; import org.bukkit.scheduler.BukkitRunnable; public class PlayerListener implements Listener { - /* - We load our player data. + // Player data loading + @EventHandler(priority = EventPriority.NORMAL) + public void playerLoadingEvent(PlayerJoinEvent event) { + MMOCore.plugin.dataProvider.getDataManager().setup(event.getPlayer().getUniqueId()); + } + + // Register custom inventory clicks + @EventHandler + public void b(InventoryClickEvent event) { + if (event.getInventory().getHolder() instanceof PluginInventory) + ((PluginInventory) event.getInventory().getHolder()).whenClicked(event); + } + + // Register custom inventory close effect + @EventHandler + public void c(InventoryCloseEvent event) { + if (event.getInventory().getHolder() instanceof PluginInventory) + ((PluginInventory) event.getInventory().getHolder()).whenClosed(event); + } + + /** + * Updates the player's combat log data every time he hits an entity, or + * gets hit by an entity or a projectile sent by another entity */ - @EventHandler(priority = EventPriority.NORMAL) - public void playerLoadingEvent(PlayerJoinEvent e) { - new BukkitRunnable() { - @Override - public void run() { - MMOCore.plugin.dataProvider.getDataManager().setup(e.getPlayer().getUniqueId()); - } - }.runTaskAsynchronously(MMOCore.plugin); - } + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void d(EntityDamageByEntityEvent event) { + if (event.getEntity() instanceof Player && !event.getEntity().hasMetadata("NPC")) + PlayerData.get((Player) event.getEntity()).updateCombat(); - /* - * custom inventories register - */ - @EventHandler - public void b(InventoryClickEvent event) { - if (event.getInventory().getHolder() instanceof PluginInventory) - ((PluginInventory) event.getInventory().getHolder()).whenClicked(event); - } + if (event.getDamager() instanceof Player && !event.getDamager().hasMetadata("NPC")) + PlayerData.get((Player) event.getDamager()).updateCombat(); - @EventHandler - public void c(InventoryCloseEvent event) { - if (event.getInventory().getHolder() instanceof PluginInventory) - ((PluginInventory) event.getInventory().getHolder()).whenClosed(event); - } + if (event.getDamager() instanceof Projectile && ((Projectile) event.getDamager()).getShooter() instanceof Player) + if (!((Player) ((Projectile) event.getDamager()).getShooter()).hasMetadata("NPC")) + PlayerData.get((Player) ((Projectile) event.getDamager()).getShooter()).updateCombat(); + } - /* - * updates the player's combat log data every time he hits an entity, or - * gets hit by an entity or a projectile sent by another entity. updates - * this stuff on LOW level so other plugins can check if the player just - * entered combat - */ - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void d(EntityDamageByEntityEvent event) { - if (event.getEntity() instanceof Player && !event.getEntity().hasMetadata("NPC")) - PlayerData.get((Player) event.getEntity()).updateCombat(); + @EventHandler + public void e(PlayerQuitEvent event) { + PlayerData playerData = PlayerData.get(event.getPlayer()); + if (playerData.hasParty()) + playerData.getParty().removeMember(playerData); - if (event.getDamager() instanceof Player && !event.getDamager().hasMetadata("NPC")) - PlayerData.get((Player) event.getDamager()).updateCombat(); + MMOCore.plugin.dataProvider.getDataManager().remove(playerData); + } - if (event.getDamager() instanceof Projectile && ((Projectile) event.getDamager()).getShooter() instanceof Player) - if (!((Player) ((Projectile) event.getDamager()).getShooter()).hasMetadata("NPC")) - PlayerData.get((Player) ((Projectile) event.getDamager()).getShooter()).updateCombat(); - } - - @EventHandler - public void e(PlayerQuitEvent event) { - PlayerData playerData = PlayerData.get(event.getPlayer()); - if (playerData.hasParty()) - playerData.getParty().removeMember(playerData); - - MMOCore.plugin.dataProvider.getDataManager().remove(playerData); - } - - /* - * reset skill data when leaving combat - */ - // @EventHandler - // public void f(PlayerCombatEvent event) { - // if (!event.entersCombat()) - // event.getData().getSkillData().resetData(); - // } - - /* - * Warning: this really is not the best way to interface with MMOCore - * generation. Use instead PlayerRegenResourceEvent to be able to access - * directly the PlayerData without an extra map lookup. - */ - @Deprecated - @EventHandler(priority = EventPriority.HIGH) - public void g(PlayerRegenResourceEvent event) { - if (event.getResource() == PlayerResource.HEALTH) { - EntityRegainHealthEvent bukkitEvent = new EntityRegainHealthEvent(event.getPlayer(), event.getAmount(), RegainReason.CUSTOM); - Bukkit.getPluginManager().callEvent(bukkitEvent); - event.setCancelled(bukkitEvent.isCancelled()); - event.setAmount(bukkitEvent.getAmount()); - } - } + /** + * Warning: this really is not the best way to interface with MMOCore + * generation. Use instead PlayerRegenResourceEvent to be able to access + * directly the PlayerData without an extra map lookup. + */ + @Deprecated + @EventHandler(priority = EventPriority.HIGH) + public void g(PlayerRegenResourceEvent event) { + if (event.getResource() == PlayerResource.HEALTH) { + EntityRegainHealthEvent bukkitEvent = new EntityRegainHealthEvent(event.getPlayer(), event.getAmount(), RegainReason.CUSTOM); + Bukkit.getPluginManager().callEvent(bukkitEvent); + event.setCancelled(bukkitEvent.isCancelled()); + event.setAmount(bukkitEvent.getAmount()); + } + } } diff --git a/src/main/java/net/Indyuce/mmocore/manager/data/PlayerDataManager.java b/src/main/java/net/Indyuce/mmocore/manager/data/PlayerDataManager.java index 656b0448..607841d0 100644 --- a/src/main/java/net/Indyuce/mmocore/manager/data/PlayerDataManager.java +++ b/src/main/java/net/Indyuce/mmocore/manager/data/PlayerDataManager.java @@ -1,127 +1,168 @@ package net.Indyuce.mmocore.manager.data; -import java.util.*; - -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.ConfigurationSection; - +import io.lumine.mythic.lib.api.player.MMOPlayerData; import net.Indyuce.mmocore.MMOCore; +import net.Indyuce.mmocore.api.event.AsyncPlayerDataLoadEvent; import net.Indyuce.mmocore.api.event.PlayerDataLoadEvent; import net.Indyuce.mmocore.api.player.OfflinePlayerData; import net.Indyuce.mmocore.api.player.PlayerData; -import io.lumine.mythic.lib.api.player.MMOPlayerData; -import org.bukkit.scheduler.BukkitRunnable; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.*; public abstract class PlayerDataManager { - private final static Map data = Collections.synchronizedMap(new HashMap<>()); + private final static Map data = Collections.synchronizedMap(new HashMap<>()); - private DefaultPlayerData defaultData = new DefaultPlayerData(1, 0, 0, 0, 0); + private DefaultPlayerData defaultData = new DefaultPlayerData(1, 0, 0, 0, 0); - public PlayerData get(OfflinePlayer player) { - return get(player.getUniqueId()); - } + public PlayerData get(OfflinePlayer player) { + return get(player.getUniqueId()); + } - public PlayerData get(UUID uuid) { - return data.getOrDefault(uuid, setup(uuid)); - } + /** + * Gets the player data, or throws an exception if not found. + * The player data should be loaded when the player logs in + * so it's really bad practice to setup the player data if it's not loaded. + * + * @param uuid Player UUID + * @return Player data, if it's loaded + */ + public PlayerData get(UUID uuid) { + Validate.isTrue(data.containsKey(uuid), "Player data is not loaded"); + return data.get(uuid); + } - public void remove(UUID uuid) { - data.remove(uuid); - } + public void remove(UUID uuid) { + data.remove(uuid); + } - public abstract OfflinePlayerData getOffline(UUID uuid); + /** + * Offline player data is used to handle processes like friend removal + * which can still occur if one of the two players is offline. + *

+ * Unlike {@link #get(UUID)} this method never returns a null instance + * + * @param uuid Player unique id + * @return Offline player data + */ + @NotNull + public abstract OfflinePlayerData getOffline(UUID uuid); - public PlayerData setup(UUID uniqueId) { - return data.compute(uniqueId, (uuid, searchData) -> { - if (searchData == null) { - PlayerData playerData = new PlayerData(MMOPlayerData.get(uniqueId)); + /** + * Called when a player logs in, loading the player data inside the map. + *

+ * For YAML configs or SQL databases, data is loaded as not to overload + * the main thread with SQL requests. Therefore, the player data returned + * by that method, when the player joined for the first time, is not + * fully loaded YET. + * + * @param uniqueId Player UUID + * @return The loaded player data. + */ + public PlayerData setup(UUID uniqueId) { - loadData(playerData); - playerData.getStats().updateStats(); + // Load player data if it does not exist + if (!data.containsKey(uniqueId)) { + PlayerData newData = new PlayerData(MMOPlayerData.get(uniqueId)); - // We call the player data load event. TODO: Convert this event to async. - new BukkitRunnable() { - @Override - public void run() { - Bukkit.getPluginManager().callEvent(new PlayerDataLoadEvent(playerData)); - } - }.runTask(MMOCore.plugin); + // Schedule async data loading + Bukkit.getScheduler().runTaskAsynchronously(MMOCore.plugin, () -> { + loadData(newData); + newData.getStats().updateStats(); + Bukkit.getPluginManager().callEvent(new AsyncPlayerDataLoadEvent(newData)); + Bukkit.getScheduler().runTask(MMOCore.plugin, () -> Bukkit.getPluginManager().callEvent(new PlayerDataLoadEvent(newData))); + }); - return playerData; - } else return searchData; + // Update data map + data.put(uniqueId, newData); - }); - } + return newData; + } + return data.get(uniqueId); + } - public DefaultPlayerData getDefaultData() { - return defaultData; - } + public DefaultPlayerData getDefaultData() { + return defaultData; + } - public void loadDefaultData(ConfigurationSection config) { - defaultData = new DefaultPlayerData(config); - } + public void loadDefaultData(ConfigurationSection config) { + defaultData = new DefaultPlayerData(config); + } - public boolean isLoaded(UUID uuid) { - return data.containsKey(uuid); - } + public boolean isLoaded(UUID uuid) { + return data.containsKey(uuid); + } - public Collection getLoaded() { - return data.values(); - } + public Collection getLoaded() { + return data.values(); + } - public abstract void loadData(PlayerData data); + /** + * Called when player data must be loaded from database or config. + * + * @param data Player data to load + */ + public abstract void loadData(PlayerData data); - public abstract void saveData(PlayerData data); + /** + * Called when player data must be saved in configs or database. + * + * @param data Player data to save + */ + public abstract void saveData(PlayerData data); - public abstract void remove(PlayerData data); + public abstract void remove(PlayerData data); - public static class DefaultPlayerData { - private final int level, classPoints, skillPoints, attributePoints, attrReallocPoints; + public static class DefaultPlayerData { + private final int level, classPoints, skillPoints, attributePoints, attrReallocPoints; - public DefaultPlayerData(ConfigurationSection config) { - level = config.getInt("level", 1); - classPoints = config.getInt("class-points"); - skillPoints = config.getInt("skill-points"); - attributePoints = config.getInt("attribute-points"); - attrReallocPoints = config.getInt("attribute-realloc-points"); - } + public DefaultPlayerData(ConfigurationSection config) { + level = config.getInt("level", 1); + classPoints = config.getInt("class-points"); + skillPoints = config.getInt("skill-points"); + attributePoints = config.getInt("attribute-points"); + attrReallocPoints = config.getInt("attribute-realloc-points"); + } - public DefaultPlayerData(int level, int classPoints, int skillPoints, int attributePoints, int attrReallocPoints) { - this.level = level; - this.classPoints = classPoints; - this.skillPoints = skillPoints; - this.attributePoints = attributePoints; - this.attrReallocPoints = attrReallocPoints; - } + public DefaultPlayerData(int level, int classPoints, int skillPoints, int attributePoints, int attrReallocPoints) { + this.level = level; + this.classPoints = classPoints; + this.skillPoints = skillPoints; + this.attributePoints = attributePoints; + this.attrReallocPoints = attrReallocPoints; + } - public int getLevel() { - return level; - } + public int getLevel() { + return level; + } - public int getSkillPoints() { - return skillPoints; - } + public int getSkillPoints() { + return skillPoints; + } - public int getClassPoints() { - return classPoints; - } + public int getClassPoints() { + return classPoints; + } - public int getAttrReallocPoints() { - return attrReallocPoints; - } + public int getAttrReallocPoints() { + return attrReallocPoints; + } - public int getAttributePoints() { - return attributePoints; - } + public int getAttributePoints() { + return attributePoints; + } - public void apply(PlayerData player) { - player.setLevel(level); - player.setClassPoints(classPoints); - player.setSkillPoints(skillPoints); - player.setAttributePoints(attributePoints); - player.setAttributeReallocationPoints(attrReallocPoints); - } - } + public void apply(PlayerData player) { + player.setLevel(level); + player.setClassPoints(classPoints); + player.setSkillPoints(skillPoints); + player.setAttributePoints(attributePoints); + player.setAttributeReallocationPoints(attrReallocPoints); + } + } }