Browse Source

Merge branch 'master' of github.com:mcMMO-Dev/mcMMO into tridentsxbows

nossr50 4 years ago
parent
commit
31904ef181
28 changed files with 1771 additions and 2985 deletions
  1. 52 1
      Changelog.txt
  2. 15 5
      pom.xml
  3. 1 1
      src/main/java/com/gmail/nossr50/api/DatabaseAPI.java
  4. 1225 1220
      src/main/java/com/gmail/nossr50/api/ExperienceAPI.java
  5. 1 1
      src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java
  6. 1 1
      src/main/java/com/gmail/nossr50/commands/database/DatabaseRemovePlayerCommand.java
  7. 43 39
      src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java
  8. 39 32
      src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java
  9. 30 27
      src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java
  10. 29 47
      src/main/java/com/gmail/nossr50/database/DatabaseManager.java
  11. 0 1524
      src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java
  12. 139 21
      src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java
  13. 2 1
      src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java
  14. 41 0
      src/main/java/com/gmail/nossr50/events/McMMOReplaceVanillaTreasureEvent.java
  15. 1 0
      src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  16. 14 3
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  17. 5 1
      src/main/java/com/gmail/nossr50/mcMMO.java
  18. 1 1
      src/main/java/com/gmail/nossr50/runnables/database/FormulaConversionTask.java
  19. 1 1
      src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java
  20. 34 26
      src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoadingTask.java
  21. 1 1
      src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java
  22. 16 0
      src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardManager.java
  23. 12 0
      src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java
  24. 4 1
      src/main/java/com/gmail/nossr50/util/text/TextUtils.java
  25. 1 1
      src/main/java/com/gmail/nossr50/util/upgrade/UpgradeManager.java
  26. 30 30
      src/main/resources/locale/locale_de.properties
  27. 1 0
      src/main/resources/upgrades_overhaul.yml
  28. 32 0
      src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java

+ 52 - 1
Changelog.txt

@@ -103,8 +103,59 @@ Version 2.2.000
     Parties got unnecessarily complex in my absence, I have removed many party features in order to simplify parties and bring them closer to my vision. I have also added new features which should improve parties where it matters.
     About the removed party features, all the features I removed I consider poor quality features and I don't think they belong in mcMMO. Feel free to yell at me in discord if you disagree.
     I don't know what genius decided to make parties public by default, when I found out that parties had been changed to such a system I could barely contain my disgust. Parties are back to being private, you get invited by a party leader or party officer. That is the only way to join a party.
+Version 2.1.182
+    Fixed several errors in de locale (Thanks TheBusyBiscuit & w1tcherrr)
+    Fixed a bug where double smelt never succeeded if the furnace was empty
+    Added some safety so that mcMMO automatic save interval is never more frequent than 1 minute
+
+Version 2.1.181
+    mcMMO no longer pointlessly tries to check for missing UUIDs for FlatFile database
+    Removed the "name change detected" message as some plugins (such as Plan) invoke API calls which spams the console with this message
+    Refactored code related to loading player data from the database
+    (API) Added DatabaseManager::loadPlayerProfile(String)
+    (API) Removed DatabaseManager::loadPlayerProfile(String, UUID, boolean)
+    (API) Removed DatabaseManager::loadPlayerProfile(String, boolean)
+
+Version 2.1.180
+    mcMMO will now automatically remove corrupted data from mcmmo.users instead of catastrophic failure
+    When using FlatFile database (the default) mcMMO will try its best to inform you which players had corrupted data when it does repairs
+    Various minor optimizations and tweaks to the FlatFile database
+    mcMMO is now much more verbose when things go wrong with the FlatFile database (removed some silent errors, added more error messages/warnings)
+    mcMMO now uses UTF-8 compliant encoding for SQL databases (utf8mb4)
+    Fixed a bug where mcMMO could in some circumstances fail to update SQL schema and mark it as successful
+    Renamed updates.yml to updates_overhaul.yml to avoid some potential issues when upgrading from classic
+
+    NOTES:
+    This update was tested pretty thoroughly so it should be pretty safe, let me know if you have issues in the mcMMO discord or GitHub issues page for mcMMO!
+
+Version 2.1.179
+    Fixed a bug for FlatFile databases where some players with changed nicknames would have their levels not loaded upon login (possibly wiping their data)
+
+    NOTES:
+    Players affected by this bug (introduced in 2.1.177) may have their data lost, but this patch reverts the change which caused this bug.
+    I suspect their data isn't lost and may be restored after this patch is loaded up, however if it is lost mcMMO makes regular backups so you can load one of those (check <Server Directory>/plugins/mcMMO/) or manually edit their levels via MMOEDIT as a solution of sorts.
+
+Version 2.1.178
+    Item replacement in vanilla fishing override back to SALMON from AIR (see notes)
+
+    NOTES:
+    Apparently can't set items to AIR, my bad. I'll look into another solution for fishing plugin compatibility soon.
+
+Version 2.1.177
+    Environmentally aware will now protect Wolves from Magma blocks
+    Fixed a bug where mcMMO would fail to update a players name when it detected a name change
+    mcMMO will treat vanished players as if they are offline when using the inspect command on them now (see notes)
+    mcMMO now listens to PlayerFishEvent at HIGH event priority instead of HIGHEST
+    Changed how vanilla fishing treasures are overridden (AIR instead of SALMON)
+    (API) Added McMMOReplaceVanillaTreasureEvent -- see notes
+
+    NOTES:
+    A few changes were made to the inspect command, it used to reject you when used on vanished players, now it will be processed as if they are offline.
+    Additionally if you do inspect a vanished player, it will not use their display name (consistent with offline players) as that would give them away for being online
+    McMMOReplaceVanillaTreasureEvent is an event which is fired when mcMMO replaces a vanilla treasure with AIR if the server config file is set to override vanilla treasures, this causes some issues for other fishing plugins so this event helps those plugins be more compatible
+
 Version 2.1.176
-    Added another measure to prevent item stacks from reaching 65 from double smelt
+    Another fix for Double Smelt bringing item stack size to illegal values
 
 Version 2.1.175
     Fixed a bug where mcMMO would occasionally give a 65 item stack from a double smelt on a furnace

+ 15 - 5
pom.xml

@@ -16,6 +16,16 @@
         <system>GitHub</system>
     </issueManagement>
     <packaging>jar</packaging>
+    <distributionManagement>
+        <repository>
+            <id>neetgames</id>
+            <url>https://nexus.neetgames.com/repository/maven-releases/</url>
+        </repository>
+        <snapshotRepository>
+            <id>neetgames</id>
+            <url>https://nexus.neetgames.com/repository/maven-snapshots/</url>
+        </snapshotRepository>
+    </distributionManagement>
     <build>
         <finalName>${project.artifactId}</finalName>
         <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
@@ -236,27 +246,27 @@
         <dependency>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-text-serializer-gson</artifactId>
-            <version>4.5.1</version>
+            <version>4.7.0</version>
         </dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-api</artifactId>
-            <version>4.5.1</version>
+            <version>4.7.0</version>
         </dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-nbt</artifactId>
-            <version>4.5.1</version>
+            <version>4.7.0</version>
         </dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-key</artifactId>
-            <version>4.5.1</version>
+            <version>4.7.0</version>
         </dependency>
         <dependency>
             <groupId>net.kyori</groupId>
             <artifactId>adventure-text-serializer-gson-legacy-impl</artifactId>
-            <version>4.5.1</version>
+            <version>4.7.0</version>
         </dependency>
         <dependency>
             <groupId>net.kyori</groupId>

+ 1 - 1
src/main/java/com/gmail/nossr50/api/DatabaseAPI.java

@@ -22,7 +22,7 @@ public class DatabaseAPI {
      * @return true if the player exists in the DB, false if they do not
      */
     public boolean doesPlayerExistInDB(UUID uuid) {
-        PlayerProfile playerProfile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(uuid);
+        PlayerProfile playerProfile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(uuid, null);
 
         return playerProfile.isLoaded();
     }

+ 1225 - 1220
src/main/java/com/gmail/nossr50/api/ExperienceAPI.java

@@ -1,1220 +1,1225 @@
-//package com.gmail.nossr50.api;
-//
-//import com.gmail.nossr50.api.exceptions.*;
-//import com.gmail.nossr50.config.Config;
-//import com.gmail.nossr50.config.experience.ExperienceConfig;
-//import com.gmail.nossr50.datatypes.experience.FormulaType;
-//import com.neetgames.mcmmo.player.OnlineMMOPlayer;
-//import com.gmail.nossr50.datatypes.player.PlayerProfile;
-//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-//import com.gmail.nossr50.mcMMO;
-//import com.gmail.nossr50.skills.child.FamilyTree;
-//import com.gmail.nossr50.util.skills.CombatUtils;
-//import org.bukkit.block.BlockState;
-//import org.bukkit.entity.LivingEntity;
-//import org.bukkit.entity.Player;
-//
-//import java.util.ArrayList;
-//import java.util.Set;
-//import java.util.UUID;
-//
-//public final class ExperienceAPI {
-//    private ExperienceAPI() {}
-//
-//    /**
-//     * Returns whether given string is a valid type of skill suitable for the
-//     * other API calls in this class.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param skillType A string that may or may not be a skill
-//     * @return true if this is a valid mcMMO skill
-//     */
-//    public static boolean isValidSkillType(String skillType) {
-//        return mcMMO.p.getSkillRegister().getSkill(skillType) != null;
-//    }
-//
-//    /**
-//     * Start the task that gives combat XP.
-//     * Processes combat XP like mcMMO normally would, so mcMMO will check whether or not the entity should reward XP when giving out the XP
-//     *
-//     * @param mmoPlayer The attacking player
-//     * @param target The defending entity
-//     * @param primarySkillType The skill being used
-//     * @param multiplier final XP result will be multiplied by this
-//     * @deprecated Draft API
-//     */
-//    @Deprecated
-//    public static void addCombatXP(OnlineMMOPlayer mmoPlayer, LivingEntity target, PrimarySkillType primarySkillType, double multiplier) {
-//        CombatUtils.processCombatXP(mmoPlayer, target, primarySkillType, multiplier);
-//    }
-//
-//    /**
-//     * Start the task that gives combat XP.
-//     * Processes combat XP like mcMMO normally would, so mcMMO will check whether or not the entity should reward XP when giving out the XP
-//     *
-//     * @param mmoPlayer The attacking player
-//     * @param target The defending entity
-//     * @param primarySkillType The skill being used
-//     * @deprecated Draft API
-//     */
-//    @Deprecated
-//    public static void addCombatXP(OnlineMMOPlayer mmoPlayer, LivingEntity target, PrimarySkillType primarySkillType) {
-//        CombatUtils.processCombatXP(mmoPlayer, target, primarySkillType);
-//    }
-//
-//    /**
-//     * Returns whether the given skill type string is both valid and not a
-//     * child skill. (Child skills have no XP of their own, and their level is
-//     * derived from the parent(s).)
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param skillType the skill to check
-//     * @return true if this is a valid, non-child mcMMO skill
-//     */
-//    public static boolean isNonChildSkill(String skillType) {
-//        PrimarySkillType skill = mcMMO.p.getSkillRegister().getSkill(skillType);
-//
-//        return skill != null && !skill.isChildSkill();
-//    }
-//
-//    @Deprecated
-//    public static void addRawXP(Player player, String skillType, int XP) {
-//        addRawXP(player, skillType, (float) XP);
-//    }
-//
-//    /**
-//     * Adds raw XP to the player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    @Deprecated
-//    public static void addRawXP(Player player, String skillType, float XP) {
-//        addRawXP(player, skillType, XP, "UNKNOWN");
-//    }
-//
-//    /**
-//     * Adds raw XP to the player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addRawXP(Player player, String skillType, float XP, String xpGainReason) {
-//        addRawXP(player, skillType, XP, xpGainReason, false);
-//    }
-//
-//    /**
-//     * Adds raw XP to the player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     * @param isUnshared true if the XP cannot be shared with party members
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addRawXP(Player player, String skillType, float XP, String xpGainReason, boolean isUnshared) {
-//        if (isUnshared) {
-//            getPlayer(player).getExperienceHandler().beginUnsharedXpGain(player, getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//            return;
-//        }
-//
-//        getPlayer(player).getExperienceHandler().applyXpGain(player, getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//    }
-//
-//    /**
-//     * Adds raw XP to an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @deprecated We're using float for our XP values now
-//     * replaced by {@link #addRawXPOffline(String playerName, String skillType, float XP)}
-//     */
-//    @Deprecated
-//    public static void addRawXPOffline(String playerName, String skillType, int XP) {
-//        addRawXPOffline(playerName, skillType, (float) XP);
-//    }
-//
-//    /**
-//     * Adds raw XP to an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @deprecated We're using uuids to get an offline player
-//     * replaced by {@link #addRawXPOffline(UUID uuid, String skillType, float XP)}
-//     *
-//     * @param playerName The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static void addRawXPOffline(String playerName, String skillType, float XP) {
-//        addOfflineXP(playerName, getSkillType(skillType), (int) Math.floor(XP));
-//    }
-//
-//    /**
-//     * Adds raw XP to an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The UUID of player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    public static void addRawXPOffline(UUID uuid, String skillType, float XP) {
-//        addOfflineXP(uuid, getSkillType(skillType), (int) Math.floor(XP));
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate only.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    @Deprecated
-//    public static void addMultipliedXP(Player player, String skillType, int XP) {
-//        addMultipliedXP(player, skillType, XP, "UNKNOWN");
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate only.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addMultipliedXP(Player player, String skillType, int XP, String xpGainReason) {
-//        getPlayer(player).getExperienceHandler().applyXpGain(player, getSkillType(skillType), (int) (XP * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//    }
-//
-//    /**
-//     * Adds XP to an offline player, calculates for XP Rate only.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static void addMultipliedXPOffline(String playerName, String skillType, int XP) {
-//        addOfflineXP(playerName, getSkillType(skillType), (int) (XP * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate and skill modifier.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    @Deprecated
-//    public static void addModifiedXP(Player player, String skillType, int XP) {
-//        addModifiedXP(player, skillType, XP, "UNKNOWN");
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate and skill modifier.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addModifiedXP(Player player, String skillType, int XP, String xpGainReason) {
-//        addModifiedXP(player, skillType, XP, xpGainReason, false);
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate and skill modifier.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     * @param isUnshared true if the XP cannot be shared with party members
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addModifiedXP(Player player, String skillType, int XP, String xpGainReason, boolean isUnshared) {
-//        PrimarySkillType skill = getSkillType(skillType);
-//
-//        if (isUnshared) {
-//            getPlayer(player).getExperienceHandler().beginUnsharedXpGain(player, skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//            return;
-//        }
-//
-//        getPlayer(player).getExperienceHandler().applyXpGain(player, skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//    }
-//
-//    /**
-//     * Adds XP to an offline player, calculates for XP Rate and skill modifier.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static void addModifiedXPOffline(String playerName, String skillType, int XP) {
-//        PrimarySkillType skill = getSkillType(skillType);
-//
-//        addOfflineXP(playerName, skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
-//     * and party sharing.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    @Deprecated
-//    public static void addXP(Player player, String skillType, int XP) {
-//        addXP(player, skillType, XP, "UNKNOWN");
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
-//     * and party sharing.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addXP(Player player, String skillType, int XP, String xpGainReason) {
-//        addXP(player, skillType, XP, xpGainReason, false);
-//    }
-//
-//    /**
-//     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
-//     * and party sharing.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add XP to
-//     * @param skillType The skill to add XP to
-//     * @param XP The amount of XP to add
-//     * @param xpGainReason The reason to gain XP
-//     * @param isUnshared true if the XP cannot be shared with party members
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
-//     */
-//    public static void addXP(Player player, String skillType, int XP, String xpGainReason, boolean isUnshared) {
-//        if (isUnshared) {
-//            getPlayer(player).getExperienceHandler().beginUnsharedXpGain(player, getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//            return;
-//        }
-//
-//        getPlayer(player).getExperienceHandler().beginXpGain(player, getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
-//    }
-//
-//    /**
-//     * Get the amount of XP a player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static int getXP(Player player, String skillType) {
-//        return getPlayer(player).getExperienceHandler().getSkillXpValue(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the amount of XP an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static int getOfflineXP(String playerName, String skillType) {
-//        return getOfflineProfile(playerName).getExperienceHandler().getSkillXpValue(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the amount of XP an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static int getOfflineXP(UUID uuid, String skillType) {
-//        return getOfflineProfile(uuid).getExperienceHandler().getSkillXpValue(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the raw amount of XP a player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static float getXPRaw(Player player, String skillType) {
-//        return getPlayer(player).getExperienceHandler().getSkillXpLevelRaw(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the raw amount of XP an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static float getOfflineXPRaw(String playerName, String skillType) {
-//        return getOfflineProfile(playerName).getExperienceHandler().getSkillXpLevelRaw(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the raw amount of XP an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP in a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static float getOfflineXPRaw(UUID uuid, String skillType) {
-//        return getOfflineProfile(uuid).getExperienceHandler().getSkillXpLevelRaw(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the total amount of XP needed to reach the next level.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get the XP amount for
-//     * @param skillType The skill to get the XP amount for
-//     * @return the total amount of XP needed to reach the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static int getXPToNextLevel(Player player, String skillType) {
-//        return getPlayer(player).getExperienceHandler().getExperienceToNextLevel(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the total amount of XP an offline player needs to reach the next level.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the total amount of XP needed to reach the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static int getOfflineXPToNextLevel(String playerName, String skillType) {
-//        return getOfflineProfile(playerName).getExperienceHandler().getExperienceToNextLevel(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the total amount of XP an offline player needs to reach the next level.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the total amount of XP needed to reach the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static int getOfflineXPToNextLevel(UUID uuid, String skillType) {
-//        return getOfflineProfile(uuid).getExperienceHandler().getExperienceToNextLevel(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the amount of XP remaining until the next level.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get the XP amount for
-//     * @param skillType The skill to get the XP amount for
-//     * @return the amount of XP remaining until the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static int getXPRemaining(Player player, String skillType) {
-//        PrimarySkillType skill = getNonChildSkillType(skillType);
-//
-//        PlayerProfile profile = getPlayer(player);
-//
-//        return profile.getExperienceHandler().getExperienceToNextLevel(skill) - profile.getExperienceHandler().getSkillXpValue(skill);
-//    }
-//
-//    /**
-//     * Get the amount of XP an offline player has left before leveling up.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP needed to reach the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static int getOfflineXPRemaining(String playerName, String skillType) {
-//        PrimarySkillType skill = getNonChildSkillType(skillType);
-//        PlayerProfile profile = getOfflineProfile(playerName);
-//
-//        return profile.getExperienceHandler().getExperienceToNextLevel(skill) - profile.getExperienceHandler().getSkillXpValue(skill);
-//    }
-//
-//    /**
-//     * Get the amount of XP an offline player has left before leveling up.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get XP for
-//     * @param skillType The skill to get XP for
-//     * @return the amount of XP needed to reach the next level
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static float getOfflineXPRemaining(UUID uuid, String skillType) {
-//        PrimarySkillType skill = getNonChildSkillType(skillType);
-//        PlayerProfile profile = getOfflineProfile(uuid);
-//
-//        return profile.getExperienceHandler().getExperienceToNextLevel(skill) - profile.getExperienceHandler().getSkillXpLevelRaw(skill);
-//    }
-//
-//    /**
-//     * Add levels to a skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to add levels to
-//     * @param skillType Type of skill to add levels to
-//     * @param levels Number of levels to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    public static void addLevel(Player player, String skillType, int levels) {
-//        getPlayer(player).getExperienceHandler().addLevels(getSkillType(skillType), levels);
-//    }
-//
-//    /**
-//     * Add levels to a skill for an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to add levels to
-//     * @param skillType Type of skill to add levels to
-//     * @param levels Number of levels to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static void addLevelOffline(String playerName, String skillType, int levels) {
-//        PlayerProfile profile = getOfflineProfile(playerName);
-//        PrimarySkillType skill = getSkillType(skillType);
-//
-//        if (skill.isChildSkill()) {
-//            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
-//
-//            for (PrimarySkillType parentSkill : parentSkills) {
-//                profile.getExperienceHandler().addLevels(parentSkill, (levels / parentSkills.size()));
-//            }
-//
-//            mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//            return;
-//        }
-//
-//        profile.getExperienceHandler().addLevels(skill, levels);
-//        mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//    }
-//
-//    /**
-//     * Add levels to a skill for an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to add levels to
-//     * @param skillType Type of skill to add levels to
-//     * @param levels Number of levels to add
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    public static void addLevelOffline(UUID uuid, String skillType, int levels) {
-//        PlayerProfile profile = getOfflineProfile(uuid);
-//        PrimarySkillType skill = getSkillType(skillType);
-//
-//        if (skill.isChildSkill()) {
-//            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
-//
-//            for (PrimarySkillType parentSkill : parentSkills) {
-//                profile.getExperienceHandler().addLevels(parentSkill, (levels / parentSkills.size()));
-//            }
-//
-//            mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//            return;
-//        }
-//
-//        profile.getExperienceHandler().addLevels(skill, levels);
-//        mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//    }
-//
-//    /**
-//     * Get the level a player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get the level for
-//     * @param skillType The skill to get the level for
-//     * @return the level of a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @deprecated Use getLevel(Player player, PrimarySkillType skillType) instead
-//     */
-//    @Deprecated
-//    public static int getLevel(Player player, String skillType) {
-//        return getPlayer(player).getExperienceHandler().getSkillLevel(getSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the level a player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get the level for
-//     * @param skillType The skill to get the level for
-//     * @return the level of a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    public static int getLevel(Player player, PrimarySkillType skillType) {
-//        return getPlayer(player).getExperienceHandler().getSkillLevel(skillType);
-//    }
-//
-//    /**
-//     * Get the level an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get the level for
-//     * @param skillType The skill to get the level for
-//     * @return the level of a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static int getLevelOffline(String playerName, String skillType) {
-//        return getOfflineProfile(playerName).getExperienceHandler().getSkillLevel(getSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the level an offline player has in a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get the level for
-//     * @param skillType The skill to get the level for
-//     * @return the level of a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    public static int getLevelOffline(UUID uuid, String skillType) {
-//        return getOfflineProfile(uuid).getExperienceHandler().getSkillLevel(getSkillType(skillType));
-//    }
-//
-//    /**
-//     * Gets the power level of a player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to get the power level for
-//     * @return the power level of the player
-//     */
-//    public static int getPowerLevel(Player player) {
-//        return getPlayer(player).getExperienceHandler().getPowerLevel();
-//    }
-//
-//    /**
-//     * Gets the power level of an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to get the power level for
-//     * @return the power level of the player
-//     *
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static int getPowerLevelOffline(String playerName) {
-//        int powerLevel = 0;
-//        PlayerProfile profile = getOfflineProfile(playerName);
-//
-//        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
-//            powerLevel += profile.getExperienceHandler().getSkillLevel(type);
-//        }
-//
-//        return powerLevel;
-//    }
-//
-//    /**
-//     * Gets the power level of an offline player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to get the power level for
-//     * @return the power level of the player
-//     *
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    public static int getPowerLevelOffline(UUID uuid) {
-//        int powerLevel = 0;
-//        PlayerProfile profile = getOfflineProfile(uuid);
-//
-//        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
-//            powerLevel += profile.getExperienceHandler().getSkillLevel(type);
-//        }
-//
-//        return powerLevel;
-//    }
-//
-//    /**
-//     * Get the level cap of a specific skill.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param skillType The skill to get the level cap for
-//     * @return the level cap of a given skill
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    public static int getLevelCap(String skillType) {
-//        return Config.getInstance().getLevelCap(getSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the power level cap.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @return the overall power level cap
-//     */
-//    public static int getPowerLevelCap() {
-//        return Config.getInstance().getPowerLevelCap();
-//    }
-//
-//    /**
-//     * Get the position on the leaderboard of a player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The name of the player to check
-//     * @param skillType The skill to check
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     *
-//     * @return the position on the leaderboard
-//     */
-//    @Deprecated
-//    public static int getPlayerRankSkill(String playerName, String skillType) {
-//        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(playerName).getName()).get(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the position on the leaderboard of a player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The name of the player to check
-//     * @param skillType The skill to check
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     *
-//     * @return the position on the leaderboard
-//     */
-//    public static int getPlayerRankSkill(UUID uuid, String skillType) {
-//        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(uuid).getName()).get(getNonChildSkillType(skillType));
-//    }
-//
-//    /**
-//     * Get the position on the power level leaderboard of a player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The name of the player to check
-//     *
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     *
-//     * @return the position on the power level leaderboard
-//     */
-//    @Deprecated
-//    public static int getPlayerRankOverall(String playerName) {
-//        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(playerName).getName()).get(null);
-//    }
-//
-//    /**
-//     * Get the position on the power level leaderboard of a player.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The name of the player to check
-//     *
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     *
-//     * @return the position on the power level leaderboard
-//     */
-//    public static int getPlayerRankOverall(UUID uuid) {
-//        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(uuid).getName()).get(null);
-//    }
-//
-//    /**
-//     * Sets the level of a player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to set the level of
-//     * @param skillType The skill to set the level for
-//     * @param skillLevel The value to set the level to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     */
-//    public static void setLevel(Player player, String skillType, int skillLevel) {
-//        getPlayer(player).getExperienceHandler().setSkillLevel(getSkillType(skillType), skillLevel);
-//    }
-//
-//    /**
-//     * Sets the level of an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to set the level of
-//     * @param skillType The skill to set the level for
-//     * @param skillLevel The value to set the level to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    @Deprecated
-//    public static void setLevelOffline(String playerName, String skillType, int skillLevel) {
-//        getOfflineProfile(playerName).getExperienceHandler().setSkillLevel(getSkillType(skillType), skillLevel);
-//    }
-//
-//    /**
-//     * Sets the level of an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to set the level of
-//     * @param skillType The skill to set the level for
-//     * @param skillLevel The value to set the level to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     */
-//    public static void setLevelOffline(UUID uuid, String skillType, int skillLevel) {
-//        getOfflineProfile(uuid).getExperienceHandler().setSkillLevel(getSkillType(skillType), skillLevel);
-//    }
-//
-//    /**
-//     * Sets the XP of a player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to set the XP of
-//     * @param skillType The skill to set the XP for
-//     * @param newValue The value to set the XP to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static void setXP(Player player, String skillType, int newValue) {
-//        getPlayer(player).getExperienceHandler().setSkillXpValue(getNonChildSkillType(skillType), (float) newValue);
-//    }
-//
-//    /**
-//     * Sets the XP of an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to set the XP of
-//     * @param skillType The skill to set the XP for
-//     * @param newValue The value to set the XP to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static void setXPOffline(String playerName, String skillType, int newValue) {
-//        getOfflineProfile(playerName).getExperienceHandler().setSkillXpValue(getNonChildSkillType(skillType), newValue);
-//    }
-//
-//    /**
-//     * Sets the XP of an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to set the XP of
-//     * @param skillType The skill to set the XP for
-//     * @param newValue The value to set the XP to
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static void setXPOffline(UUID uuid, String skillType, int newValue) {
-//        getOfflineProfile(uuid).getExperienceHandler().setSkillXpValue(getNonChildSkillType(skillType), newValue);
-//    }
-//
-//    /**
-//     * Removes XP from a player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param player The player to change the XP of
-//     * @param skillType The skill to change the XP for
-//     * @param xp The amount of XP to remove
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static void removeXP(Player player, String skillType, int xp) {
-//        getPlayer(player).getExperienceHandler().removeXp(getNonChildSkillType(skillType), xp);
-//    }
-//
-//    /**
-//     * Removes XP from an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param playerName The player to change the XP of
-//     * @param skillType The skill to change the XP for
-//     * @param xp The amount of XP to remove
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    @Deprecated
-//    public static void removeXPOffline(String playerName, String skillType, int xp) {
-//        getOfflineProfile(playerName).getExperienceHandler().removeXp(getNonChildSkillType(skillType), xp);
-//    }
-//
-//    /**
-//     * Removes XP from an offline player in a specific skill type.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param uuid The player to change the XP of
-//     * @param skillType The skill to change the XP for
-//     * @param xp The amount of XP to remove
-//     *
-//     * @throws InvalidSkillException if the given skill is not valid
-//     * @throws InvalidPlayerException if the given player does not exist in the database
-//     * @throws UnsupportedOperationException if the given skill is a child skill
-//     */
-//    public static void removeXPOffline(UUID uuid, String skillType, int xp) {
-//        getOfflineProfile(uuid).getExperienceHandler().removeXp(getNonChildSkillType(skillType), xp);
-//    }
-//
-//    /**
-//     * Check how much XP is needed for a specific level with the selected level curve.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param level The level to get the amount of XP for
-//     *
-//     * @throws InvalidFormulaTypeException if the given formulaType is not valid
-//     */
-//    public static int getXpNeededToLevel(int level) {
-//        return mcMMO.getFormulaManager().getXPtoNextLevel(level, ExperienceConfig.getInstance().getFormulaType());
-//    }
-//
-//    /**
-//     * Check how much XP is needed for a specific level with the provided level curve.
-//     * </br>
-//     * This function is designed for API usage.
-//     *
-//     * @param level The level to get the amount of XP for
-//     * @param formulaType The formula type to get the amount of XP for
-//     *
-//     * @throws InvalidFormulaTypeException if the given formulaType is not valid
-//     */
-//    public static int getXpNeededToLevel(int level, String formulaType) {
-//        return mcMMO.getFormulaManager().getXPtoNextLevel(level, getFormulaType(formulaType));
-//    }
-//
-//    /**
-//     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given
-//     * @param blockStates the blocks to reward XP for
-//     * @param mmoPlayer the target player
-//     */
-//    public static void addXpFromBlocks(ArrayList<BlockState> blockStates, OnlineMMOPlayer mmoPlayer)
-//    {
-//        for(BlockState bs : blockStates)
-//        {
-//            for(PrimarySkillType skillType : PrimarySkillType.values())
-//            {
-//                if(ExperienceConfig.getInstance().getXp(skillType, bs.getType()) > 0)
-//                {
-//                    mmoPlayer.getExperienceHandler().applyXpGain(Misc.adaptPlayer(mmoPlayer), skillType, ExperienceConfig.getInstance().getXp(skillType, bs.getType()), XPGainReason.PVE, XPGainSource.SELF);
-//                }
-//            }
-//        }
-//    }
-//
-//    /**
-//     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given if it matches the given skillType
-//     * @param blockStates the blocks to reward XP for
-//     * @param mmoPlayer the target player
-//     * @param skillType target primary skill
-//     */
-//    public static void addXpFromBlocksBySkill(ArrayList<BlockState> blockStates, OnlineMMOPlayer mmoPlayer, PrimarySkillType skillType)
-//    {
-//        for(BlockState bs : blockStates)
-//        {
-//            if(ExperienceConfig.getInstance().getXp(skillType, bs.getType()) > 0)
-//            {
-//                mmoPlayer.getExperienceHandler().applyXpGain(Misc.adaptPlayer(mmoPlayer), skillType, ExperienceConfig.getInstance().getXp(skillType, bs.getType()), XPGainReason.PVE, XPGainSource.SELF);
-//            }
-//        }
-//    }
-//
-//    /**
-//     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given
-//     * @param blockState The target blockstate
-//     * @param mmoPlayer The target player
-//     */
-//    public static void addXpFromBlock(BlockState blockState, OnlineMMOPlayer mmoPlayer)
-//    {
-//        for(PrimarySkillType skillType : PrimarySkillType.values())
-//        {
-//            if(ExperienceConfig.getInstance().getXp(skillType, blockState.getType()) > 0)
-//            {
-//                mmoPlayer.getExperienceHandler().applyXpGain(Misc.adaptPlayer(mmoPlayer), skillType, ExperienceConfig.getInstance().getXp(skillType, blockState.getType()), XPGainReason.PVE, XPGainSource.SELF);
-//            }
-//        }
-//    }
-//
-//    /**
-//     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given if it matches the given skillType
-//     * @param blockState The target blockstate
-//     * @param mmoPlayer The target player
-//     * @param skillType target primary skill
-//     */
-//    public static void addXpFromBlockBySkill(BlockState blockState, OnlineMMOPlayer mmoPlayer, PrimarySkillType skillType)
-//    {
-//        if(ExperienceConfig.getInstance().getXp(skillType, blockState.getType()) > 0)
-//        {
-//            mmoPlayer.getExperienceHandler().applyXpGain(Misc.adaptPlayer(mmoPlayer), skillType, ExperienceConfig.getInstance().getXp(skillType, blockState.getType()), XPGainReason.PVE, XPGainSource.SELF);
-//        }
-//    }
-//
-//
-//
-//    // Utility methods follow.
-//    private static void addOfflineXP(UUID playerUniqueId, PrimarySkillType skill, int XP) {
-//        PlayerProfile profile = getOfflineProfile(playerUniqueId);
-//
-//        profile.getExperienceHandler().addXp(skill, XP);
-//        mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//    }
-//
-//    @Deprecated
-//    private static void addOfflineXP(String playerName, PrimarySkillType skill, int XP) {
-//        PlayerProfile profile = getOfflineProfile(playerName);
-//
-//        profile.getExperienceHandler().addXp(skill, XP);
-//        mcMMO.getUserManager().scheduleAsyncSave(profile.getPersistentPlayerData());
-//    }
-//
-//    private static PlayerProfile getOfflineProfile(UUID uuid) {
-//        PlayerProfile profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(uuid);
-//
-//        if (profile == null) {
-//            throw new InvalidPlayerException();
-//        }
-//
-//        return profile;
-//    }
-//
-//    @Deprecated
-//    private static PlayerProfile getOfflineProfile(String playerName) {
-//        UUID uuid = mcMMO.p.getServer().getOfflinePlayer(playerName).getUniqueId();
-//        PlayerProfile profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(uuid);
-//
-//        if (profile == null) {
-//            throw new InvalidPlayerException();
-//        }
-//
-//        return profile;
-//    }
-//
-//    private static PrimarySkillType getSkillType(String skillType) throws InvalidSkillException {
-//        PrimarySkillType skill = mcMMO.p.getSkillRegister().getSkill(skillType);
-//
-//        if (skill == null) {
-//            throw new InvalidSkillException();
-//        }
-//
-//        return skill;
-//    }
-//
-//    private static PrimarySkillType getNonChildSkillType(String skillType) throws InvalidSkillException, UnsupportedOperationException {
-//        PrimarySkillType skill = getSkillType(skillType);
-//
-//        if (skill.isChildSkill()) {
-//            throw new UnsupportedOperationException("Child skills do not have XP");
-//        }
-//
-//        return skill;
-//    }
-//
-//    private static XPGainReason getXPGainReason(String reason) throws InvalidXPGainReasonException {
-//        XPGainReason xpGainReason = XPGainReason.getXPGainReason(reason);
-//
-//        if (xpGainReason == null) {
-//            throw new InvalidXPGainReasonException();
-//        }
-//
-//        return xpGainReason;
-//    }
-//
-//    private static FormulaType getFormulaType(String formula) throws InvalidFormulaTypeException {
-//        FormulaType formulaType = FormulaType.getFormulaType(formula);
-//
-//        if (formulaType == null) {
-//            throw new InvalidFormulaTypeException();
-//        }
-//
-//        return formulaType;
-//    }
-//
-//    /**
-//     * @deprecated Use UserManager::getPlayer(Player player) instead
-//     * @param player target player
-//     * @return OnlineMMOPlayer for that player if the profile is loaded, otherwise null
-//     * @throws McMMOPlayerNotFoundException
-//     */
-//    @Deprecated
-//    private static OnlineMMOPlayer getPlayer(Player player) throws McMMOPlayerNotFoundException {
-//        if (!mcMMO.getUserManager().hasPlayerDataKey(player)) {
-//            throw new McMMOPlayerNotFoundException(player);
-//        }
-//
-//        return mcMMO.getUserManager().queryPlayer(player);
-//    }
-//}
+package com.gmail.nossr50.api;
+
+import com.gmail.nossr50.api.exceptions.*;
+import com.gmail.nossr50.config.Config;
+import com.gmail.nossr50.config.experience.ExperienceConfig;
+import com.gmail.nossr50.datatypes.experience.FormulaType;
+import com.gmail.nossr50.datatypes.experience.XPGainReason;
+import com.gmail.nossr50.datatypes.experience.XPGainSource;
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+import com.gmail.nossr50.datatypes.player.PlayerProfile;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.skills.child.FamilyTree;
+import com.gmail.nossr50.util.player.UserManager;
+import com.gmail.nossr50.util.skills.CombatUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.block.BlockState;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.UUID;
+
+public final class ExperienceAPI {
+    private ExperienceAPI() {}
+
+    /**
+     * Returns whether given string is a valid type of skill suitable for the
+     * other API calls in this class.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param skillType A string that may or may not be a skill
+     * @return true if this is a valid mcMMO skill
+     */
+    public static boolean isValidSkillType(String skillType) {
+        return PrimarySkillType.getSkill(skillType) != null;
+    }
+
+    /**
+     * Start the task that gives combat XP.
+     * Processes combat XP like mcMMO normally would, so mcMMO will check whether or not the entity should reward XP when giving out the XP
+     *
+     * @param mcMMOPlayer The attacking player
+     * @param target The defending entity
+     * @param primarySkillType The skill being used
+     * @param multiplier final XP result will be multiplied by this
+     * @deprecated Draft API
+     */
+    @Deprecated
+    public static void addCombatXP(McMMOPlayer mcMMOPlayer, LivingEntity target, PrimarySkillType primarySkillType, double multiplier) {
+        CombatUtils.processCombatXP(mcMMOPlayer, target, primarySkillType, multiplier);
+    }
+
+    /**
+     * Start the task that gives combat XP.
+     * Processes combat XP like mcMMO normally would, so mcMMO will check whether or not the entity should reward XP when giving out the XP
+     *
+     * @param mcMMOPlayer The attacking player
+     * @param target The defending entity
+     * @param primarySkillType The skill being used
+     * @deprecated Draft API
+     */
+    @Deprecated
+    public static void addCombatXP(McMMOPlayer mcMMOPlayer, LivingEntity target, PrimarySkillType primarySkillType) {
+        CombatUtils.processCombatXP(mcMMOPlayer, target, primarySkillType);
+    }
+
+    /**
+     * Returns whether the given skill type string is both valid and not a
+     * child skill. (Child skills have no XP of their own, and their level is
+     * derived from the parent(s).)
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param skillType the skill to check
+     * @return true if this is a valid, non-child mcMMO skill
+     */
+    public static boolean isNonChildSkill(String skillType) {
+        PrimarySkillType skill = PrimarySkillType.getSkill(skillType);
+
+        return skill != null && !skill.isChildSkill();
+    }
+
+    @Deprecated
+    public static void addRawXP(Player player, String skillType, int XP) {
+        addRawXP(player, skillType, (float) XP);
+    }
+
+    /**
+     * Adds raw XP to the player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    @Deprecated
+    public static void addRawXP(Player player, String skillType, float XP) {
+        addRawXP(player, skillType, XP, "UNKNOWN");
+    }
+
+    /**
+     * Adds raw XP to the player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addRawXP(Player player, String skillType, float XP, String xpGainReason) {
+        addRawXP(player, skillType, XP, xpGainReason, false);
+    }
+
+    /**
+     * Adds raw XP to the player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     * @param isUnshared true if the XP cannot be shared with party members
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addRawXP(Player player, String skillType, float XP, String xpGainReason, boolean isUnshared) {
+        if (isUnshared) {
+            getPlayer(player).beginUnsharedXpGain(getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+            return;
+        }
+
+        getPlayer(player).applyXpGain(getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+    }
+
+    /**
+     * Adds raw XP to an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @deprecated We're using float for our XP values now
+     * replaced by {@link #addRawXPOffline(String playerName, String skillType, float XP)}
+     */
+    @Deprecated
+    public static void addRawXPOffline(String playerName, String skillType, int XP) {
+        addRawXPOffline(playerName, skillType, (float) XP);
+    }
+
+    /**
+     * Adds raw XP to an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @deprecated We're using uuids to get an offline player
+     * replaced by {@link #addRawXPOffline(UUID uuid, String skillType, float XP)}
+     *
+     * @param playerName The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static void addRawXPOffline(String playerName, String skillType, float XP) {
+        addOfflineXP(playerName, getSkillType(skillType), (int) Math.floor(XP));
+    }
+
+    /**
+     * Adds raw XP to an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The UUID of player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static void addRawXPOffline(UUID uuid, String skillType, float XP) {
+        addOfflineXP(uuid, getSkillType(skillType), (int) Math.floor(XP));
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate only.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    @Deprecated
+    public static void addMultipliedXP(Player player, String skillType, int XP) {
+        addMultipliedXP(player, skillType, XP, "UNKNOWN");
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate only.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addMultipliedXP(Player player, String skillType, int XP, String xpGainReason) {
+        getPlayer(player).applyXpGain(getSkillType(skillType), (int) (XP * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+    }
+
+    /**
+     * Adds XP to an offline player, calculates for XP Rate only.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static void addMultipliedXPOffline(String playerName, String skillType, int XP) {
+        addOfflineXP(playerName, getSkillType(skillType), (int) (XP * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate and skill modifier.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    @Deprecated
+    public static void addModifiedXP(Player player, String skillType, int XP) {
+        addModifiedXP(player, skillType, XP, "UNKNOWN");
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate and skill modifier.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addModifiedXP(Player player, String skillType, int XP, String xpGainReason) {
+        addModifiedXP(player, skillType, XP, xpGainReason, false);
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate and skill modifier.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     * @param isUnshared true if the XP cannot be shared with party members
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addModifiedXP(Player player, String skillType, int XP, String xpGainReason, boolean isUnshared) {
+        PrimarySkillType skill = getSkillType(skillType);
+
+        if (isUnshared) {
+            getPlayer(player).beginUnsharedXpGain(skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+            return;
+        }
+
+        getPlayer(player).applyXpGain(skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()), getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+    }
+
+    /**
+     * Adds XP to an offline player, calculates for XP Rate and skill modifier.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static void addModifiedXPOffline(String playerName, String skillType, int XP) {
+        PrimarySkillType skill = getSkillType(skillType);
+
+        addOfflineXP(playerName, skill, (int) (XP / skill.getXpModifier() * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()));
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
+     * and party sharing.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    @Deprecated
+    public static void addXP(Player player, String skillType, int XP) {
+        addXP(player, skillType, XP, "UNKNOWN");
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
+     * and party sharing.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addXP(Player player, String skillType, int XP, String xpGainReason) {
+        addXP(player, skillType, XP, xpGainReason, false);
+    }
+
+    /**
+     * Adds XP to the player, calculates for XP Rate, skill modifiers, perks, child skills,
+     * and party sharing.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add XP to
+     * @param skillType The skill to add XP to
+     * @param XP The amount of XP to add
+     * @param xpGainReason The reason to gain XP
+     * @param isUnshared true if the XP cannot be shared with party members
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidXPGainReasonException if the given xpGainReason is not valid
+     */
+    public static void addXP(Player player, String skillType, int XP, String xpGainReason, boolean isUnshared) {
+        if (isUnshared) {
+            getPlayer(player).beginUnsharedXpGain(getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+            return;
+        }
+
+        getPlayer(player).beginXpGain(getSkillType(skillType), XP, getXPGainReason(xpGainReason), XPGainSource.CUSTOM);
+    }
+
+    /**
+     * Get the amount of XP a player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getXP(Player player, String skillType) {
+        return getPlayer(player).getSkillXpLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static int getOfflineXP(String playerName, String skillType) {
+        return getOfflineProfile(playerName).getSkillXpLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getOfflineXP(UUID uuid, String skillType) {
+        return getOfflineProfile(uuid).getSkillXpLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the raw amount of XP a player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static float getXPRaw(Player player, String skillType) {
+        return getPlayer(player).getSkillXpLevelRaw(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the raw amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static float getOfflineXPRaw(String playerName, String skillType) {
+        return getOfflineProfile(playerName).getSkillXpLevelRaw(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the raw amount of XP an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP in a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static float getOfflineXPRaw(UUID uuid, String skillType) {
+        return getOfflineProfile(uuid).getSkillXpLevelRaw(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the total amount of XP needed to reach the next level.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get the XP amount for
+     * @param skillType The skill to get the XP amount for
+     * @return the total amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getXPToNextLevel(Player player, String skillType) {
+        return getPlayer(player).getXpToLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the total amount of XP an offline player needs to reach the next level.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the total amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static int getOfflineXPToNextLevel(String playerName, String skillType) {
+        return getOfflineProfile(playerName).getXpToLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the total amount of XP an offline player needs to reach the next level.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the total amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getOfflineXPToNextLevel(UUID uuid, String skillType) {
+        return getOfflineProfile(uuid).getXpToLevel(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the amount of XP remaining until the next level.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get the XP amount for
+     * @param skillType The skill to get the XP amount for
+     * @return the amount of XP remaining until the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static int getXPRemaining(Player player, String skillType) {
+        PrimarySkillType skill = getNonChildSkillType(skillType);
+
+        PlayerProfile profile = getPlayer(player).getProfile();
+
+        return profile.getXpToLevel(skill) - profile.getSkillXpLevel(skill);
+    }
+
+    /**
+     * Get the amount of XP an offline player has left before leveling up.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static int getOfflineXPRemaining(String playerName, String skillType) {
+        PrimarySkillType skill = getNonChildSkillType(skillType);
+        PlayerProfile profile = getOfflineProfile(playerName);
+
+        return profile.getXpToLevel(skill) - profile.getSkillXpLevel(skill);
+    }
+
+    /**
+     * Get the amount of XP an offline player has left before leveling up.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get XP for
+     * @param skillType The skill to get XP for
+     * @return the amount of XP needed to reach the next level
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static float getOfflineXPRemaining(UUID uuid, String skillType) {
+        PrimarySkillType skill = getNonChildSkillType(skillType);
+        PlayerProfile profile = getOfflineProfile(uuid);
+
+        return profile.getXpToLevel(skill) - profile.getSkillXpLevelRaw(skill);
+    }
+
+    /**
+     * Add levels to a skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to add levels to
+     * @param skillType Type of skill to add levels to
+     * @param levels Number of levels to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    public static void addLevel(Player player, String skillType, int levels) {
+        getPlayer(player).addLevels(getSkillType(skillType), levels);
+    }
+
+    /**
+     * Add levels to a skill for an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to add levels to
+     * @param skillType Type of skill to add levels to
+     * @param levels Number of levels to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static void addLevelOffline(String playerName, String skillType, int levels) {
+        PlayerProfile profile = getOfflineProfile(playerName);
+        PrimarySkillType skill = getSkillType(skillType);
+
+        if (skill.isChildSkill()) {
+            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+
+            for (PrimarySkillType parentSkill : parentSkills) {
+                profile.addLevels(parentSkill, (levels / parentSkills.size()));
+            }
+
+            profile.scheduleAsyncSave();
+            return;
+        }
+
+        profile.addLevels(skill, levels);
+        profile.scheduleAsyncSave();
+    }
+
+    /**
+     * Add levels to a skill for an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to add levels to
+     * @param skillType Type of skill to add levels to
+     * @param levels Number of levels to add
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static void addLevelOffline(UUID uuid, String skillType, int levels) {
+        PlayerProfile profile = getOfflineProfile(uuid);
+        PrimarySkillType skill = getSkillType(skillType);
+
+        if (skill.isChildSkill()) {
+            Set<PrimarySkillType> parentSkills = FamilyTree.getParents(skill);
+
+            for (PrimarySkillType parentSkill : parentSkills) {
+                profile.addLevels(parentSkill, (levels / parentSkills.size()));
+            }
+
+            profile.scheduleAsyncSave();
+            return;
+        }
+
+        profile.addLevels(skill, levels);
+        profile.scheduleAsyncSave();
+    }
+
+    /**
+     * Get the level a player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get the level for
+     * @param skillType The skill to get the level for
+     * @return the level of a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @deprecated Use getLevel(Player player, PrimarySkillType skillType) instead
+     */
+    @Deprecated
+    public static int getLevel(Player player, String skillType) {
+        return getPlayer(player).getSkillLevel(getSkillType(skillType));
+    }
+
+    /**
+     * Get the level a player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get the level for
+     * @param skillType The skill to get the level for
+     * @return the level of a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    public static int getLevel(Player player, PrimarySkillType skillType) {
+        return getPlayer(player).getSkillLevel(skillType);
+    }
+
+    /**
+     * Get the level an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get the level for
+     * @param skillType The skill to get the level for
+     * @return the level of a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static int getLevelOffline(String playerName, String skillType) {
+        return getOfflineProfile(playerName).getSkillLevel(getSkillType(skillType));
+    }
+
+    /**
+     * Get the level an offline player has in a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get the level for
+     * @param skillType The skill to get the level for
+     * @return the level of a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static int getLevelOffline(UUID uuid, String skillType) {
+        return getOfflineProfile(uuid).getSkillLevel(getSkillType(skillType));
+    }
+
+    /**
+     * Gets the power level of a player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to get the power level for
+     * @return the power level of the player
+     */
+    public static int getPowerLevel(Player player) {
+        return getPlayer(player).getPowerLevel();
+    }
+
+    /**
+     * Gets the power level of an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to get the power level for
+     * @return the power level of the player
+     *
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static int getPowerLevelOffline(String playerName) {
+        int powerLevel = 0;
+        PlayerProfile profile = getOfflineProfile(playerName);
+
+        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
+            powerLevel += profile.getSkillLevel(type);
+        }
+
+        return powerLevel;
+    }
+
+    /**
+     * Gets the power level of an offline player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to get the power level for
+     * @return the power level of the player
+     *
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static int getPowerLevelOffline(UUID uuid) {
+        int powerLevel = 0;
+        PlayerProfile profile = getOfflineProfile(uuid);
+
+        for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
+            powerLevel += profile.getSkillLevel(type);
+        }
+
+        return powerLevel;
+    }
+
+    /**
+     * Get the level cap of a specific skill.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param skillType The skill to get the level cap for
+     * @return the level cap of a given skill
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    public static int getLevelCap(String skillType) {
+        return Config.getInstance().getLevelCap(getSkillType(skillType));
+    }
+
+    /**
+     * Get the power level cap.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @return the overall power level cap
+     */
+    public static int getPowerLevelCap() {
+        return Config.getInstance().getPowerLevelCap();
+    }
+
+    /**
+     * Get the position on the leaderboard of a player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The name of the player to check
+     * @param skillType The skill to check
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     *
+     * @return the position on the leaderboard
+     */
+    @Deprecated
+    public static int getPlayerRankSkill(String playerName, String skillType) {
+        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(playerName).getName()).get(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the position on the leaderboard of a player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The name of the player to check
+     * @param skillType The skill to check
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     *
+     * @return the position on the leaderboard
+     */
+    public static int getPlayerRankSkill(UUID uuid, String skillType) {
+        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(uuid).getName()).get(getNonChildSkillType(skillType));
+    }
+
+    /**
+     * Get the position on the power level leaderboard of a player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The name of the player to check
+     *
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     *
+     * @return the position on the power level leaderboard
+     */
+    @Deprecated
+    public static int getPlayerRankOverall(String playerName) {
+        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(playerName).getName()).get(null);
+    }
+
+    /**
+     * Get the position on the power level leaderboard of a player.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The name of the player to check
+     *
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     *
+     * @return the position on the power level leaderboard
+     */
+    public static int getPlayerRankOverall(UUID uuid) {
+        return mcMMO.getDatabaseManager().readRank(mcMMO.p.getServer().getOfflinePlayer(uuid).getName()).get(null);
+    }
+
+    /**
+     * Sets the level of a player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to set the level of
+     * @param skillType The skill to set the level for
+     * @param skillLevel The value to set the level to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     */
+    public static void setLevel(Player player, String skillType, int skillLevel) {
+        getPlayer(player).modifySkill(getSkillType(skillType), skillLevel);
+    }
+
+    /**
+     * Sets the level of an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to set the level of
+     * @param skillType The skill to set the level for
+     * @param skillLevel The value to set the level to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    @Deprecated
+    public static void setLevelOffline(String playerName, String skillType, int skillLevel) {
+        getOfflineProfile(playerName).modifySkill(getSkillType(skillType), skillLevel);
+    }
+
+    /**
+     * Sets the level of an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to set the level of
+     * @param skillType The skill to set the level for
+     * @param skillLevel The value to set the level to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     */
+    public static void setLevelOffline(UUID uuid, String skillType, int skillLevel) {
+        getOfflineProfile(uuid).modifySkill(getSkillType(skillType), skillLevel);
+    }
+
+    /**
+     * Sets the XP of a player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to set the XP of
+     * @param skillType The skill to set the XP for
+     * @param newValue The value to set the XP to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static void setXP(Player player, String skillType, int newValue) {
+        getPlayer(player).setSkillXpLevel(getNonChildSkillType(skillType), newValue);
+    }
+
+    /**
+     * Sets the XP of an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to set the XP of
+     * @param skillType The skill to set the XP for
+     * @param newValue The value to set the XP to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static void setXPOffline(String playerName, String skillType, int newValue) {
+        getOfflineProfile(playerName).setSkillXpLevel(getNonChildSkillType(skillType), newValue);
+    }
+
+    /**
+     * Sets the XP of an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to set the XP of
+     * @param skillType The skill to set the XP for
+     * @param newValue The value to set the XP to
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static void setXPOffline(UUID uuid, String skillType, int newValue) {
+        getOfflineProfile(uuid).setSkillXpLevel(getNonChildSkillType(skillType), newValue);
+    }
+
+    /**
+     * Removes XP from a player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param player The player to change the XP of
+     * @param skillType The skill to change the XP for
+     * @param xp The amount of XP to remove
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static void removeXP(Player player, String skillType, int xp) {
+        getPlayer(player).removeXp(getNonChildSkillType(skillType), xp);
+    }
+
+    /**
+     * Removes XP from an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param playerName The player to change the XP of
+     * @param skillType The skill to change the XP for
+     * @param xp The amount of XP to remove
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    @Deprecated
+    public static void removeXPOffline(String playerName, String skillType, int xp) {
+        getOfflineProfile(playerName).removeXp(getNonChildSkillType(skillType), xp);
+    }
+
+    /**
+     * Removes XP from an offline player in a specific skill type.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param uuid The player to change the XP of
+     * @param skillType The skill to change the XP for
+     * @param xp The amount of XP to remove
+     *
+     * @throws InvalidSkillException if the given skill is not valid
+     * @throws InvalidPlayerException if the given player does not exist in the database
+     * @throws UnsupportedOperationException if the given skill is a child skill
+     */
+    public static void removeXPOffline(UUID uuid, String skillType, int xp) {
+        getOfflineProfile(uuid).removeXp(getNonChildSkillType(skillType), xp);
+    }
+
+    /**
+     * Check how much XP is needed for a specific level with the selected level curve.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param level The level to get the amount of XP for
+     *
+     * @throws InvalidFormulaTypeException if the given formulaType is not valid
+     */
+    public static int getXpNeededToLevel(int level) {
+        return mcMMO.getFormulaManager().getXPtoNextLevel(level, ExperienceConfig.getInstance().getFormulaType());
+    }
+
+    /**
+     * Check how much XP is needed for a specific level with the provided level curve.
+     * </br>
+     * This function is designed for API usage.
+     *
+     * @param level The level to get the amount of XP for
+     * @param formulaType The formula type to get the amount of XP for
+     *
+     * @throws InvalidFormulaTypeException if the given formulaType is not valid
+     */
+    public static int getXpNeededToLevel(int level, String formulaType) {
+        return mcMMO.getFormulaManager().getXPtoNextLevel(level, getFormulaType(formulaType));
+    }
+
+    /**
+     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given
+     * @param blockStates the blocks to reward XP for
+     * @param mcMMOPlayer the target player
+     */
+    public static void addXpFromBlocks(ArrayList<BlockState> blockStates, McMMOPlayer mcMMOPlayer)
+    {
+        for(BlockState bs : blockStates)
+        {
+            for(PrimarySkillType skillType : PrimarySkillType.values())
+            {
+                if(ExperienceConfig.getInstance().getXp(skillType, bs.getType()) > 0)
+                {
+                    mcMMOPlayer.applyXpGain(skillType, ExperienceConfig.getInstance().getXp(skillType, bs.getType()), XPGainReason.PVE, XPGainSource.SELF);
+                }
+            }
+        }
+    }
+
+    /**
+     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given if it matches the given skillType
+     * @param blockStates the blocks to reward XP for
+     * @param mcMMOPlayer the target player
+     * @param skillType target primary skill
+     */
+    public static void addXpFromBlocksBySkill(ArrayList<BlockState> blockStates, McMMOPlayer mcMMOPlayer, PrimarySkillType skillType)
+    {
+        for(BlockState bs : blockStates)
+        {
+            if(ExperienceConfig.getInstance().getXp(skillType, bs.getType()) > 0)
+            {
+                mcMMOPlayer.applyXpGain(skillType, ExperienceConfig.getInstance().getXp(skillType, bs.getType()), XPGainReason.PVE, XPGainSource.SELF);
+            }
+        }
+    }
+
+    /**
+     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given
+     * @param blockState The target blockstate
+     * @param mcMMOPlayer The target player
+     */
+    public static void addXpFromBlock(BlockState blockState, McMMOPlayer mcMMOPlayer)
+    {
+        for(PrimarySkillType skillType : PrimarySkillType.values())
+        {
+            if(ExperienceConfig.getInstance().getXp(skillType, blockState.getType()) > 0)
+            {
+                mcMMOPlayer.applyXpGain(skillType, ExperienceConfig.getInstance().getXp(skillType, blockState.getType()), XPGainReason.PVE, XPGainSource.SELF);
+            }
+        }
+    }
+
+    /**
+     * Will add the appropriate type of XP from the block to the player based on the material of the blocks given if it matches the given skillType
+     * @param blockState The target blockstate
+     * @param mcMMOPlayer The target player
+     * @param skillType target primary skill
+     */
+    public static void addXpFromBlockBySkill(BlockState blockState, McMMOPlayer mcMMOPlayer, PrimarySkillType skillType)
+    {
+        if(ExperienceConfig.getInstance().getXp(skillType, blockState.getType()) > 0)
+        {
+            mcMMOPlayer.applyXpGain(skillType, ExperienceConfig.getInstance().getXp(skillType, blockState.getType()), XPGainReason.PVE, XPGainSource.SELF);
+        }
+    }
+
+    // Utility methods follow.
+    private static void addOfflineXP(UUID playerUniqueId, PrimarySkillType skill, int XP) {
+        PlayerProfile profile = getOfflineProfile(playerUniqueId);
+
+        profile.addXp(skill, XP);
+        profile.save(true);
+    }
+
+    @Deprecated
+    private static void addOfflineXP(String playerName, PrimarySkillType skill, int XP) {
+        PlayerProfile profile = getOfflineProfile(playerName);
+
+        profile.addXp(skill, XP);
+        profile.scheduleAsyncSave();
+    }
+
+    private static PlayerProfile getOfflineProfile(UUID uuid) throws InvalidPlayerException {
+        OfflinePlayer offlinePlayer = Bukkit.getServer().getOfflinePlayer(uuid);
+        String playerName = offlinePlayer.getName();
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, playerName);
+
+        if (!profile.isLoaded()) {
+            throw new InvalidPlayerException();
+        }
+
+        return profile;
+    }
+
+    @Deprecated
+    private static PlayerProfile getOfflineProfile(String playerName) throws InvalidPlayerException {
+        OfflinePlayer offlinePlayer = Bukkit.getServer().getOfflinePlayer(playerName);
+        UUID uuid = offlinePlayer.getUniqueId();
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, playerName);
+
+        if (!profile.isLoaded()) {
+            throw new InvalidPlayerException();
+        }
+
+        return profile;
+    }
+
+    private static PrimarySkillType getSkillType(String skillType) throws InvalidSkillException {
+        PrimarySkillType skill = PrimarySkillType.getSkill(skillType);
+
+        if (skill == null) {
+            throw new InvalidSkillException();
+        }
+
+        return skill;
+    }
+
+    private static PrimarySkillType getNonChildSkillType(String skillType) throws InvalidSkillException, UnsupportedOperationException {
+        PrimarySkillType skill = getSkillType(skillType);
+
+        if (skill.isChildSkill()) {
+            throw new UnsupportedOperationException("Child skills do not have XP");
+        }
+
+        return skill;
+    }
+
+    private static XPGainReason getXPGainReason(String reason) throws InvalidXPGainReasonException {
+        XPGainReason xpGainReason = XPGainReason.getXPGainReason(reason);
+
+        if (xpGainReason == null) {
+            throw new InvalidXPGainReasonException();
+        }
+
+        return xpGainReason;
+    }
+
+    private static FormulaType getFormulaType(String formula) throws InvalidFormulaTypeException {
+        FormulaType formulaType = FormulaType.getFormulaType(formula);
+
+        if (formulaType == null) {
+            throw new InvalidFormulaTypeException();
+        }
+
+        return formulaType;
+    }
+
+    /**
+     * @deprecated Use UserManager::getPlayer(Player player) instead
+     * @param player target player
+     * @return McMMOPlayer for that player if the profile is loaded, otherwise null
+     * @throws McMMOPlayerNotFoundException
+     */
+    @Deprecated
+    private static McMMOPlayer getPlayer(Player player) throws McMMOPlayerNotFoundException {
+        if (!UserManager.hasPlayerDataKey(player)) {
+            throw new McMMOPlayerNotFoundException(player);
+        }
+
+        return UserManager.getPlayer(player);
+    }
+}

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java

@@ -58,7 +58,7 @@ public class ConvertDatabaseCommand implements CommandExecutor {
             }
 
             for (Player player : mcMMO.p.getServer().getOnlinePlayers()) {
-                PlayerProfile profile = oldDatabase.queryPlayerDataByUUID(player.getUniqueId());
+                PlayerProfile profile = oldDatabase.queryPlayerDataByUUID(player.getUniqueId(), null);
 
                 if(profile == null)
                     continue;

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/database/DatabaseRemovePlayerCommand.java

@@ -23,7 +23,7 @@ public class DatabaseRemovePlayerCommand implements TabExecutor {
             String playerName = CommandUtils.getMatchedPlayerName(args[0]);
 
             if (mcMMO.getUserManager().queryPlayer(playerName) == null
-                    && CommandUtils.hasNoProfile(sender, mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName, false))) {
+                    && CommandUtils.hasNoProfile(sender, mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName))) {
                 sender.sendMessage(LocaleLoader.getString("Commands.Offline"));
                 return true;
             }

+ 43 - 39
src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java

@@ -1,14 +1,13 @@
 package com.gmail.nossr50.commands.experience;
 
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.util.Misc;
 import com.gmail.nossr50.util.commands.CommandUtils;
+import com.gmail.nossr50.util.player.UserManager;
 import com.google.common.collect.ImmutableList;
-import com.neetgames.mcmmo.player.MMOPlayer;
-import com.neetgames.mcmmo.player.OnlineMMOPlayer;
-import com.neetgames.mcmmo.skill.RootSkill;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
@@ -16,7 +15,6 @@ import org.bukkit.command.TabExecutor;
 import org.bukkit.entity.Player;
 import org.bukkit.util.StringUtil;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -25,7 +23,7 @@ import java.util.UUID;
 public abstract class ExperienceCommand implements TabExecutor {
     @Override
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
-        RootSkill rootSkill;
+        PrimarySkillType skill;
 
         if(args.length < 2) {
             return false;
@@ -46,27 +44,27 @@ public abstract class ExperienceCommand implements TabExecutor {
                     return true;
                 }
 
-                rootSkill = mcMMO.p.getSkillRegister().getSkill(args[0]);
+                skill = PrimarySkillType.getSkill(args[0]);
 
                 if (args[1].equalsIgnoreCase("all")) {
-                    rootSkill = null;
+                    skill = null;
                 }
 
-                if (rootSkill != null && rootSkill.isChildSkill()) {
+                if (skill != null && skill.isChildSkill())
+                {
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     return true;
                 }
 
                 //Profile not loaded
-                Player player = (Player) sender;
-                OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(player);
-                if(mmoPlayer == null) {
+                if(UserManager.getPlayer(sender.getName()) == null)
+                {
                     sender.sendMessage(LocaleLoader.getString("Profile.PendingLoad"));
                     return true;
                 }
 
 
-                editValues(mmoPlayer, rootSkill, Integer.parseInt(args[1]), isSilent(args));
+                editValues((Player) sender, UserManager.getPlayer(sender.getName()).getProfile(), skill, Integer.parseInt(args[1]), isSilent(args));
                 return true;
             } else if((args.length == 3 && !isSilent(args))
                     || (args.length == 4 && isSilent(args))) {
@@ -79,13 +77,13 @@ public abstract class ExperienceCommand implements TabExecutor {
                     return true;
                 }
 
-                rootSkill = mcMMO.p.getSkillRegister().getSkill(args[1]);
+                skill = PrimarySkillType.getSkill(args[1]);
 
                 if (args[1].equalsIgnoreCase("all")) {
-                    rootSkill = null;
+                    skill = null;
                 }
 
-                if (rootSkill != null && rootSkill.isChildSkill())
+                if (skill != null && skill.isChildSkill())
                 {
                     sender.sendMessage(LocaleLoader.getString("Commands.Skill.ChildSkill"));
                     return true;
@@ -94,25 +92,31 @@ public abstract class ExperienceCommand implements TabExecutor {
                 int value = Integer.parseInt(args[2]);
 
                 String playerName = CommandUtils.getMatchedPlayerName(args[0]);
-                OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(playerName);
+                McMMOPlayer mcMMOPlayer = UserManager.getOfflinePlayer(playerName);
 
-                // If the mmoPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
-                if (mmoPlayer == null) {
+                // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
+                if (mcMMOPlayer == null) {
                     UUID uuid = null;
-                    OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(playerName);
-                    if (player != null) {
-                        uuid = player.getUniqueId();
-                    }
-                    PlayerProfile profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName, uuid, false);
+                    OfflinePlayer offlinePlayer = mcMMO.p.getServer().getOfflinePlayer(playerName);
+                    PlayerProfile profile;
+
+                    uuid = offlinePlayer.getUniqueId();
+                    profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, null);
+
+                    //Check loading by UUID
+                    if (CommandUtils.unloadedProfile(sender, profile)) {
+                        //Check loading by name
+                        profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
 
-                    if (CommandUtils.hasNoProfile(sender, profile)) {
-                        return true;
+                        if(CommandUtils.unloadedProfile(sender, profile)) {
+                            return true;
+                        }
                     }
 
-                    editValues(null, profile, rootSkill, value, isSilent(args));
+                    editValues(null, profile, skill, value, isSilent(args));
                 }
                 else {
-                    editValues(Misc.adaptPlayer(mmoPlayer), mcMMOPlayer.getProfile(), rootSkill, value, isSilent(args));
+                    editValues(mcMMOPlayer.getPlayer(), mcMMOPlayer.getProfile(), skill, value, isSilent(args));
                 }
 
                 handleSenderMessage(sender, playerName, skill);
@@ -148,27 +152,27 @@ public abstract class ExperienceCommand implements TabExecutor {
 
     protected abstract boolean permissionsCheckSelf(CommandSender sender);
     protected abstract boolean permissionsCheckOthers(CommandSender sender);
-    protected abstract void handleCommand(Player player, PlayerProfile profile, RootSkill rootSkill, int value);
+    protected abstract void handleCommand(Player player, PlayerProfile profile, PrimarySkillType skill, int value);
     protected abstract void handlePlayerMessageAll(Player player, int value, boolean isSilent);
-    protected abstract void handlePlayerMessageSkill(Player player, int value, RootSkill rootSkill, boolean isSilent);
+    protected abstract void handlePlayerMessageSkill(Player player, int value, PrimarySkillType skill, boolean isSilent);
 
     private boolean validateArguments(CommandSender sender, String skillName, String value) {
         return !(CommandUtils.isInvalidInteger(sender, value) || (!skillName.equalsIgnoreCase("all") && CommandUtils.isInvalidSkill(sender, skillName)));
     }
 
-    protected static void handleSenderMessage(CommandSender sender, String playerName, RootSkill rootSkill) {
-        if (rootSkill == null) {
+    protected static void handleSenderMessage(CommandSender sender, String playerName, PrimarySkillType skill) {
+        if (skill == null) {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", skill.getName(), playerName));
         }
     }
 
-    protected void editValues(@NotNull MMOPlayer mmoPlayer, @Nullable RootSkill rootSkill, int value, boolean isSilent) {
-        if (primarySkillType == null) {
-            for (PrimarySkillType type : PrimarySkillType.NON_CHILD_SKILLS) {
-                handleCommand(player, profile, type, value);
+    protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill, int value, boolean isSilent) {
+        if (skill == null) {
+            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
+                handleCommand(player, profile, primarySkillType, value);
             }
 
             if (player != null) {
@@ -176,10 +180,10 @@ public abstract class ExperienceCommand implements TabExecutor {
             }
         }
         else {
-            handleCommand(player, profile, primarySkillType, value);
+            handleCommand(player, profile, skill, value);
 
             if (player != null) {
-                handlePlayerMessageSkill(player, value, primarySkillType, isSilent);
+                handlePlayerMessageSkill(player, value, skill, isSilent);
             }
         }
     }

+ 39 - 32
src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java

@@ -1,14 +1,16 @@
 package com.gmail.nossr50.commands.experience;
 
+import com.gmail.nossr50.datatypes.experience.XPGainReason;
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.commands.CommandUtils;
+import com.gmail.nossr50.util.player.UserManager;
 import com.google.common.collect.ImmutableList;
-import com.neetgames.mcmmo.player.OnlineMMOPlayer;
-import com.neetgames.mcmmo.skill.RootSkill;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
@@ -28,7 +30,7 @@ import java.util.UUID;
 public class SkillresetCommand implements TabExecutor {
     @Override
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
-        RootSkill rootSkill;
+        PrimarySkillType skill;
         switch (args.length) {
             case 1:
                 if (CommandUtils.noConsoleUsage(sender)) {
@@ -45,14 +47,13 @@ public class SkillresetCommand implements TabExecutor {
                 }
 
                 if (args[0].equalsIgnoreCase("all")) {
-                    rootSkill = null;
+                    skill = null;
                 }
                 else {
-                    rootSkill = mcMMO.p.getSkillRegister().getSkill(args[0]);
+                    skill = PrimarySkillType.getSkill(args[0]);
                 }
 
-                editValues((Player) sender, mcMMO.getUserManager().queryPlayer(player)
-, skill);
+                editValues((Player) sender, UserManager.getPlayer(sender.getName()).getProfile(), skill);
                 return true;
 
             case 2:
@@ -66,32 +67,38 @@ public class SkillresetCommand implements TabExecutor {
                 }
 
                 if (args[1].equalsIgnoreCase("all")) {
-                    rootSkill = null;
+                    skill = null;
                 }
                 else {
-                    rootSkill = mcMMO.p.getSkillRegister().getSkill(args[1]);
+                    skill = PrimarySkillType.getSkill(args[1]);
                 }
 
                 String playerName = CommandUtils.getMatchedPlayerName(args[0]);
-                OnlineMMOPlayer mmoPlayer = mcMMO.getUserManager().queryPlayer(playerName);
+                McMMOPlayer mcMMOPlayer = UserManager.getOfflinePlayer(playerName);
 
-                // If the mmoPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
-                if (mmoPlayer == null) {
+                // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
+                if (mcMMOPlayer == null) {
                     UUID uuid = null;
                     OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(playerName);
-                    if (player != null) {
-                        uuid = player.getUniqueId();
-                    }
-                    PlayerProfile profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName, uuid, false);
+                    uuid = player.getUniqueId();
+
+                    PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, playerName);
+
+                    //Check loading by UUID
+                    if (CommandUtils.unloadedProfile(sender, profile)) {
+                        //Didn't find it by UUID so try to find it by name
+                        profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName);
 
-                    if (CommandUtils.hasNoProfile(sender, profile)) {
-                        return true;
+                        //Check if it was present in DB
+                        if(CommandUtils.unloadedProfile(sender, profile)) {
+                            return true;
+                        }
                     }
 
                     editValues(null, profile, skill);
                 }
                 else {
-                    editValues(Misc.adaptPlayer(mmoPlayer), mmoPlayer, skill);
+                    editValues(mcMMOPlayer.getPlayer(), mcMMOPlayer.getProfile(), skill);
                 }
 
                 handleSenderMessage(sender, playerName, skill);
@@ -115,18 +122,18 @@ public class SkillresetCommand implements TabExecutor {
         }
     }
 
-    protected void handleCommand(Player player, PlayerProfile profile, RootSkill rootSkill) {
-        int levelsRemoved = profile.getSkillLevel(rootSkill);
-        float xpRemoved = profile.getSkillXpLevelRaw(rootSkill);
+    protected void handleCommand(Player player, PlayerProfile profile, PrimarySkillType skill) {
+        int levelsRemoved = profile.getSkillLevel(skill);
+        float xpRemoved = profile.getSkillXpLevelRaw(skill);
 
-        profile.modifySkill(rootSkill, 0);
+        profile.modifySkill(skill, 0);
 
         if (player == null) {
             profile.scheduleAsyncSave();
             return;
         }
 
-        EventUtils.tryLevelChangeEvent(player, rootSkill, levelsRemoved, xpRemoved, false, XPGainReason.COMMAND);
+        EventUtils.tryLevelChangeEvent(player, skill, levelsRemoved, xpRemoved, false, XPGainReason.COMMAND);
     }
 
     protected boolean permissionsCheckSelf(CommandSender sender) {
@@ -141,26 +148,26 @@ public class SkillresetCommand implements TabExecutor {
         player.sendMessage(LocaleLoader.getString("Commands.Reset.All"));
     }
 
-    protected void handlePlayerMessageSkill(Player player, RootSkill rootSkill) {
-        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", rootSkill.getName()));
+    protected void handlePlayerMessageSkill(Player player, PrimarySkillType skill) {
+        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", skill.getName()));
     }
 
     private boolean validateArguments(CommandSender sender, String skillName) {
         return skillName.equalsIgnoreCase("all") || !CommandUtils.isInvalidSkill(sender, skillName);
     }
 
-    protected static void handleSenderMessage(CommandSender sender, String playerName, RootSkill rootSkill) {
-        if (rootSkill == null) {
+    protected static void handleSenderMessage(CommandSender sender, String playerName, PrimarySkillType skill) {
+        if (skill == null) {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", skill.getName(), playerName));
         }
     }
 
-    protected void editValues(Player player, PlayerProfile profile, RootSkill rootSkill) {
-        if (rootSkill == null) {
-            for (RootSkill rootSkill : PrimarySkillType.NON_CHILD_SKILLS) {
+    protected void editValues(Player player, PlayerProfile profile, PrimarySkillType skill) {
+        if (skill == null) {
+            for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
                 handleCommand(player, profile, primarySkillType);
             }
 

+ 30 - 27
src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java

@@ -1,15 +1,16 @@
 package com.gmail.nossr50.commands.player;
 
 import com.gmail.nossr50.config.Config;
+import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
+import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.commands.CommandUtils;
+import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import com.google.common.collect.ImmutableList;
-import com.neetgames.mcmmo.skill.RootSkill;
-import org.bukkit.Bukkit;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandSender;
 import org.bukkit.command.TabExecutor;
@@ -25,19 +26,15 @@ public class InspectCommand implements TabExecutor {
     public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
         if (args.length == 1) {
             String playerName = CommandUtils.getMatchedPlayerName(args[0]);
+            McMMOPlayer mcMMOPlayer = UserManager.getOfflinePlayer(playerName);
 
-            PlayerProfile playerProfile = mcMMO.getUserManager().queryPlayer(playerName);
-            Player targetPlayer = Bukkit.getPlayer(playerName);
+            // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
+            if (mcMMOPlayer == null) {
+                PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName); // Temporary Profile
 
-            if(playerProfile == null) {
-                //TODO: Localize
-                sender.sendMessage("Data was not found in the database for the given player name!");
-                return true;
-            }
-
-
-            if(targetPlayer == null) {
-                //Target is offline
+                if (!CommandUtils.isLoaded(sender, profile)) {
+                    return true;
+                }
 
                 if (Config.getInstance().getScoreboardsEnabled()
                         && sender instanceof Player
@@ -52,42 +49,48 @@ public class InspectCommand implements TabExecutor {
                 sender.sendMessage(LocaleLoader.getString("Inspect.OfflineStats", playerName));
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Gathering"));
-                for (RootSkill rootSkill : PrimarySkillType.GATHERING_SKILLS) {
+                for (PrimarySkillType skill : PrimarySkillType.GATHERING_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Combat"));
-                for (RootSkill rootSkill : PrimarySkillType.COMBAT_SKILLS) {
+                for (PrimarySkillType skill : PrimarySkillType.COMBAT_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
 
                 sender.sendMessage(LocaleLoader.getString("Stats.Header.Misc"));
-                for (RootSkill rootSkill : PrimarySkillType.MISC_SKILLS) {
+                for (PrimarySkillType skill : PrimarySkillType.MISC_SKILLS) {
                     sender.sendMessage(CommandUtils.displaySkill(profile, skill));
                 }
+
             } else {
+                Player target = mcMMOPlayer.getPlayer();
+                boolean isVanished = false;
 
-                if (CommandUtils.hidden(sender, targetPlayer, Permissions.inspectHidden(sender))) {
-                    sender.sendMessage(LocaleLoader.getString("Inspect.Offline"));
-                    return true;
-                } else if (CommandUtils.tooFar(sender, targetPlayer, Permissions.inspectFar(sender))) {
+                if (CommandUtils.hidden(sender, target, Permissions.inspectHidden(sender))) {
+                    isVanished = true;
+                }
+
+                //Only distance check players who are online and not vanished
+                if (!isVanished && CommandUtils.tooFar(sender, target, Permissions.inspectFar(sender))) {
                     return true;
                 }
 
                 if (Config.getInstance().getScoreboardsEnabled()
-                        && sender instanceof Player && Config.getInstance().getInspectUseBoard()) {
-                    ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, playerProfile);
+                        && sender instanceof Player
+                        && Config.getInstance().getInspectUseBoard()) {
+                    ScoreboardManager.enablePlayerInspectScoreboard((Player) sender, mcMMOPlayer);
 
                     if (!Config.getInstance().getInspectUseChat()) {
                         return true;
                     }
                 }
 
-                sender.sendMessage(LocaleLoader.getString("Inspect.Stats", targetPlayer.getName()));
-                CommandUtils.printGatheringSkills(targetPlayer, sender);
-                CommandUtils.printCombatSkills(targetPlayer, sender);
-                CommandUtils.printMiscSkills(targetPlayer, sender);
-                sender.sendMessage(LocaleLoader.getString("Commands.PowerLevel", playerProfile.getExperienceHandler().getPowerLevel()));
+                sender.sendMessage(LocaleLoader.getString("Inspect.Stats", target.getName()));
+                CommandUtils.printGatheringSkills(target, sender);
+                CommandUtils.printCombatSkills(target, sender);
+                CommandUtils.printMiscSkills(target, sender);
+                sender.sendMessage(LocaleLoader.getString("Commands.PowerLevel", mcMMOPlayer.getPowerLevel()));
             }
 
             return true;

+ 29 - 47
src/main/java/com/gmail/nossr50/database/DatabaseManager.java

@@ -1,16 +1,11 @@
 package com.gmail.nossr50.database;
 
+import com.gmail.nossr50.api.exceptions.InvalidSkillException;
 import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.database.DatabaseType;
 import com.gmail.nossr50.datatypes.database.PlayerStat;
-import com.gmail.nossr50.datatypes.player.MMODataSnapshot;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.neetgames.mcmmo.exceptions.InvalidSkillException;
-import com.neetgames.mcmmo.exceptions.ProfileRetrievalException;
-import com.neetgames.mcmmo.player.MMOPlayerData;
-import com.neetgames.mcmmo.skill.RootSkill;
-import org.apache.commons.lang.NullArgumentException;
 import org.bukkit.entity.Player;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -39,35 +34,36 @@ public interface DatabaseManager {
      * Remove a user from the database.
      *
      * @param playerName The name of the user to remove
-     * @param uuid uuid of player to remove, can be null
+     * @param uuid player UUID, can be null
      * @return true if the user was successfully removed, false otherwise
      */
-    boolean removeUser(@NotNull String playerName, @Nullable UUID uuid);
+    boolean removeUser(String playerName, UUID uuid);
 
     /**
      * Removes any cache used for faster lookups
      * Currently only used for SQL
      * @param uuid target UUID to cleanup
      */
-    void removeCache(@NotNull UUID uuid);
+    void cleanupUser(UUID uuid);
 
     /**
      * Save a user to the database.
      *
-     * @param mmoDataSnapshot Snapshot of player data to save
+     * @param profile The profile of the player to save
+     * @return true if successful, false on failure
      */
-    boolean saveUser(@NotNull MMODataSnapshot mmoDataSnapshot);
+    boolean saveUser(PlayerProfile profile);
 
     /**
     * Retrieve leaderboard info.
      * Will never be null but it may be empty
     *
-    * @param rootSkill The skill to retrieve info on
+    * @param skill The skill to retrieve info on
     * @param pageNumber Which page in the leaderboards to retrieve
     * @param statsPerPage The number of stats per page
     * @return the requested leaderboard information
     */
-    @NotNull List<PlayerStat> readLeaderboard(@NotNull RootSkill rootSkill, int pageNumber, int statsPerPage) throws InvalidSkillException;
+    @NotNull List<PlayerStat> readLeaderboard(@Nullable PrimarySkillType skill, int pageNumber, int statsPerPage) throws InvalidSkillException;
 
     /**
      * Retrieve rank info into a HashMap from PrimarySkillType to the rank.
@@ -78,74 +74,60 @@ public interface DatabaseManager {
      * @param playerName The name of the user to retrieve the rankings for
      * @return the requested rank information
      */
-    @NotNull Map<RootSkill, Integer> readRank(@NotNull String playerName);
+    Map<PrimarySkillType, Integer> readRank(String playerName);
 
     /**
      * Add a new user to the database.
-     *  @param playerName The name of the player to be added to the database
+     *
+     * @param playerName The name of the player to be added to the database
      * @param uuid The uuid of the player to be added to the database
      */
-    void insertNewUser(@NotNull String playerName, @NotNull UUID uuid) throws Exception;
-
-    @Nullable MMOPlayerData queryPlayerDataByPlayer(@NotNull Player player) throws ProfileRetrievalException, NullArgumentException;
+    void newUser(String playerName, UUID uuid);
 
-    /**
-     * Load player data (in the form of {@link PlayerProfile}) if player data exists
-     * Returns null if it doesn't
-     *
-     * @param uuid The uuid of the player to load from the database
-     * @param playerName the current player name for this player
-     * @return The player's data, or null if not found
-     */
-    @Nullable MMOPlayerData queryPlayerDataByUUID(@NotNull UUID uuid, @NotNull String playerName) throws ProfileRetrievalException, NullArgumentException;
+    @NotNull PlayerProfile newUser(@NotNull Player player);
 
     /**
-     * Load player data (in the form of {@link PlayerProfile}) if player data exists
-     * Returns null if it doesn't
+     * Load a player from the database.
      *
-     * @param playerName the current player name for this player
-     * @return The player's data, or null if not found
+     * @param playerName The name of the player to load from the database
+     * @return The player's data, or an unloaded PlayerProfile if not found
+     *          and createNew is false
      */
-    @Nullable MMOPlayerData queryPlayerByName(@NotNull String playerName) throws ProfileRetrievalException;
+    @NotNull PlayerProfile loadPlayerProfile(@NotNull String playerName);
 
     /**
-     * This method queries the DB for player data for target player
-     * If it fails to find data for this player, or if it does find data but the data is corrupted,
-     *  it will then proceed to make brand new data for the target player, which will be saved to the DB during the next save
+     * Load a player from the database.
      *
-     * This method will return null for all other errors, which indicates a problem with the DB, in which case mcMMO
-     *  will try to load the player data periodically, but that isn't handled in this method
-     *
-     * @param player target player
-     * @return {@link PlayerProfile} for the target player
+     * @param uuid The uuid of the player to load from the database
+     * @return The player's data, or an unloaded PlayerProfile if not found
      */
-    @Nullable MMOPlayerData initPlayerProfile(@NotNull Player player) throws Exception;
+    @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid, @Nullable String playerName);
 
     /**
      * Get all users currently stored in the database.
      *
      * @return list of playernames
      */
-    @NotNull List<String> getStoredUsers();
+    List<String> getStoredUsers();
 
     /**
      * Convert all users from this database to the provided database using
-     * {@link #saveUser(MMODataSnapshot)}.
+     * {@link #saveUser(PlayerProfile)}.
      *
      * @param destination The DatabaseManager to save to
      */
-    void convertUsers(@NotNull DatabaseManager destination);
+    void convertUsers(DatabaseManager destination);
 
-//    boolean saveUserUUID(String userName, UUID uuid);
+    boolean saveUserUUID(String userName, UUID uuid);
 
-//    boolean saveUserUUIDs(Map<String, UUID> fetchedUUIDs);
+    boolean saveUserUUIDs(Map<String, UUID> fetchedUUIDs);
 
     /**
      * Retrieve the type of database in use. Custom databases should return CUSTOM.
      *
      * @return The type of database
      */
-    @NotNull DatabaseType getDatabaseType();
+    DatabaseType getDatabaseType();
 
     /**
      * Called when the plugin disables

+ 0 - 1524
src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java

@@ -1,1524 +0,0 @@
-package com.gmail.nossr50.database;
-
-import com.gmail.nossr50.config.AdvancedConfig;
-import com.gmail.nossr50.config.Config;
-import com.gmail.nossr50.datatypes.database.DatabaseType;
-import com.gmail.nossr50.datatypes.database.PlayerStat;
-import com.gmail.nossr50.datatypes.database.UpgradeType;
-import com.gmail.nossr50.datatypes.player.*;
-import com.gmail.nossr50.datatypes.skills.CoreSkills;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
-import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask;
-import com.gmail.nossr50.util.Misc;
-import com.gmail.nossr50.util.experience.MMOExperienceBarManager;
-import com.gmail.nossr50.util.skills.SkillUtils;
-import com.gmail.nossr50.util.text.StringUtils;
-import com.google.common.collect.ImmutableMap;
-import com.neetgames.mcmmo.MobHealthBarType;
-import com.neetgames.mcmmo.UniqueDataType;
-import com.neetgames.mcmmo.exceptions.ProfileRetrievalException;
-import com.neetgames.mcmmo.player.MMOPlayerData;
-import com.neetgames.mcmmo.skill.RootSkill;
-import com.neetgames.mcmmo.skill.SkillBossBarState;
-import com.neetgames.mcmmo.skill.SuperSkill;
-import it.unimi.dsi.fastutil.Hash;
-import org.apache.commons.lang.NullArgumentException;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.*;
-import java.util.*;
-
-public final class FlatFileDatabaseManager extends AbstractDatabaseManager {
-    public static final String FLATFILE_SPLIT_CHARACTER_REGEX = ":";
-    private final HashMap<RootSkill, List<PlayerStat>> playerStatHash = new HashMap<>();
-    private final List<PlayerStat> powerLevels = new ArrayList<>();
-    private long lastUpdate = 0;
-
-    private final long UPDATE_WAIT_TIME = 600000L; // 10 minutes
-    private final File usersFile;
-    private static final Object fileWritingLock = new Object();
-
-    protected FlatFileDatabaseManager() {
-        usersFile = new File(mcMMO.getUsersFilePath());
-        checkStructure();
-        updateLeaderboards();
-
-        if (mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.ADD_UUIDS)) {
-            new UUIDUpdateAsyncTask(mcMMO.p, getStoredUsers()).start();
-        }
-    }
-
-    public void purgePowerlessUsers() {
-        int purgedUsers = 0;
-
-        mcMMO.p.getLogger().info("Purging powerless users...");
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        // This code is O(n) instead of O(n²)
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    Map<RootSkill, Integer> skills = getSkillMapFromLine(character);
-
-                    boolean powerless = true;
-                    for (int skill : skills.values()) {
-                        if (skill != 0) {
-                            powerless = false;
-                            break;
-                        }
-                    }
-
-                    // If they're still around, rewrite them to the file.
-                    if (!powerless) {
-                        writer.append(line).append("\r\n");
-                    }
-                    else {
-                        purgedUsers++;
-                    }
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        mcMMO.p.getLogger().info("Purged " + purgedUsers + " users from the database.");
-    }
-
-    public void purgeOldUsers() {
-        int removedPlayers = 0;
-        long currentTime = System.currentTimeMillis();
-
-        mcMMO.p.getLogger().info("Purging old users...");
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        // This code is O(n) instead of O(n²)
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    String name = character[FlatFileMappings.USERNAME];
-                    long lastPlayed = 0;
-                    boolean rewrite = false;
-                    try {
-                        lastPlayed = Long.parseLong(character[37]) * Misc.TIME_CONVERSION_FACTOR;
-                    }
-                    catch (NumberFormatException e) {
-                        e.printStackTrace();
-                    }
-                    if (lastPlayed == 0) {
-                        OfflinePlayer player = mcMMO.p.getServer().getOfflinePlayer(name);
-                        lastPlayed = player.getLastPlayed();
-                        rewrite = true;
-                    }
-
-                    if (currentTime - lastPlayed > PURGE_TIME) {
-                        removedPlayers++;
-                    }
-                    else {
-                        if (rewrite) {
-                            // Rewrite their data with a valid time
-                            character[37] = Long.toString(lastPlayed);
-                            String newLine = org.apache.commons.lang.StringUtils.join(character, ":");
-                            writer.append(newLine).append("\r\n");
-                        }
-                        else {
-                            writer.append(line).append("\r\n");
-                        }
-                    }
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        mcMMO.p.getLogger().info("Purged " + removedPlayers + " users from the database.");
-    }
-
-    public boolean removeUser(@NotNull String playerName, @Nullable UUID uuid) {
-        //NOTE: UUID is unused for FlatFile for this interface implementation
-        boolean worked = false;
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    // Write out the same file but when we get to the player we want to remove, we skip his line.
-                    if (!worked && line.split(":")[FlatFileMappings.USERNAME].equalsIgnoreCase(playerName)) {
-                        mcMMO.p.getLogger().info("User found, removing...");
-                        worked = true;
-                        continue; // Skip the player
-                    }
-
-                    writer.append(line).append("\r\n");
-                }
-
-                out = new FileWriter(usersFilePath); // Write out the new file
-                out.write(writer.toString());
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        Misc.profileCleanup(playerName);
-
-        return worked;
-    }
-
-    @Override
-    public void removeCache(@NotNull UUID uuid) {
-        //Not used in FlatFile
-    }
-
-    public boolean saveUser(@NotNull MMODataSnapshot dataSnapshot) {
-        String playerName = dataSnapshot.getPlayerName();
-        UUID uuid = dataSnapshot.getPlayerUUID();
-
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                boolean wroteUser = false;
-                // While not at the end of the file
-                while ((line = in.readLine()) != null) {
-                    // Read the line in and copy it to the output if it's not the player we want to edit
-                    String[] character = line.split(":");
-                    if (!character[FlatFileMappings.UUID_INDEX].equalsIgnoreCase(uuid.toString()) && !character[FlatFileMappings.USERNAME].equalsIgnoreCase(playerName)) {
-                        writer.append(line).append("\r\n");
-                    }
-                    else {
-                        // Otherwise write the new player information
-                        writeUserToLine(dataSnapshot, playerName, uuid, writer);
-                        wroteUser = true;
-                    }
-                }
-
-                /*
-                 * If we couldn't find the user in the DB we need to add him
-                 */
-                if(!wroteUser)
-                {
-                    writeUserToLine(dataSnapshot, playerName, uuid, writer);
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-                return true;
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-                return false;
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    private void writeUserToLine(@NotNull MMODataSnapshot mmoDataSnapshot, @NotNull String playerName, @NotNull UUID uuid, @NotNull StringBuilder writer) {
-        ImmutableMap<RootSkill, Integer> primarySkillLevelMap = mmoDataSnapshot.getSkillLevelValues();
-        ImmutableMap<RootSkill, Float> primarySkillExperienceValueMap = mmoDataSnapshot.getSkillExperienceValues();
-
-        writer.append(playerName).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.MINING_CS)).append(":");
-        writer.append(":");
-        writer.append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.MINING_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.WOODCUTTING_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.WOODCUTTING_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.REPAIR_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.UNARMED_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.HERBALISM_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.EXCAVATION_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.ARCHERY_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.SWORDS_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.AXES_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.ACROBATICS_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.REPAIR_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.UNARMED_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.HERBALISM_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.EXCAVATION_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.ARCHERY_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.SWORDS_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.AXES_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.ACROBATICS_CS)).append(":");
-        writer.append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.TAMING_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.TAMING_CS)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.BERSERK)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.GIGA_DRILL_BREAKER)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.TREE_FELLER)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.GREEN_TERRA)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.SERRATED_STRIKES)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.SUPER_BREAKER)).append(":");
-        writer.append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.FISHING_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.FISHING_CS)).append(":");
-        writer.append((int) mmoDataSnapshot.getAbilityDATS(SuperAbilityType.BLAST_MINING)).append(":");
-        writer.append(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR).append(":");
-
-        MobHealthBarType mobHealthbarType = mmoDataSnapshot.getMobHealthBarType();
-        writer.append(mobHealthbarType.toString()).append(":");
-
-        writer.append(primarySkillLevelMap.get(CoreSkills.ALCHEMY_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.ALCHEMY_CS)).append(":");
-        writer.append(uuid != null ? uuid.toString() : "NULL").append(":");
-        writer.append(mmoDataSnapshot.getScoreboardTipsShown()).append(":");
-        writer.append(mmoDataSnapshot.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS)).append(":");
-
-        /*
-            public static int SKILLS_TRIDENTS = 44;
-            public static int EXP_TRIDENTS = 45;
-            public static int SKILLS_CROSSBOWS = 46;
-            public static int EXP_CROSSBOWS = 47;
-            public static int BARSTATE_ACROBATICS = 48;
-            public static int BARSTATE_ALCHEMY = 49;
-            public static int BARSTATE_ARCHERY = 50;
-            public static int BARSTATE_AXES = 51;
-            public static int BARSTATE_EXCAVATION = 52;
-            public static int BARSTATE_FISHING = 53;
-            public static int BARSTATE_HERBALISM = 54;
-            public static int BARSTATE_MINING = 55;
-            public static int BARSTATE_REPAIR = 56;
-            public static int BARSTATE_SALVAGE = 57;
-            public static int BARSTATE_SMELTING = 58;
-            public static int BARSTATE_SWORDS = 59;
-            public static int BARSTATE_TAMING = 60;
-            public static int BARSTATE_UNARMED = 61;
-            public static int BARSTATE_WOODCUTTING = 62;
-            public static int BARSTATE_TRIDENTS = 63;
-            public static int BARSTATE_CROSSBOWS = 64;
-         */
-
-        writer.append(primarySkillLevelMap.get(CoreSkills.TRIDENTS_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.TRIDENTS_CS)).append(":");
-        writer.append(primarySkillLevelMap.get(CoreSkills.CROSSBOWS_CS)).append(":");
-        writer.append(primarySkillExperienceValueMap.get(CoreSkills.CROSSBOWS_CS)).append(":");
-
-        //XPBar States
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.ACROBATICS_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.ALCHEMY_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.ARCHERY_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.AXES_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.EXCAVATION_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.FISHING_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.HERBALISM_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.MINING_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.REPAIR_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.SALVAGE_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.SMELTING_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.SWORDS_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.TAMING_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.UNARMED_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.WOODCUTTING_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.TRIDENTS_CS).toString()).append(":");
-        writer.append(mmoDataSnapshot.getBarStateMap().get(CoreSkills.CROSSBOWS_CS).toString()).append(":");
-
-        writer.append(0).append(":"); //archery super 1 cd
-        writer.append(0).append(":"); //xbow super 1 cd
-        writer.append(0).append(":"); //tridents super 1 cd
-        writer.append(0).append(":"); //chatspy toggle
-        writer.append(0).append(":"); //leaderboard ignored
-
-        writer.append("\r\n");
-    }
-
-    @Override
-    public @NotNull List<PlayerStat> readLeaderboard(@NotNull RootSkill skill, int pageNumber, int statsPerPage) {
-        updateLeaderboards();
-        List<PlayerStat> statsList = skill == null ? powerLevels : playerStatHash.get(skill);
-        int fromIndex = (Math.max(pageNumber, 1) - 1) * statsPerPage;
-
-        return statsList.subList(Math.min(fromIndex, statsList.size()), Math.min(fromIndex + statsPerPage, statsList.size()));
-    }
-
-    @Override
-    public @NotNull Map<RootSkill, Integer> readRank(@NotNull String playerName) {
-        updateLeaderboards();
-
-        Map<RootSkill, Integer> skills = new HashMap<>();
-
-        for (RootSkill rootSkill : CoreSkills.getImmutableCoreRootSkillSet()) {
-            if(CoreSkills.isChildSkill(rootSkill))
-                continue;
-
-            skills.put(rootSkill, getPlayerRank(playerName, playerStatHash.get(rootSkill)));
-        }
-
-        skills.put(null, getPlayerRank(playerName, powerLevels));
-
-        return skills;
-    }
-
-    @Override
-    public void insertNewUser(@NotNull String playerName, @NotNull UUID uuid) {
-        BufferedWriter out = null;
-        synchronized (fileWritingLock) {
-            try {
-                // Open the file to write the player
-                out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true));
-
-                String startingLevel = AdvancedConfig.getInstance().getStartingLevel() + ":";
-
-                // Add the player to the end
-                out.append(playerName).append(":");
-                out.append(startingLevel); // Mining
-                out.append(":");
-                out.append(":");
-                out.append("0:"); // Xp
-                out.append(startingLevel); // Woodcutting
-                out.append("0:"); // WoodCuttingXp
-                out.append(startingLevel); // Repair
-                out.append(startingLevel); // Unarmed
-                out.append(startingLevel); // Herbalism
-                out.append(startingLevel); // Excavation
-                out.append(startingLevel); // Archery
-                out.append(startingLevel); // Swords
-                out.append(startingLevel); // Axes
-                out.append(startingLevel); // Acrobatics
-                out.append("0:"); // RepairXp
-                out.append("0:"); // UnarmedXp
-                out.append("0:"); // HerbalismXp
-                out.append("0:"); // ExcavationXp
-                out.append("0:"); // ArcheryXp
-                out.append("0:"); // SwordsXp
-                out.append("0:"); // AxesXp
-                out.append("0:"); // AcrobaticsXp
-                out.append(":");
-                out.append(startingLevel); // Taming
-                out.append("0:"); // TamingXp
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append("0:"); // DATS
-                out.append(":");
-                out.append(startingLevel); // Fishing
-                out.append("0:"); // FishingXp
-                out.append("0:"); // Blast Mining
-                out.append(String.valueOf(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR)).append(":"); // LastLogin
-                out.append(Config.getInstance().getMobHealthbarDefault().toString()).append(":"); // Mob Healthbar HUD
-                out.append(startingLevel); // Alchemy
-                out.append("0:"); // AlchemyXp
-                out.append(uuid != null ? uuid.toString() : "NULL").append(":"); // UUID
-                out.append("0:"); // Scoreboard tips shown
-                out.append("0:"); // Chimaera Wing Dats
-
-                out.append("0:"); // Tridents Skill Level
-                out.append("0:"); // Tridents XP
-                out.append("0:"); // Crossbow Skill Level
-                out.append("0:"); // Crossbow XP Level
-
-                //Barstates for the 15 currently existing skills by ordinal value
-                out.append("NORMAL:"); // Acrobatics
-                out.append("NORMAL:"); // Alchemy
-                out.append("NORMAL:"); // Archery
-                out.append("NORMAL:"); // Axes
-                out.append("NORMAL:"); // Excavation
-                out.append("NORMAL:"); // Fishing
-                out.append("NORMAL:"); // Herbalism
-                out.append("NORMAL:"); // Mining
-                out.append("NORMAL:"); // Repair
-                out.append("DISABLED:"); // Salvage
-                out.append("DISABLED:"); // Smelting
-                out.append("NORMAL:"); // Swords
-                out.append("NORMAL:"); // Taming
-                out.append("NORMAL:"); // Unarmed
-                out.append("NORMAL:"); // Woodcutting
-                out.append("NORMAL:"); // Tridents
-                out.append("NORMAL:"); // Crossbows
-
-                //2.2.000+
-                out.append("0:"); // arch super 1
-                out.append("0:"); //xbow super 1
-                out.append("0:"); //tridents super 1
-                out.append("0:"); //chatspy toggle
-                out.append("0:"); //leaderboard ignored toggle
-
-
-                // Add more in the same format as the line above
-
-                out.newLine();
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    public @Nullable MMOPlayerData queryPlayerByName(@NotNull String playerName) throws ProfileRetrievalException {
-        BufferedReader bufferedReader = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        //Retrieve player
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                bufferedReader = new BufferedReader(new FileReader(usersFilePath));
-                String currentLine;
-
-                while ((currentLine = bufferedReader.readLine()) != null) {
-                    // Split the data which is stored as a string with : as break points
-                    String[] stringDataArray = currentLine.split(FLATFILE_SPLIT_CHARACTER_REGEX);
-
-                    //Search for matching name
-                    if (!stringDataArray[FlatFileMappings.USERNAME].equalsIgnoreCase(playerName)) {
-                        continue;
-                    }
-
-                    //We found our player, load the data
-                    return loadFromLine(stringDataArray);
-                }
-
-                throw new ProfileRetrievalException("Couldn't find a matching player in the database! Using name matching - " + playerName);
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-
-            //Cleanup resource leaks
-            finally {
-                if (bufferedReader != null) {
-                    try {
-                        bufferedReader.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        //Theoretically this statement should never be reached
-        mcMMO.p.getLogger().severe("Critical failure in execution of loading player from DB, contact the devs!");
-        return null;
-    }
-
-    public @Nullable MMOPlayerData queryPlayerDataByPlayer(@NotNull Player player) throws ProfileRetrievalException, NullArgumentException {
-        return queryPlayerDataByUUID(player.getUniqueId(), player.getName());
-    }
-
-    /**
-     * Queries by UUID will always have the current player name included as this method only gets executed when players join the server
-     * The name will be used to update player names in the DB if the name has changed
-     * There exists scenarios where players can share the same name in the DB, there is no code to account for this currently
-     * @param uuid uuid to match
-     * @param playerName used to overwrite playername values in the database if an existing value that is not equal to this one is found
-     * @return the player profile if retrieved successfully, otherwise null
-     * @throws ProfileRetrievalException
-     * @throws NullArgumentException
-     */
-    public @Nullable MMOPlayerData queryPlayerDataByUUID(@NotNull UUID uuid, @NotNull String playerName) throws ProfileRetrievalException, NullArgumentException {
-        BufferedReader bufferedReader = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        //Retrieve player
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                bufferedReader = new BufferedReader(new FileReader(usersFilePath));
-                String currentLine;
-
-                while ((currentLine = bufferedReader.readLine()) != null) {
-                    // Split the data which is stored as a string with : as break points
-                    String[] stringDataArray = currentLine.split(FLATFILE_SPLIT_CHARACTER_REGEX);
-
-                    //Search for matching UUID
-                    if (!stringDataArray[FlatFileMappings.UUID_INDEX].equalsIgnoreCase(uuid.toString())) {
-                        continue;
-                    }
-
-                    //If the player has changed their name, we need to update it too
-                    if (!stringDataArray[FlatFileMappings.USERNAME].equalsIgnoreCase(playerName)) {
-                        mcMMO.p.getLogger().info("Name change detected: " + stringDataArray[FlatFileMappings.USERNAME] + " => " + playerName);
-                        stringDataArray[FlatFileMappings.USERNAME] = playerName;
-                    }
-
-                    //We found our player, load the data
-                    return loadFromLine(stringDataArray);
-                }
-
-                throw new ProfileRetrievalException("Couldn't find a matching player in the database! - "+playerName+", "+uuid.toString());
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-
-            //Cleanup resource leaks
-            finally {
-                if (bufferedReader != null) {
-                    try {
-                        bufferedReader.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        //Theoretically this statement should never be reached
-        mcMMO.p.getLogger().severe("Critical failure in execution of loading player from DB, contact the devs!");
-        return null;
-    }
-
-    public void convertUsers(@NotNull DatabaseManager destination) {
-        BufferedReader in = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-        int convertedUsers = 0;
-        long startMillis = System.currentTimeMillis();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] stringDataSplit = line.split(":");
-
-                    try {
-                        MMOPlayerData mmoPlayerData = loadFromLine(stringDataSplit);
-                        if(mmoPlayerData == null)
-                            continue;
-
-                        destination.saveUser(mcMMO.getUserManager().createPlayerDataSnapshot(mmoPlayerData));
-                    }
-                    catch (Exception e) {
-                        e.printStackTrace();
-                    }
-                    convertedUsers++;
-                    Misc.printProgress(convertedUsers, progressInterval, startMillis);
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-
-    public @NotNull List<String> getStoredUsers() {
-        ArrayList<String> users = new ArrayList<>();
-        BufferedReader in = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                // Open the user file
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] character = line.split(":");
-                    users.add(character[FlatFileMappings.USERNAME]);
-                }
-            }
-            catch (Exception e) {
-                e.printStackTrace();
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-        return users;
-    }
-
-    /**
-     * Update the leader boards.
-     */
-    private void updateLeaderboards() {
-        // Only update FFS leaderboards every 10 minutes.. this puts a lot of strain on the server (depending on the size of the database) and should not be done frequently
-        if (System.currentTimeMillis() < lastUpdate + UPDATE_WAIT_TIME) {
-            return;
-        }
-
-        String usersFilePath = mcMMO.getUsersFilePath();
-        lastUpdate = System.currentTimeMillis(); // Log when the last update was run
-        powerLevels.clear(); // Clear old values from the power levels
-
-        // Initialize lists
-        List<PlayerStat> mining = new ArrayList<>();
-        List<PlayerStat> woodcutting = new ArrayList<>();
-        List<PlayerStat> herbalism = new ArrayList<>();
-        List<PlayerStat> excavation = new ArrayList<>();
-        List<PlayerStat> acrobatics = new ArrayList<>();
-        List<PlayerStat> repair = new ArrayList<>();
-        List<PlayerStat> swords = new ArrayList<>();
-        List<PlayerStat> axes = new ArrayList<>();
-        List<PlayerStat> archery = new ArrayList<>();
-        List<PlayerStat> unarmed = new ArrayList<>();
-        List<PlayerStat> taming = new ArrayList<>();
-        List<PlayerStat> fishing = new ArrayList<>();
-        List<PlayerStat> alchemy = new ArrayList<>();
-
-        BufferedReader in = null;
-        String playerName = null;
-        // Read from the FlatFile database and fill our arrays with information
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    String[] data = line.split(":");
-                    playerName = data[FlatFileMappings.USERNAME];
-                    int powerLevel = 0;
-
-                    Map<RootSkill, Integer> skills = getSkillMapFromLine(data);
-
-                    powerLevel += putStat(acrobatics, playerName, skills.get(CoreSkills.ACROBATICS_CS));
-                    powerLevel += putStat(alchemy, playerName, skills.get(CoreSkills.ALCHEMY_CS));
-                    powerLevel += putStat(archery, playerName, skills.get(CoreSkills.ARCHERY_CS));
-                    powerLevel += putStat(axes, playerName, skills.get(CoreSkills.AXES_CS));
-                    powerLevel += putStat(excavation, playerName, skills.get(CoreSkills.EXCAVATION_CS));
-                    powerLevel += putStat(fishing, playerName, skills.get(CoreSkills.FISHING_CS));
-                    powerLevel += putStat(herbalism, playerName, skills.get(CoreSkills.HERBALISM_CS));
-                    powerLevel += putStat(mining, playerName, skills.get(CoreSkills.MINING_CS));
-                    powerLevel += putStat(repair, playerName, skills.get(CoreSkills.REPAIR_CS));
-                    powerLevel += putStat(swords, playerName, skills.get(CoreSkills.SWORDS_CS));
-                    powerLevel += putStat(taming, playerName, skills.get(CoreSkills.TAMING_CS));
-                    powerLevel += putStat(unarmed, playerName, skills.get(CoreSkills.UNARMED_CS));
-                    powerLevel += putStat(woodcutting, playerName, skills.get(CoreSkills.WOODCUTTING_CS));
-                    powerLevel += putStat(woodcutting, playerName, skills.get(CoreSkills.CROSSBOWS_CS));
-                    powerLevel += putStat(woodcutting, playerName, skills.get(CoreSkills.TRIDENTS_CS));
-
-                    putStat(powerLevels, playerName, powerLevel);
-                }
-            }
-            catch (Exception e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " during user " + playerName + " (Are you sure you formatted it correctly?) " + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        SkillComparator c = new SkillComparator();
-
-        mining.sort(c);
-        woodcutting.sort(c);
-        repair.sort(c);
-        unarmed.sort(c);
-        herbalism.sort(c);
-        excavation.sort(c);
-        archery.sort(c);
-        swords.sort(c);
-        axes.sort(c);
-        acrobatics.sort(c);
-        taming.sort(c);
-        fishing.sort(c);
-        alchemy.sort(c);
-        powerLevels.sort(c);
-
-        playerStatHash.put(CoreSkills.MINING_CS, mining);
-        playerStatHash.put(CoreSkills.WOODCUTTING_CS, woodcutting);
-        playerStatHash.put(CoreSkills.REPAIR_CS, repair);
-        playerStatHash.put(CoreSkills.UNARMED_CS, unarmed);
-        playerStatHash.put(CoreSkills.HERBALISM_CS, herbalism);
-        playerStatHash.put(CoreSkills.EXCAVATION_CS, excavation);
-        playerStatHash.put(CoreSkills.ARCHERY_CS, archery);
-        playerStatHash.put(CoreSkills.SWORDS_CS, swords);
-        playerStatHash.put(CoreSkills.AXES_CS, axes);
-        playerStatHash.put(CoreSkills.ACROBATICS_CS, acrobatics);
-        playerStatHash.put(CoreSkills.TAMING_CS, taming);
-        playerStatHash.put(CoreSkills.FISHING_CS, fishing);
-        playerStatHash.put(CoreSkills.ALCHEMY_CS, alchemy);
-    }
-
-    /**
-     * Checks that the file is present and valid
-     */
-    private void checkStructure() {
-        if (usersFile.exists()) {
-            BufferedReader in = null;
-            FileWriter out = null;
-            String usersFilePath = mcMMO.getUsersFilePath();
-
-            synchronized (fileWritingLock) {
-                try {
-                    in = new BufferedReader(new FileReader(usersFilePath));
-                    StringBuilder writer = new StringBuilder();
-                    String line;
-                    HashSet<String> usernames = new HashSet<>();
-                    HashSet<String> players = new HashSet<>();
-
-                    while ((line = in.readLine()) != null) {
-                        // Remove empty lines from the file
-                        if (line.isEmpty()) {
-                            continue;
-                        }
-
-                        // Length checks depend on last stringDataArray being ':'
-                        if (line.charAt(line.length() - 1) != ':') {
-                            line = line.concat(":");
-                        }
-                        boolean updated = false;
-                        String[] stringDataArray = line.split(":");
-                        int originalLength = stringDataArray.length;
-
-                        // Prevent the same username from being present multiple times
-                        if (!usernames.add(stringDataArray[FlatFileMappings.USERNAME])) {
-                            stringDataArray[FlatFileMappings.USERNAME] = "_INVALID_OLD_USERNAME_'";
-                            updated = true;
-                            if (stringDataArray.length < FlatFileMappings.UUID_INDEX + 1 || stringDataArray[FlatFileMappings.UUID_INDEX].equals("NULL")) {
-                                continue;
-                            }
-                        }
-
-
-                        if (stringDataArray.length < 33) {
-                            // Before Version 1.0 - Drop
-                            mcMMO.p.getLogger().warning("Dropping malformed or before version 1.0 line from database - " + line);
-                            continue;
-                        }
-
-                        String oldVersion = null;
-
-                        if (stringDataArray.length > 33 && !stringDataArray[33].isEmpty()) {
-                            // Removal of Spout Support
-                            // Version 1.4.07-dev2
-                            // commit 7bac0e2ca5143bce84dc160617fed97f0b1cb968
-                            stringDataArray[33] = "";
-                            oldVersion = "1.4.07";
-                            updated = true;
-                        }
-
-                        if (stringDataArray.length <= 33) {
-                            // Introduction of HUDType
-                            // Version 1.1.06
-                            // commit 78f79213cdd7190cd11ae54526f3b4ea42078e8a
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "";
-                            oldVersion = "1.1.06";
-                            updated = true;
-                        }
-
-                        if (stringDataArray.length <= 35) {
-                            // Introduction of Fishing
-                            // Version 1.2.00
-                            // commit a814b57311bc7734661109f0e77fc8bab3a0bd29
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 2);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-                            stringDataArray[stringDataArray.length - 2] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.2.00";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 36) {
-                            // Introduction of Blast Mining cooldowns
-                            // Version 1.3.00-dev
-                            // commit fadbaf429d6b4764b8f1ad0efaa524a090e82ef5
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.3.00";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 37) {
-                            // Making old-purge work with flatfile
-                            // Version 1.4.00-dev
-                            // commmit 3f6c07ba6aaf44e388cc3b882cac3d8f51d0ac28
-                            // XXX Cannot create an OfflinePlayer at startup, use 0 and fix in purge
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.00";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 38) {
-                            // Addition of mob healthbars
-                            // Version 1.4.06
-                            // commit da29185b7dc7e0d992754bba555576d48fa08aa6
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = Config.getInstance().getMobHealthbarDefault().toString();
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.06";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 39) {
-                            // Addition of Alchemy
-                            // Version 1.4.08
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 2);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-                            stringDataArray[stringDataArray.length - 2] = "0";
-                            if (oldVersion == null) {
-                                oldVersion = "1.4.08";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 41) {
-                            // Addition of UUIDs
-                            // Version 1.5.01
-                            // Add a value because otherwise it gets removed
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "NULL";
-                            if (oldVersion == null) {
-                                oldVersion = "1.5.01";
-                            }
-                            updated = true;
-                        }
-                        if (stringDataArray.length <= 42) {
-                            // Addition of scoreboard tips auto disable
-                            // Version 1.5.02
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-
-                            if (oldVersion == null) {
-                                oldVersion = "1.5.02";
-                            }
-                            updated = true;
-                        }
-
-                        if(stringDataArray.length <= 43) {
-                            // Addition of Chimaera wing DATS
-                            stringDataArray = Arrays.copyOf(stringDataArray, stringDataArray.length + 1);
-                            stringDataArray[stringDataArray.length - 1] = "0";
-
-                            if (oldVersion == null) {
-                                oldVersion = "2.1.133";
-                            }
-                            updated = true;
-                        }
-
-                        if(stringDataArray.length <= FlatFileMappings.LENGTH_OF_SPLIT_DATA_ARRAY) {
-
-                            if (oldVersion == null) {
-                                oldVersion = "2.1.134";
-                            }
-
-                            stringDataArray = Arrays.copyOf(stringDataArray, FlatFileMappings.LENGTH_OF_SPLIT_DATA_ARRAY); // new array size
-
-                            /*
-                                public static int SKILLS_TRIDENTS = 44;
-                                public static int EXP_TRIDENTS = 45;
-                                public static int SKILLS_CROSSBOWS = 46;
-                                public static int EXP_CROSSBOWS = 47;
-                                public static int BARSTATE_ACROBATICS = 48;
-                                public static int BARSTATE_ALCHEMY = 49;
-                                public static int BARSTATE_ARCHERY = 50;
-                                public static int BARSTATE_AXES = 51;
-                                public static int BARSTATE_EXCAVATION = 52;
-                                public static int BARSTATE_FISHING = 53;
-                                public static int BARSTATE_HERBALISM = 54;
-                                public static int BARSTATE_MINING = 55;
-                                public static int BARSTATE_REPAIR = 56;
-                                public static int BARSTATE_SALVAGE = 57;
-                                public static int BARSTATE_SMELTING = 58;
-                                public static int BARSTATE_SWORDS = 59;
-                                public static int BARSTATE_TAMING = 60;
-                                public static int BARSTATE_UNARMED = 61;
-                                public static int BARSTATE_WOODCUTTING = 62;
-                                public static int BARSTATE_TRIDENTS = 63;
-                                public static int BARSTATE_CROSSBOWS = 64;
-                             */
-
-                            stringDataArray[FlatFileMappings.SKILLS_TRIDENTS] = "0"; //trident skill lvl
-                            stringDataArray[FlatFileMappings.EXP_TRIDENTS] = "0"; //trident xp value
-                            stringDataArray[FlatFileMappings.SKILLS_CROSSBOWS] = "0"; //xbow skill lvl
-                            stringDataArray[FlatFileMappings.EXP_CROSSBOWS] = "0"; //xbow xp lvl
-
-                            //Barstates 48-64
-                            stringDataArray[FlatFileMappings.BARSTATE_ACROBATICS] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_ALCHEMY] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_ARCHERY] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_AXES] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_EXCAVATION] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_FISHING] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_HERBALISM] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_MINING] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_REPAIR] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_SALVAGE] = "DISABLED"; //Child skills
-                            stringDataArray[FlatFileMappings.BARSTATE_SMELTING] = "DISABLED"; //Child skills
-                            stringDataArray[FlatFileMappings.BARSTATE_SWORDS] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_TAMING] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_UNARMED] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_WOODCUTTING] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_TRIDENTS] = "NORMAL";
-                            stringDataArray[FlatFileMappings.BARSTATE_CROSSBOWS] = "NORMAL";
-
-                            stringDataArray[FlatFileMappings.COOLDOWN_ARCHERY_SUPER_1] = "0";
-                            stringDataArray[FlatFileMappings.COOLDOWN_CROSSBOWS_SUPER_1] = "0";
-                            stringDataArray[FlatFileMappings.COOLDOWN_TRIDENTS_SUPER_1] = "0";
-
-                            stringDataArray[FlatFileMappings.CHATSPY_TOGGLE] = "0";
-                            stringDataArray[FlatFileMappings.LEADERBOARD_IGNORED] = "0";
-
-                            //This part is a bit odd because lastlogin already has a place in the index but it was unused
-                            stringDataArray[FlatFileMappings.LAST_LOGIN] = "0";
-
-                            updated = true;
-                        }
-
-                        //TODO: If new skills are added this needs to be rewritten
-                        if (Config.getInstance().getTruncateSkills()) {
-                            for (RootSkill rootSkill : CoreSkills.getImmutableCoreRootSkillSet()) {
-                                if(CoreSkills.isChildSkill(rootSkill))
-                                    continue;
-
-                                int index = getSkillIndex(rootSkill);
-                                if (index >= stringDataArray.length) {
-                                    continue;
-                                }
-                                int cap = Config.getInstance().getLevelCap(rootSkill);
-                                if (Integer.parseInt(stringDataArray[index]) > cap) {
-                                    mcMMO.p.getLogger().warning("Truncating " + rootSkill.getRawSkillName() + " to configured max level for player " + stringDataArray[FlatFileMappings.USERNAME]);
-                                    stringDataArray[index] = cap + "";
-                                    updated = true;
-                                }
-                            }
-                        }
-
-                        boolean corrupted = false;
-
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of date
-                        //TODO: Update this corruption code, its super out of dated
-
-                        for (int i = 0; i < stringDataArray.length; i++) {
-                            //Sigh... this code
-                            if (stringDataArray[i].isEmpty() && !(i == 2 || i == 3 || i == 23 || i == 33 || i == 41)) {
-                                mcMMO.p.getLogger().info("Player data at index "+i+" appears to be empty, possible corruption of data has occurred.");
-                                corrupted = true;
-                                if (i == 37) {
-                                    stringDataArray[i] = String.valueOf(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR);
-                                }
-                                else if (i == 38) {
-                                    stringDataArray[i] = Config.getInstance().getMobHealthbarDefault().toString();
-                                }
-                                else {
-                                    stringDataArray[i] = "0";
-                                }
-                            }
-
-                            if (StringUtils.isInt(stringDataArray[i]) && i == 38) {
-                                corrupted = true;
-                                stringDataArray[i] = Config.getInstance().getMobHealthbarDefault().toString();
-                            }
-
-                            if (!StringUtils.isInt(stringDataArray[i]) && !(i == 0 || i == 2 || i == 3 || i == 23 || i == 33 || i == 38 || i == 41)) {
-                                corrupted = true;
-                                stringDataArray[i] = "0";
-                            }
-                        }
-
-                        if (corrupted) {
-                            mcMMO.p.getLogger().info("Updating corrupted database line for player " + stringDataArray[FlatFileMappings.USERNAME]);
-                        }
-
-                        if (oldVersion != null) {
-                            mcMMO.p.getLogger().info("Updating database line from before version " + oldVersion + " for player " + stringDataArray[FlatFileMappings.USERNAME]);
-                        }
-
-                        updated |= corrupted;
-                        updated |= oldVersion != null;
-
-                        if (Config.getInstance().getTruncateSkills()) {
-                            Map<RootSkill, Integer> skillsMap = getSkillMapFromLine(stringDataArray);
-                            for (RootSkill rootSkill : CoreSkills.getNonChildSkills()) {
-                                int cap = Config.getInstance().getLevelCap(rootSkill);
-                                if (skillsMap.get(rootSkill) > cap) {
-                                    updated = true;
-                                }
-                            }
-                        }
-
-                        if (updated) {
-                            line = org.apache.commons.lang.StringUtils.join(stringDataArray, ":") + ":";
-                        }
-
-                        // Prevent the same player from being present multiple times
-                        if (stringDataArray.length == originalLength //If the length changed then the schema was expanded
-                                && (!stringDataArray[FlatFileMappings.UUID_INDEX].isEmpty()
-                                && !stringDataArray[FlatFileMappings.UUID_INDEX].equals("NULL")
-                                && !players.add(stringDataArray[FlatFileMappings.UUID_INDEX]))) {
-                            continue;
-                        }
-
-                        writer.append(line).append("\r\n");
-                    }
-
-                    // Write the new file
-                    out = new FileWriter(usersFilePath);
-                    out.write(writer.toString());
-                }
-                catch (IOException e) {
-                    mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-                }
-                finally {
-                    if (in != null) {
-                        try {
-                            in.close();
-                        }
-                        catch (IOException e) {
-                            // Ignore
-                        }
-                    }
-                    if (out != null) {
-                        try {
-                            out.close();
-                        }
-                        catch (IOException e) {
-                            // Ignore
-                        }
-                    }
-                }
-            }
-
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SQL_INDEXES);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
-//            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SQL_PARTY_NAMES);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SPOUT);
-            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
-            return;
-        }
-
-        usersFile.getParentFile().mkdir();
-
-        try {
-            mcMMO.p.getLogger().info("Creating mcmmo.users file...");
-            new File(mcMMO.getUsersFilePath()).createNewFile();
-        }
-        catch (IOException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private Integer getPlayerRank(String playerName, List<PlayerStat> statsList) {
-        if (statsList == null) {
-            return null;
-        }
-
-        int currentPos = 1;
-
-        for (PlayerStat stat : statsList) {
-            if (stat.name.equalsIgnoreCase(playerName)) {
-                return currentPos;
-            }
-
-            currentPos++;
-        }
-
-        return null;
-    }
-
-    private int putStat(List<PlayerStat> statList, String playerName, int statValue) {
-        statList.add(new PlayerStat(playerName, statValue));
-        return statValue;
-    }
-
-    private static class SkillComparator implements Comparator<PlayerStat> {
-        @Override
-        public int compare(PlayerStat o1, PlayerStat o2) {
-            return (o2.statVal - o1.statVal);
-        }
-    }
-
-    private @Nullable MMOPlayerData loadFromLine(@NotNull String[] dataStrSplit) {
-        MMODataBuilder playerDataBuilder = new MMODataBuilder();
-
-        Map<RootSkill, Integer> skillLevelMap = getSkillMapFromLine(dataStrSplit);      // Skill levels
-        Map<RootSkill, Float> skillExperienceValueMap   = new HashMap<>();     // Skill & XP
-        Map<SuperSkill, Integer> skillAbilityDeactivationTimeStamp = new HashMap<>(); // Ability & Cooldown
-        Map<UniqueDataType, Integer> uniquePlayerDataMap = new EnumMap<UniqueDataType, Integer>(UniqueDataType.class);
-        Map<RootSkill, SkillBossBarState> xpBarStateMap = new HashMap<>();
-        int scoreboardTipsShown;
-
-        skillExperienceValueMap.put(CoreSkills.TAMING_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_TAMING]));
-        skillExperienceValueMap.put(CoreSkills.MINING_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_MINING]));
-        skillExperienceValueMap.put(CoreSkills.REPAIR_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_REPAIR]));
-        skillExperienceValueMap.put(CoreSkills.WOODCUTTING_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_WOODCUTTING]));
-        skillExperienceValueMap.put(CoreSkills.UNARMED_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_UNARMED]));
-        skillExperienceValueMap.put(CoreSkills.HERBALISM_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_HERBALISM]));
-        skillExperienceValueMap.put(CoreSkills.EXCAVATION_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_EXCAVATION]));
-        skillExperienceValueMap.put(CoreSkills.ARCHERY_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_ARCHERY]));
-        skillExperienceValueMap.put(CoreSkills.SWORDS_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_SWORDS]));
-        skillExperienceValueMap.put(CoreSkills.AXES_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_AXES]));
-        skillExperienceValueMap.put(CoreSkills.ACROBATICS_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_ACROBATICS]));
-        skillExperienceValueMap.put(CoreSkills.FISHING_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_FISHING]));
-        skillExperienceValueMap.put(CoreSkills.ALCHEMY_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_ALCHEMY]));
-        skillExperienceValueMap.put(CoreSkills.TRIDENTS_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_TRIDENTS]));
-        skillExperienceValueMap.put(CoreSkills.CROSSBOWS_CS, (float) Integer.parseInt(dataStrSplit[FlatFileMappings.EXP_CROSSBOWS]));
-
-        //Set Skill XP
-
-        // Taming - Unused
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.SUPER_BREAKER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_SUPER_BREAKER]));
-        // Repair - Unused
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.TREE_FELLER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_TREE_FELLER]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.BERSERK, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_BERSERK]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.GREEN_TERRA, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_GREEN_TERRA]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.GIGA_DRILL_BREAKER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_GIGA_DRILL_BREAKER]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.SERRATED_STRIKES, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_SERRATED_STRIKES]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.SKULL_SPLITTER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_SKULL_SPLITTER]));
-        // Acrobatics - Unused
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.BLAST_MINING, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_BLAST_MINING]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.ARCHERY_SUPER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_ARCHERY_SUPER_1]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.SUPER_SHOTGUN, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_CROSSBOWS_SUPER_1]));
-        skillAbilityDeactivationTimeStamp.put(SuperAbilityType.TRIDENT_SUPER, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_TRIDENTS_SUPER_1]));
-
-
-//        try {
-//            mobHealthbarType = MobHealthBarType.valueOf(dataStrSplit[FlatFileMappings.HEALTHBAR]);
-//        }
-//        catch (Exception e) {
-//            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
-//        }
-
-
-        //Sometimes players are retrieved by name
-        UUID playerUUID;
-
-        try {
-            playerUUID = UUID.fromString(dataStrSplit[FlatFileMappings.UUID_INDEX]);
-        }
-        catch (Exception e) {
-            mcMMO.p.getLogger().severe("UUID not found for data entry, skipping entry");
-            return null;
-        }
-
-        try {
-            scoreboardTipsShown = Integer.parseInt(dataStrSplit[FlatFileMappings.SCOREBOARD_TIPS]);
-        }
-        catch (Exception e) {
-            scoreboardTipsShown = 0;
-        }
-
-
-        try {
-            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, Integer.valueOf(dataStrSplit[FlatFileMappings.COOLDOWN_CHIMAERA_WING]));
-        }
-        catch (Exception e) {
-            uniquePlayerDataMap.put(UniqueDataType.CHIMAERA_WING_DATS, 0);
-        }
-
-        try {
-            xpBarStateMap.put(CoreSkills.ACROBATICS_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_ACROBATICS]));
-            xpBarStateMap.put(CoreSkills.ALCHEMY_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_ALCHEMY]));
-            xpBarStateMap.put(CoreSkills.ARCHERY_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_ARCHERY]));
-            xpBarStateMap.put(CoreSkills.AXES_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_AXES]));
-            xpBarStateMap.put(CoreSkills.EXCAVATION_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_EXCAVATION]));
-            xpBarStateMap.put(CoreSkills.FISHING_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_FISHING]));
-            xpBarStateMap.put(CoreSkills.HERBALISM_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_HERBALISM]));
-            xpBarStateMap.put(CoreSkills.MINING_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_MINING]));
-            xpBarStateMap.put(CoreSkills.REPAIR_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_REPAIR]));
-            xpBarStateMap.put(CoreSkills.SALVAGE_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_SALVAGE]));
-            xpBarStateMap.put(CoreSkills.SMELTING_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_SMELTING]));
-            xpBarStateMap.put(CoreSkills.SWORDS_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_SWORDS]));
-            xpBarStateMap.put(CoreSkills.TAMING_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_TAMING]));
-            xpBarStateMap.put(CoreSkills.UNARMED_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_UNARMED]));
-            xpBarStateMap.put(CoreSkills.WOODCUTTING_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_WOODCUTTING]));
-            xpBarStateMap.put(CoreSkills.TRIDENTS_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_TRIDENTS]));
-            xpBarStateMap.put(CoreSkills.CROSSBOWS_CS, SkillUtils.asBarState(dataStrSplit[FlatFileMappings.BARSTATE_CROSSBOWS]));
-
-        } catch (Exception e) {
-            xpBarStateMap = MMOExperienceBarManager.generateDefaultBarStateMap();
-        }
-        MMOPlayerData mmoPlayerData;
-
-        try {
-            //Set Player Data
-            playerDataBuilder.setSkillLevelValues(skillLevelMap)
-                    .setSkillExperienceValues(skillExperienceValueMap)
-                    .setAbilityDeactivationTimestamps(skillAbilityDeactivationTimeStamp)
-//                    .setMobHealthBarType(mobHealthbarType)
-                    .setPlayerUUID(playerUUID)
-                    .setScoreboardTipsShown(scoreboardTipsShown)
-                    .setUniquePlayerData(uniquePlayerDataMap)
-                    .setBarStateMap(xpBarStateMap);
-
-            //Build Data
-            return playerDataBuilder.build();
-        } catch (Exception e) {
-            mcMMO.p.getLogger().severe("Critical failure when trying to construct persistent player data!");
-            e.printStackTrace();
-            return null;
-        }
-    }
-
-    //TODO: Add tests
-    private @NotNull Map<RootSkill, Integer> getSkillMapFromLine(@NotNull String[] stringDataArray) {
-        HashMap<RootSkill, Integer> skillLevelsMap = new HashMap<>();   // Skill & Level
-
-        skillLevelsMap.put(CoreSkills.TAMING_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_TAMING]));
-        skillLevelsMap.put(CoreSkills.MINING_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_MINING]));
-        skillLevelsMap.put(CoreSkills.REPAIR_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_REPAIR]));
-        skillLevelsMap.put(CoreSkills.WOODCUTTING_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_WOODCUTTING]));
-        skillLevelsMap.put(CoreSkills.UNARMED_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_UNARMED]));
-        skillLevelsMap.put(CoreSkills.HERBALISM_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_HERBALISM]));
-        skillLevelsMap.put(CoreSkills.EXCAVATION_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_EXCAVATION]));
-        skillLevelsMap.put(CoreSkills.ARCHERY_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_ARCHERY]));
-        skillLevelsMap.put(CoreSkills.SWORDS_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_SWORDS]));
-        skillLevelsMap.put(CoreSkills.AXES_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_AXES]));
-        skillLevelsMap.put(CoreSkills.ACROBATICS_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_ACROBATICS]));
-        skillLevelsMap.put(CoreSkills.FISHING_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_FISHING]));
-        skillLevelsMap.put(CoreSkills.ALCHEMY_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_ALCHEMY]));
-        skillLevelsMap.put(CoreSkills.TRIDENTS_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_TRIDENTS]));
-        skillLevelsMap.put(CoreSkills.CROSSBOWS_CS, Integer.valueOf(stringDataArray[FlatFileMappings.SKILLS_CROSSBOWS]));
-
-        return skillLevelsMap;
-    }
-
-    public @NotNull DatabaseType getDatabaseType() {
-        return DatabaseType.FLATFILE;
-    }
-
-    private int getSkillIndex(@NotNull RootSkill rootSkill) {
-        PrimarySkillType primarySkillType = CoreSkills.getSkill(rootSkill);
-
-        switch (primarySkillType) {
-            case ACROBATICS:
-                return FlatFileMappings.SKILLS_ACROBATICS;
-            case ALCHEMY:
-                return FlatFileMappings.SKILLS_ALCHEMY;
-            case ARCHERY:
-                return FlatFileMappings.SKILLS_ARCHERY;
-            case AXES:
-                return FlatFileMappings.SKILLS_AXES;
-            case EXCAVATION:
-                return FlatFileMappings.SKILLS_EXCAVATION;
-            case FISHING:
-                return FlatFileMappings.SKILLS_FISHING;
-            case HERBALISM:
-                return FlatFileMappings.SKILLS_HERBALISM;
-            case MINING:
-                return FlatFileMappings.SKILLS_MINING;
-            case REPAIR:
-                return FlatFileMappings.SKILLS_REPAIR;
-            case SWORDS:
-                return FlatFileMappings.SKILLS_SWORDS;
-            case TAMING:
-                return FlatFileMappings.SKILLS_TAMING;
-            case UNARMED:
-                return FlatFileMappings.SKILLS_UNARMED;
-            case WOODCUTTING:
-                return FlatFileMappings.SKILLS_WOODCUTTING;
-            case TRIDENTS:
-                return FlatFileMappings.SKILLS_TRIDENTS;
-            case CROSSBOWS:
-                return FlatFileMappings.SKILLS_CROSSBOWS;
-            default:
-                throw new RuntimeException("Primary Skills only");
-
-        }
-    }
-
-    @Override
-    public void onDisable() { }
-
-    public void resetMobHealthSettings() {
-        BufferedReader in = null;
-        FileWriter out = null;
-        String usersFilePath = mcMMO.getUsersFilePath();
-
-        synchronized (fileWritingLock) {
-            try {
-                in = new BufferedReader(new FileReader(usersFilePath));
-                StringBuilder writer = new StringBuilder();
-                String line;
-
-                while ((line = in.readLine()) != null) {
-                    // Remove empty lines from the file
-                    if (line.isEmpty()) {
-                        continue;
-                    }
-                    String[] character = line.split(":");
-                    
-                    character[FlatFileMappings.HEALTHBAR] = Config.getInstance().getMobHealthbarDefault().toString();
-                    
-                    line = org.apache.commons.lang.StringUtils.join(character, ":") + ":";
-
-                    writer.append(line).append("\r\n");
-                }
-
-                // Write the new file
-                out = new FileWriter(usersFilePath);
-                out.write(writer.toString());
-            }
-            catch (IOException e) {
-                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
-            }
-            finally {
-                if (in != null) {
-                    try {
-                        in.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-                if (out != null) {
-                    try {
-                        out.close();
-                    }
-                    catch (IOException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-    }
-}

+ 139 - 21
src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java

@@ -33,6 +33,9 @@ import java.util.concurrent.locks.ReentrantLock;
 
 public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private static final String ALL_QUERY_VERSION = "total";
+    public static final String MOBHEALTHBAR_VARCHAR = "VARCHAR(50)";
+    public static final String UUID_VARCHAR = "VARCHAR(36)";
+    public static final String USER_VARCHAR = "VARCHAR(40)";
     private final String tablePrefix = Config.getInstance().getMySQLTablePrefix();
 
     private final Map<UUID, Integer> cachedUserIDs = new HashMap<>();
@@ -45,6 +48,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
 
     private final ReentrantLock massUpdateLock = new ReentrantLock();
 
+    private final String CHARSET_SQL = "utf8mb4"; //This is compliant with UTF-8 while "utf8" is not, confusing but this is how it is.
+
     protected SQLDatabaseManager() {
         String connectionString = "jdbc:mysql://" + Config.getInstance().getMySQLServerName()
                 + ":" + Config.getInstance().getMySQLServerPort() + "/" + Config.getInstance().getMySQLDatabaseName();
@@ -565,6 +570,24 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
         }
     }
 
+    @Override
+    public @NotNull PlayerProfile newUser(@NotNull Player player) {
+        try {
+            Connection connection = getConnection(PoolIdentifier.SAVE);
+            int id = newUser(connection, player.getName(), player.getUniqueId());
+
+            if (id == -1) {
+                return new PlayerProfile(player.getName(), player.getUniqueId(), false);
+            } else {
+                return loadPlayerProfile(player.getUniqueId(), player.getName());
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+
+        return new PlayerProfile(player.getName(), player.getUniqueId(), false);
+    }
+
     private int newUser(Connection connection, String playerName, UUID uuid) {
         ResultSet resultSet = null;
         PreparedStatement statement = null;
@@ -603,11 +626,26 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
         return -1;
     }
 
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull String playerName) {
+        try {
+            return loadPlayerFromDB(null, playerName);
+        } catch (RuntimeException e) {
+            e.printStackTrace();
+            return new PlayerProfile(playerName, false);
+        }
+    }
+
+    public @NotNull PlayerProfile loadPlayerProfile(@NotNull UUID uuid, @Nullable String playerName) {
+        return loadPlayerFromDB(uuid, playerName);
     @Override
     public @Nullable MMOPlayerData queryPlayerDataByPlayer(@NotNull Player player) throws ProfileRetrievalException, NullArgumentException {
         return loadPlayerProfile(player, player.getName(), player.getUniqueId());
     }
 
+    private PlayerProfile loadPlayerFromDB(@Nullable UUID uuid, @Nullable String playerName) throws RuntimeException {
+        if(uuid == null && playerName == null) {
+            throw new RuntimeException("Error looking up player, both UUID and playerName are null and one must not be.");
+        }
     @Override
     public @Nullable MMOPlayerData queryPlayerDataByUUID(@NotNull UUID uuid, @NotNull String playerName) throws ProfileRetrievalException, NullArgumentException {
         return loadPlayerProfile(null, playerName, uuid);
@@ -623,15 +661,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
             int id = getUserID(connection, playerName, playerUUID);
 
             if (id == -1) {
-                // There is no such user
-                if (player != null) {
-                    id = newUser(connection, playerName, playerUUID);
-                    if (id == -1) {
-                        return null;
-                    }
-                } else {
-                    return null;
-                }
+            // There is no such user
+                return new PlayerProfile(playerName, false);
             }
             // There is such a user
             writeMissingRows(connection, id);
@@ -648,7 +679,10 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                     resultSet.close();
                     statement.close();
 
-                    if (!playerName.isEmpty() && !playerName.equalsIgnoreCase(name) && playerUUID != null) {
+                    if (playerName != null
+                            && !playerName.isEmpty()
+                            && !playerName.equalsIgnoreCase(name)
+                            && playerUUID != null) {
                         statement = connection.prepareStatement(
                                 "UPDATE `" + tablePrefix + "users` "
                                         + "SET user = ? "
@@ -685,10 +719,11 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
             tryClose(connection);
         }
 
-        return null;
+        //Return empty profile
+        return new PlayerProfile(playerName, false);
     }
 
-    public void convertUsers(@NotNull DatabaseManager destination) {
+    public void convertUsers(DatabaseManager destination) {
         PreparedStatement statement = null;
         Connection connection = null;
         ResultSet resultSet = null;
@@ -867,7 +902,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                     + "`lastlogin` int(32) unsigned NOT NULL,"
                     + "PRIMARY KEY (`id`),"
                     + "INDEX(`user`(20) ASC),"
-                    + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;");
+                    + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=" + CHARSET_SQL + " AUTO_INCREMENT=1;");
                 tryClose(createStatement);
             }
             tryClose(resultSet);
@@ -881,7 +916,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                         + "`mobhealthbar` varchar(50) NOT NULL DEFAULT '" + Config.getInstance().getMobHealthbarDefault() + "',"
                         + "`scoreboardtips` int(10) NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
             }
             tryClose(resultSet);
@@ -908,7 +943,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                         + "`tridents` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "`crossbows` int(32) unsigned NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
             }
             tryClose(resultSet);
@@ -936,7 +971,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                         + "`crossbows` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "`total` int(10) unsigned NOT NULL DEFAULT '0',"
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
             }
             tryClose(resultSet);
@@ -1010,7 +1045,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                         + "`view_tridents` varchar(40) NOT NULL DEFAULT 'NORMAL',"
                         + "`view_crossbows` varchar(40) NOT NULL DEFAULT 'NORMAL',"
                         + "PRIMARY KEY (`user_id`)) "
-                        + "DEFAULT CHARSET=latin1;");
+                        + "DEFAULT CHARSET=" + CHARSET_SQL + ";");
                 tryClose(createStatement);
             }
             tryClose(resultSet);
@@ -1135,6 +1170,11 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                 case ADD_UNIQUE_PLAYER_DATA:
                     checkUpgradeAddUniqueChimaeraWing(statement);
                     break;
+
+                case SQL_CHARSET_UTF8MB4:
+                    updateCharacterSet(statement);
+                    break;
+
                 case ADD_SQL_2_2:
                     checkUpgradeAddTridentsAndCrossbowsSQL(statement);
                     break;
@@ -1142,8 +1182,6 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                     break;
 
             }
-
-            mcMMO.getUpgradeManager().setUpgradeCompleted(upgrade);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1358,6 +1396,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
             statement.execute("ALTER TABLE `" + tablePrefix + "users` " 
                     + "DROP INDEX `user`,"
                     + "ADD INDEX `user` (`user`(20) ASC)");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_NAME_UNIQUENESS);
         } catch (SQLException ex) {
             ex.printStackTrace();
         } finally {
@@ -1385,6 +1424,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddAlchemy(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `alchemy` FROM `" + tablePrefix + "skills` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy...");
@@ -1396,6 +1436,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddBlastMiningCooldown(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `blast_mining` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining...");
@@ -1406,6 +1447,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddUniqueChimaeraWing(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `chimaera_wing` FROM `" + tablePrefix + "cooldowns` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UNIQUE_PLAYER_DATA);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Chimaera Wing...");
@@ -1416,6 +1458,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddFishing(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `fishing` FROM `" + tablePrefix + "skills` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing...");
@@ -1427,6 +1470,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddMobHealthbars(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `mobhealthbar` FROM `" + tablePrefix + "huds` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars...");
@@ -1437,6 +1481,7 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
     private void checkUpgradeAddScoreboardTips(final Statement statement) throws SQLException {
         try {
             statement.executeQuery("SELECT `scoreboardtips` FROM `" + tablePrefix + "huds` LIMIT 1");
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SCOREBOARD_TIPS);
         }
         catch (SQLException ex) {
             mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for scoreboard tips...");
@@ -1465,6 +1510,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                     }
                 }
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SQL_INDEXES);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1494,7 +1541,11 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                 mcMMO.p.getLogger().info("Adding UUIDs to mcMMO MySQL user table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD `uuid` varchar(36) NULL DEFAULT NULL");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD UNIQUE INDEX `uuid` (`uuid`) USING BTREE");
+
+                new GetUUIDUpdatesRequired().runTaskLaterAsynchronously(mcMMO.p, 100); // wait until after first purge
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UUIDS);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1502,8 +1553,6 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
         finally {
             tryClose(resultSet);
         }
-
-        new GetUUIDUpdatesRequired().runTaskLaterAsynchronously(mcMMO.p, 100); // wait until after first purge
     }
 
     private class GetUUIDUpdatesRequired extends BukkitRunnable {
@@ -1561,6 +1610,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                 mcMMO.p.getLogger().info("Removing party name from users table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`");
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SQL_PARTY_NAMES);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1596,6 +1647,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_total` (`total`) USING BTREE");
                 connection.commit();
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SKILL_TOTAL);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1627,6 +1680,8 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
                 mcMMO.p.getLogger().info("Removing Spout HUD type from huds table...");
                 statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`");
             }
+
+            mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.DROP_SPOUT);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -1740,6 +1795,69 @@ public final class SQLDatabaseManager extends AbstractDatabaseManager {
         }
     }
 
+    private void updateCharacterSet(@NotNull Statement statement) {
+        //TODO: Could check the tables for being latin1 before executing queries but it seems moot because it is likely the same computational effort
+        /*
+            The following columns were set to use latin1 historically (now utf8mb4)
+            column user in <tablePrefix>users
+            column uuid in <tablePrefix>users
+
+            column mobhealthbar in <tablePrefix>huds
+         */
+
+        //Alter users table
+        mcMMO.p.getLogger().info("SQL Converting tables from latin1 to utf8mb4");
+
+        //Update "user" column
+        try {
+        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        statement.executeUpdate(getUpdateUserInUsersTableSQLQuery());
+
+        //Update "uuid" column
+        mcMMO.p.getLogger().info("Updating user column to new encoding");
+        statement.executeUpdate(getUpdateUUIDInUsersTableSQLQuery());
+
+        //Update "mobhealthbar" column
+        mcMMO.p.getLogger().info("Updating mobhealthbar column to new encoding");
+        statement.executeUpdate(getUpdateMobHealthBarInHudsTableSQLQuery());
+
+        mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.SQL_CHARSET_UTF8MB4);
+
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @NotNull
+    private String getUpdateUserInUsersTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "users\n" +
+                "    CHANGE user user\n" +
+                "    " + USER_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
+
+    @NotNull
+    private String getUpdateUUIDInUsersTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "users\n" +
+                "    CHANGE uuid uuid\n" +
+                "    " + UUID_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
+
+    @NotNull
+    private String getUpdateMobHealthBarInHudsTableSQLQuery() {
+        return "ALTER TABLE\n" +
+                "    " + tablePrefix + "huds\n" +
+                "    CHANGE mobhealthbar mobhealthbar\n" +
+                "    " + MOBHEALTHBAR_VARCHAR + "\n" +
+                "    CHARACTER SET utf8mb4\n" +
+                "    COLLATE utf8mb4_unicode_ci;";
+    }
+
     @Override
     public void removeCache(@NotNull UUID uuid) {
         cachedUserIDs.remove(uuid);

+ 2 - 1
src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java

@@ -17,5 +17,6 @@ public enum UpgradeType {
     ADD_SQL_2_2,
     FIX_SPELLING_NETHERITE_SALVAGE,
     FIX_SPELLING_NETHERITE_REPAIR,
-    FIX_NETHERITE_SALVAGE_QUANTITIES
+    FIX_NETHERITE_SALVAGE_QUANTITIES,
+    SQL_CHARSET_UTF8MB4
 }

+ 41 - 0
src/main/java/com/gmail/nossr50/events/McMMOReplaceVanillaTreasureEvent.java

@@ -0,0 +1,41 @@
+package com.gmail.nossr50.events;
+
+import org.bukkit.entity.Item;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+public class McMMOReplaceVanillaTreasureEvent extends Event {
+    private @NotNull ItemStack replacementItemStack;
+    private final @NotNull Item originalItem;
+
+    public McMMOReplaceVanillaTreasureEvent(@NotNull Item originalItem, @NotNull ItemStack replacementItemStack) {
+        this.originalItem = originalItem;
+        this.replacementItemStack = replacementItemStack;
+    }
+
+    /** Rest of file is required boilerplate for custom events **/
+    private static final @NotNull HandlerList handlers = new HandlerList();
+
+    @Override
+    public @NotNull HandlerList getHandlers() {
+        return handlers;
+    }
+
+    public static @NotNull HandlerList getHandlerList() {
+        return handlers;
+    }
+
+    public @NotNull ItemStack getReplacementItemStack() {
+        return replacementItemStack;
+    }
+
+    public void setReplacementItemStack(@NotNull ItemStack replacementItemStack) {
+        this.replacementItemStack = replacementItemStack;
+    }
+
+    public @NotNull Item getOriginalItem() {
+        return originalItem;
+    }
+}

+ 1 - 0
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -616,6 +616,7 @@ public class EntityListener implements Listener {
                 switch (cause) {
                     case CONTACT:
                     case FIRE:
+                    case HOT_FLOOR:
                     case LAVA:
                         if (tamingManager.canUseEnvironmentallyAware()) {
                             tamingManager.processEnvironmentallyAware(wolf, event.getDamage());

+ 14 - 3
src/main/java/com/gmail/nossr50/listeners/PlayerListener.java

@@ -8,6 +8,9 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.neetgames.mcmmo.player.OnlineMMOPlayer;
 import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
 import com.gmail.nossr50.datatypes.skills.SubSkillType;
+import com.gmail.nossr50.datatypes.skills.subskills.taming.CallOfTheWildType;
+import com.gmail.nossr50.events.McMMOReplaceVanillaTreasureEvent;
+import com.gmail.nossr50.events.fake.FakePlayerAnimationEvent;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.party.ShareHandler;
@@ -258,7 +261,7 @@ public class PlayerListener implements Listener {
      *
      * @param event The event to modify
      */
-    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
     public void onPlayerFishHighest(PlayerFishEvent event) {
         /* WORLD BLACKLIST CHECK */
         if(WorldBlacklist.isWorldBlacklisted(event.getPlayer().getWorld()))
@@ -292,12 +295,20 @@ public class PlayerListener implements Listener {
                 if(event.getCaught() != null) {
                     Item fishingCatch = (Item) event.getCaught();
 
-                    if (Config.getInstance().   getFishingOverrideTreasures() &&
+                    if (Config.getInstance().getFishingOverrideTreasures() &&
                             fishingCatch.getItemStack().getType() != Material.SALMON &&
                             fishingCatch.getItemStack().getType() != Material.COD &&
                             fishingCatch.getItemStack().getType() != Material.TROPICAL_FISH &&
                             fishingCatch.getItemStack().getType() != Material.PUFFERFISH) {
-                        fishingCatch.setItemStack(new ItemStack(Material.SALMON, 1));
+
+                        ItemStack replacementCatch = new ItemStack(Material.SALMON, 1);
+
+                        McMMOReplaceVanillaTreasureEvent replaceVanillaTreasureEvent = new McMMOReplaceVanillaTreasureEvent(fishingCatch, replacementCatch);
+                        Bukkit.getPluginManager().callEvent(replaceVanillaTreasureEvent);
+
+                        //Replace
+                        replacementCatch = replaceVanillaTreasureEvent.getReplacementItemStack();
+                        fishingCatch.setItemStack(replacementCatch);
                     }
 
                     if (Permissions.vanillaXpBoost(player, PrimarySkillType.FISHING)) {

+ 5 - 1
src/main/java/com/gmail/nossr50/mcMMO.java

@@ -615,7 +615,11 @@ public class mcMMO extends JavaPlugin {
 
     private void scheduleTasks() {
         // Periodic save timer (Saves every 10 minutes by default)
-        long saveIntervalTicks = Config.getInstance().getSaveInterval() * 1200;
+        long second = 20;
+        long minute = second * 60;
+
+        long saveIntervalTicks = Math.max(minute, Config.getInstance().getSaveInterval() * minute);
+
         new SaveTimerTask().runTaskTimer(this, saveIntervalTicks, saveIntervalTicks);
 
         // Cleanup the backups folder

+ 1 - 1
src/main/java/com/gmail/nossr50/runnables/database/FormulaConversionTask.java

@@ -31,7 +31,7 @@ public class FormulaConversionTask extends BukkitRunnable {
 
             // If the mmoPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
             if (mmoPlayer == null) {
-                profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName, false);
+                profile = mcMMO.getDatabaseManager().queryPlayerDataByUUID(playerName);
 
                 if (!profile.isLoaded()) {
                     mcMMO.p.debug("Profile not loaded.");

+ 1 - 1
src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java

@@ -99,7 +99,7 @@ public class UUIDUpdateAsyncTask implements Runnable {
         position += batch.size();
         plugin.getLogger().info(String.format("Conversion progress: %d/%d users", position, userNames.size()));
 
-        if (position == userNames.size()) {
+        if (position +1 >= userNames.size()) {
             mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UUIDS);
             awaiter.countDown();
             plugin.getLogger().info("UUID checks completed");

+ 34 - 26
src/main/java/com/gmail/nossr50/runnables/player/PlayerProfileLoadingTask.java

@@ -2,12 +2,13 @@ package com.gmail.nossr50.runnables.player;
 
 import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-import com.neetgames.mcmmo.player.MMOPlayerData;
+import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.runnables.commands.McScoreboardKeepTask;
 import com.gmail.nossr50.util.EventUtils;
 import com.gmail.nossr50.util.Misc;
+import com.gmail.nossr50.util.player.UserManager;
 import com.gmail.nossr50.util.scoreboards.ScoreboardManager;
 import org.bukkit.Server;
 import org.bukkit.entity.Player;
@@ -41,40 +42,46 @@ public class PlayerProfileLoadingTask extends BukkitRunnable {
             return;
         }
 
-        try {
-            MMOPlayerData mmoPlayerData = mcMMO.getDatabaseManager().queryPlayerDataByPlayer(player);
-            McMMOPlayer mmoPlayer = new McMMOPlayer(player, player.getUniqueId(), player.getName());
-            new ApplySuccessfulProfile(new McMMOPlayer(player, )).runTask(mcMMO.p);
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(player.getUniqueId(), player.getName());
+
+        if(!profile.isLoaded()) {
+            mcMMO.p.getLogger().info("Creating new data for player: "+player.getName());
+            //Profile isn't loaded so add as new user
+            profile = mcMMO.getDatabaseManager().newUser(player);
+        }
+
+        // If successful, schedule the apply
+        if (profile.isLoaded()) {
+            new ApplySuccessfulProfile(new McMMOPlayer(player, profile)).runTask(mcMMO.p);
             EventUtils.callPlayerProfileLoadEvent(player, profile);
             return;
+        }
 
-        } catch () {
-            // Print errors to console/logs if we're failing at least 2 times in a row to load the profile
-            if (attempt >= 3)
-            {
-                //Log the error
-                mcMMO.p.getLogger().severe(LocaleLoader.getString("Profile.Loading.FailureNotice",
-                        player.getName(), String.valueOf(attempt)));
+        // Print errors to console/logs if we're failing at least 2 times in a row to load the profile
+        if (attempt >= 3)
+        {
+            //Log the error
+            mcMMO.p.getLogger().severe(LocaleLoader.getString("Profile.Loading.FailureNotice",
+                    player.getName(), String.valueOf(attempt)));
 
-                //Notify the admins
-                mcMMO.p.getServer().broadcast(LocaleLoader.getString("Profile.Loading.FailureNotice", player.getName()), Server.BROADCAST_CHANNEL_ADMINISTRATIVE);
+            //Notify the admins
+            mcMMO.p.getServer().broadcast(LocaleLoader.getString("Profile.Loading.FailureNotice", player.getName()), Server.BROADCAST_CHANNEL_ADMINISTRATIVE);
 
-                //Notify the player
-                player.sendMessage(LocaleLoader.getString("Profile.Loading.FailurePlayer", String.valueOf(attempt)).split("\n"));
-            }
+            //Notify the player
+            player.sendMessage(LocaleLoader.getString("Profile.Loading.FailurePlayer", String.valueOf(attempt)).split("\n"));
+        }
 
-            // Increment attempt counter and try
-            attempt++;
+        // Increment attempt counter and try
+        attempt++;
 
-            new PlayerProfileLoadingTask(player, attempt).runTaskLaterAsynchronously(mcMMO.p, (100 + (attempt * 100)));
-        }
+        new PlayerProfileLoadingTask(player, attempt).runTaskLaterAsynchronously(mcMMO.p, (100 + (attempt * 100)));
     }
 
     private class ApplySuccessfulProfile extends BukkitRunnable {
-        private final McMMOPlayer mmoPlayer;
+        private final McMMOPlayer mcMMOPlayer;
 
-        private ApplySuccessfulProfile(McMMOPlayer mmoPlayer) {
-            this.mmoPlayer = mmoPlayer;
+        private ApplySuccessfulProfile(McMMOPlayer mcMMOPlayer) {
+            this.mcMMOPlayer = mcMMOPlayer;
         }
 
         // Synchronized task
@@ -86,8 +93,9 @@ public class PlayerProfileLoadingTask extends BukkitRunnable {
                 return;
             }
 
-            mcMMO.getUserManager().track(mmoPlayer);
-            mmoPlayer.actualizeRespawnATS();
+            mcMMOPlayer.setupPartyData();
+            UserManager.track(mcMMOPlayer);
+            mcMMOPlayer.actualizeRespawnATS();
 
             if (Config.getInstance().getScoreboardsEnabled()) {
                 ScoreboardManager.setupPlayer(player);

+ 1 - 1
src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java

@@ -138,7 +138,7 @@ public class SmeltingManager extends SkillManager {
         ItemStack furnaceResult = furnaceInventory.getResult();
 
         if(furnaceResult == null)
-            return false;
+            return true; //This actually means there is nothing yet in the resulting item slot, which means it should always be okay to double smelt
 
         int resultAmount = furnaceResult.getAmount(); //Amount before double smelt
         int itemLimit = furnaceResult.getMaxStackSize();

+ 16 - 0
src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardManager.java

@@ -344,6 +344,22 @@ public class ScoreboardManager {
         }
     }
 
+    public static void enablePlayerInspectScoreboard(@NotNull Player player, @NotNull McMMOPlayer targetMcMMOPlayer) {
+        ScoreboardWrapper wrapper = getWrapper(player);
+
+        if(wrapper == null) {
+            setupPlayer(player);
+            wrapper = getWrapper(player);
+        }
+
+        if(wrapper != null) {
+            wrapper.setOldScoreboard();
+            wrapper.setTypeInspectStats(targetMcMMOPlayer);
+
+            changeScoreboard(wrapper, Config.getInstance().getInspectScoreboardTime());
+        }
+    }
+
     public static void enablePlayerCooldownScoreboard(Player player) {
         ScoreboardWrapper wrapper = getWrapper(player);
 

+ 12 - 0
src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java

@@ -24,6 +24,7 @@ import org.bukkit.scoreboard.DisplaySlot;
 import org.bukkit.scoreboard.Objective;
 import org.bukkit.scoreboard.Score;
 import org.bukkit.scoreboard.Scoreboard;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 import java.util.Map;
@@ -321,6 +322,17 @@ public class ScoreboardWrapper {
         loadObjective(LocaleLoader.getString("Scoreboard.Header.PlayerInspect", targetPlayer));
     }
 
+    public void setTypeInspectStats(@NotNull McMMOPlayer mcMMOPlayer) {
+        this.sidebarType = SidebarType.STATS_BOARD;
+        targetPlayer = mcMMOPlayer.getPlayer().getName();
+        targetProfile = mcMMOPlayer.getProfile();
+
+        targetSkill = null;
+        leaderboardPage = -1;
+
+        loadObjective(LocaleLoader.getString("Scoreboard.Header.PlayerInspect", targetPlayer));
+    }
+
     public void setTypeCooldowns() {
         this.sidebarType = SidebarType.COOLDOWNS_BOARD;
 

+ 4 - 1
src/main/java/com/gmail/nossr50/util/text/TextUtils.java

@@ -16,9 +16,12 @@ import org.jetbrains.annotations.Nullable;
 import java.util.List;
 
 public class TextUtils {
-
     private static @Nullable LegacyComponentSerializer customLegacySerializer;
 
+    private TextUtils() {
+        // We don't want any instances of this class.
+    }
+
     /**
      * Makes a single component from an array of components, can optionally add prefixes and suffixes to come before and after each component
      * @param componentsArray target array

+ 1 - 1
src/main/java/com/gmail/nossr50/util/upgrade/UpgradeManager.java

@@ -11,7 +11,7 @@ public class UpgradeManager extends ConfigLoader {
     private final Set<UpgradeType> setNeededUpgrades;
 
     public UpgradeManager() {
-        super("upgrades.yml");
+        super("upgrades_overhaul.yml"); //overhaul is added so we don't have any issues with classic
 
         setNeededUpgrades = EnumSet.allOf(UpgradeType.class);
 

+ 30 - 30
src/main/resources/locale/locale_de.properties

@@ -17,7 +17,7 @@ Acrobatics.SubSkill.GracefulRoll.Name = Anmutiges Abrollen
 Acrobatics.SubSkill.Roll.Chance = Chance abzurollen: &e{0}
 Acrobatics.SubSkill.Roll.Description = Lande gezielt, um deinen Fallschaden zu reduzieren.
 Acrobatics.SubSkill.Roll.GraceChance = Chance anmutig abzurollen: &e{0}
-Acrobatics.SubSkill.Roll.Mechanics = &7Abrollen ist eine aktive F\u00E4higkeit mit einem passiven Teil. Immer, wenn du Fallschaden nimmst, gibt es eine Chance, dass der Schaden reduziert wird, je nachdem wie hoch dein Akrobatik-Level ist. Auf Level 50 hast du eine &e{0}%&7 Chance, den Schaden zu reduzieren bzw. &e{1}%&7 wenn Anmutiges Abrollen aktiviert wird. Die Erfolgschance steigt linear bis Level &e{2}&7, auf welchem es seinen maximalen Wert erreicht. Jedes Akrobatik-Level gibt dir eine &e{3}%&7 Chance zum erfolgreichen Abrollen. H\u00E4ltst du im Fall die Duck-Taste (standardm\u00E4\u00DFig Shift), aktivierst du Anmutiges Abrollen, welches den Fallschaden um noch mehr Schaden reduzieren oder sogar komplett verhindern kann. Normales Abrollen wird maximal &c{4}&7 Schaden verhindern, Anmutiges Abrollen bis zu &a{5}&7.
+Acrobatics.SubSkill.Roll.Mechanics = &7Abrollen ist eine aktive F\u00E4higkeit mit einem passiven Teil. Immer, wenn du Fallschaden nimmst, gibt es eine Chance, dass der Schaden reduziert wird, je nachdem wie hoch dein Akrobatik-Level ist. Auf Level 50 hast du eine &e{0}%&7 Chance, den Schaden zu reduzieren bzw. &e{1}%&7 wenn Anmutiges Abrollen aktiviert wird. Die Erfolgschance steigt linear bis Level &e{2}&7, auf welchem es seinen maximalen Wert erreicht. Jedes Akrobatik-Level gibt dir eine &e{3}%&7 Chance zum erfolgreichen Abrollen. H\u00E4ltst du im Fall die Duck-Taste (standardm\u00E4\u00DFig Shift), aktivierst du Anmutiges Abrollen, welches den Fallschaden auf noch weniger Schaden reduzieren oder sogar komplett verhindern kann. Normales Abrollen wird maximal &c{4}&7 Schaden verhindern, Anmutiges Abrollen bis zu &a{5}&7.
 Acrobatics.SubSkill.Roll.Name = Abrollen
 Acrobatics.SubSkill.Roll.Stat = Chance abzurollen
 Acrobatics.SubSkill.Roll.Stat.Extra = Chance anmutig abzurollen
@@ -62,7 +62,7 @@ Axes.Ability.Lower = &7&o**Du senkst deine Axt.**
 Axes.Ability.Ready = &a&o**Du hebst deine Axt...**
 Axes.Ability.Ready.Extra = &3Du &6hebst&3 deine Axt. &7({0} ist f\u00FCr {1}s pausiert)
 Axes.Combat.CritStruck = &cDu wurdest &4schwer &cverwundet!
-Axes.Combat.CriticalHit = &4&Kritischer Treffer!
+Axes.Combat.CriticalHit = &4Kritischer Treffer!
 Axes.Combat.GI.Proc = &a**Du landest einen &2gewaltigen &aSchlag**
 Axes.Combat.GI.Struck = &a&o**Von einem Wuchtschlag getroffen**
 Axes.Combat.SS.Struck = &a&o**Von einem Sch\u00E4delspalter getroffen**
@@ -233,7 +233,7 @@ Commands.Party.PartyFull.InviteAccept = Du kannst der Party &a{0}&c nicht beitre
 Commands.Party.Quit = &a- Verlasse deine aktuelle Party.
 Commands.Party.Rename = &7Party Name wurde zu &f{0} &7ver\u00E4ndert
 Commands.Party.SetSharing = &7Party {0} teilen: &3{1}
-Commands.Party.ShareMode = &8Teilen Modus:
+Commands.Party.ShareMode = &8Teilen-Modus:
 Commands.Party.Status = &8Name: &f{0} {1} &8Level: &3{2}
 Commands.Party.Status.Alliance = &8Verb\u00FCndeter: &f{0}
 Commands.Party.Teleport = &a- Teleportiere dich zu Partymitgliedern.
@@ -242,7 +242,7 @@ Commands.Party.ToggleShareCategory = &7Party Item teilen f\u00FCr&6{0} &7wurde &
 Commands.Party.UnlockedFeatures = &8Freigeschaltete Features: &7&o{0}
 Commands.Party1 = &a- Erstelle eine neue Party.
 Commands.Party2 = &a- Tritt der Party eines Spielers bei.
-Commands.PowerLevel = &4GESAMT LEVEL: &a{0}
+Commands.PowerLevel = &4Gesamtlevel: &a{0}
 Commands.PowerLevel.Capped = &4Gesamtlevel: &a{0} &4H\u00F6chstlevel: &e{1}
 Commands.PowerLevel.Leaderboard = --mcMMO&9 Power-Level &eBestenliste--
 Commands.Reset = &a- Setze ein Skilllevel auf 0
@@ -262,14 +262,14 @@ Commands.Skill.ChildSkill = Unterskills sind f\u00FCr diesen Befehl nicht benutz
 Commands.Skill.Invalid = Das ist kein g\u00FCltiger Skillname!
 Commands.Skill.Leaderboard = --mcMMO &9{0}&e Bestenliste--
 Commands.SkillInfo = &a- Detaillierte Informationen zu einem Skill.
-Commands.Stats = &a- Zeige deine Skill Statistiken.
+Commands.Stats = &a- Zeige deine Skill-Statistiken.
 Commands.Stats.Self.Overhaul = Statistiken
 Commands.ToggleAbility = &a- Schalte F\u00E4higkeiten-Aktivierung mit Rechtsklick an oder aus.
 Commands.Usage.0 = &cDie korrekte Verwendung ist /{0}
 Commands.Usage.1 = &cDie korrekte Verwendung ist /{0} {1}
 Commands.Usage.2 = &cDie korrekte Verwendung ist /{0} {1} {2}
 Commands.Usage.3 = &cDie korrekte Verwendung ist /{0} {1} {2} {3}
-Commands.Usage.3.XP = &cDie korrekte Verwendung ist /{0} {1} {2} {3}&7 (Du kannst auch -s an das Ende des Befehls hinzuf\u00FC"gen, damit der Spieler nicht benachrichtigt wird.)
+Commands.Usage.3.XP = &cDie korrekte Verwendung ist /{0} {1} {2} {3}&7 (Du kannst auch -s an das Ende des Befehls hinzuf\u00FCgen, damit der Spieler nicht benachrichtigt wird.)
 Commands.Usage.FullClassName = Klassenname
 Commands.Usage.Level = Level
 Commands.Usage.Message = Nachricht
@@ -285,7 +285,7 @@ Commands.XPBar.DisableAll = &6Alle mcMMO Erfahrungsleisten wurden deaktiviert, b
 Commands.XPBar.Reset = &6Die Erfahrungsleisten-Einstellungen f\u00FCr mcMMO wurden zur\u00FCckgesetzt.
 Commands.XPBar.SettingChanged = &6Die Erfahrungsleisten-Einstellungen f\u00FCr &a{0}&6 wurden gesetzt auf: &a{1}
 Commands.XPBar.Usage = Die korrekte Verwendung ist /mmoxpbar <skillname | reset> <show | hide>
-Commands.XPGain = &8XP ZUWACHS: &f{0}
+Commands.XPGain = &8XP-Zuwachs: &f{0}
 Commands.XPGain.Acrobatics = Fallen
 Commands.XPGain.Alchemy = Tr\u00E4nke brauen
 Commands.XPGain.Archery = Monster angreifen
@@ -379,12 +379,12 @@ Excavation.SubSkill.GigaDrillBreaker.Description = Dreifache Droprate, dreifache
 Excavation.SubSkill.GigaDrillBreaker.Name = Gigabohrer
 Excavation.SubSkill.GigaDrillBreaker.Stat = Gigabohrer-Dauer
 
-Fishing.Ability.Info = Zauberj\u00E4ger: &7 **Verbessert sich mit Schatzj\u00E4ger-Rang**
+Fishing.Ability.Info = Zauberj\u00E4ger: &7**Verbessert sich mit Schatzj\u00E4ger-Rang**
 Fishing.Ability.Locked.0 = Gesperrt bis Level {0}!
 Fishing.Ability.Locked.1 = Gesperrt bis Level {0}!
 Fishing.Ability.Locked.2 = Gesperrt bis Level {0}!
 Fishing.Ability.TH.Boom = &c&lDeine Angelschnur hat sich in einer &4&lSeemine &c&lverfangen!
-Fishing.Ability.TH.MagicFound = &bDu f\u00FChlst etwas Magisches in diesem Fang...
+Fishing.Ability.TH.MagicFound = &bDu f\u00FChlst etwas Magisches an diesem Fang...
 Fishing.Ability.TH.Poison = &7Irgendetwas stinkt hier...
 Fishing.Chance.Raining = &9Regen-Bonus
 Fishing.Exhausting = &c&oUnsachgem\u00E4\u00DFe Nutzung der Angelrute f\u00FChrt zu Erm\u00FCdung und Abnutzen der Rute.
@@ -557,7 +557,7 @@ Inspect.OfflineStats = mcMMO Stats f\u00FCr Offline-Spieler &e{0}
 Inspect.Stats = &amcMMO Stats f\u00FCr &e{0}
 Inspect.TooFar = Du bist zu weit entfernt um den Spieler zu inspizieren!
 
-Item.ChimaeraWing.Fail = &c**CHIMAERA FL\u00DCGEL GESCHEITERT!**
+Item.ChimaeraWing.Fail = &c**Chimaera Fl\u00FCgel gescheitert!**
 Item.ChimaeraWing.Lore = &7Teleportiert dich zu deinem Bett.
 Item.ChimaeraWing.Name = Chimaera Fl\u00FCgel
 Item.ChimaeraWing.NotEnough = Du ben\u00F6tigst &e{0}&c weitere &6{1}&c!
@@ -595,7 +595,7 @@ JSON.JWrapper.Perks.Lucky = {0}% Bessere Chancen
 JSON.JWrapper.Target.Block = Block
 JSON.JWrapper.Target.Player = Spieler
 JSON.JWrapper.Target.Type = Zieltyp:
-JSON.LevelRequirement = Level Voraussetzung
+JSON.LevelRequirement = Level-Voraussetzung
 JSON.Locked = -=[NICHT VERF\u00DCGBAR]=-
 JSON.Mining = Bergbau
 JSON.Notification.SuperAbility = {0}
@@ -610,7 +610,7 @@ JSON.Type.Passive = Passiv
 JSON.Type.SuperAbility = Superf\u00E4higkeit
 JSON.URL.Discord = Der offizielle (englische) mcMMO Discord Server!
 JSON.URL.Patreon = Unterst\u00FCtze die Entwicklung von mcMMO \u00FCber nossr50's Patreon!
-JSON.URL.Spigot = Die offizielle mcmmo Spigot Seite
+JSON.URL.Spigot = Die offizielle mcMMO Spigot-Seite.
 JSON.URL.Translation = \u00DCbersetze mcMMO in andere Sprachen!
 JSON.URL.Website = Die offizielle mcMMO Website!
 JSON.URL.Wiki = Das offizielle mcMMO Wiki!
@@ -666,7 +666,7 @@ Mining.SubSkill.SuperBreaker.Stat = Superbrecher L\u00E4nge
 
 Notifications.Admin.Format.Others = &6(&amcMMO &3Admin&6) &7{0}
 Notifications.Admin.Format.Self = &6(&amcMMO&6) &7{0}
-Notifications.Admin.XPRate.End.Others = {0} &7hat das Bonuserfahrungs-Event beendet
+Notifications.Admin.XPRate.End.Others = {0} &7hat das Bonuserfahrungs-Event beendet.
 Notifications.Admin.XPRate.End.Self = &7Du hast das Bonuserfahrungs-Event beendet.
 Notifications.Admin.XPRate.Start.Others = {0} &7hat ein Bonuserfahrungs-Event mit einem Faktor von {1}x gestartet.
 Notifications.Admin.XPRate.Start.Self = &7Du hast den globalen Erfahrungsraten-Multiplikator auf &6{0}x&7 gesetzt.
@@ -715,7 +715,7 @@ Party.Help.0 = &cDie korrekte Benutzung ist &3{0} <spieler> [passwort].
 Party.Help.1 = &cUm eine Gruppe zu erstellen, nutze &3{0} <gruppenname> [gruppenpasswort].
 Party.Help.10 = &cNutze &3{0} &cum Erfahrungsteilung mit Mitgliedern zu aktivieren.
 Party.Help.2 = &cNutze &3{0} &cf\u00FCr mehr Informationen.
-Party.Help.3 = &cNutze &3{0} <spieler> [passwort] &czum beitreten oder &3{1} &czum verlassen.
+Party.Help.3 = &cNutze &3{0} <spieler> [passwort] &czum Beitreten oder &3{1} &czum Verlassen.
 Party.Help.4 = &cUm deine Gruppe zu sperren oder entsperren, nutze &3{0}.
 Party.Help.5 = &cUm deine Gruppe per Passwort zu sch\u00FCtzen, nutze &3{0} <passwort>.
 Party.Help.6 = &cUm einen Spieler aus deiner Gruppe zu entfernen, nutze &3{0} <spieler>.
@@ -823,7 +823,7 @@ Repair.SubSkill.StoneRepair.Description = Repariere Stein-Werkzeuge.
 Repair.SubSkill.StoneRepair.Name = Stein-Reparatur ({0}+ SKILL)
 Repair.SubSkill.SuperRepair.Description = Doppelte Effektivit\u00E4t.
 Repair.SubSkill.SuperRepair.Name = Super-Reparatur
-Repair.SubSkill.SuperRepair.Stat = Chance auf Superreparatur
+Repair.SubSkill.SuperRepair.Stat = Chance auf Super-Reparatur
 
 Salvage.Ability.Bonus.0 = Fortgeschrittenes Verwerten
 Salvage.Ability.Bonus.1 = Max Ertrag {0} Item zerst\u00F6rt
@@ -845,7 +845,7 @@ Salvage.Skills.Success = &aItem verwertet!
 Salvage.Skills.TooDamaged = &4Das Item ist zu besch\u00E4digt um verwertet zu werden.
 Salvage.SubSkill.ArcaneSalvage.Description = Extrahiere Verzauberungen aus Items.
 Salvage.SubSkill.ArcaneSalvage.Name = Magische Bergung
-Salvage.SubSkill.ArcaneSalvage.Stat = Magische Bergung: &eRank {0}/{1}
+Salvage.SubSkill.ArcaneSalvage.Stat = Magische Bergung: &eRang {0}/{1}
 Salvage.SubSkill.ScrapCollector.Description = Verschrotte einen Gegenstand, um Materialien zur\u00FCckzugewinnen; eine perfekte Verschrottung erfordert Gl\u00FCck und Geschick.
 Salvage.SubSkill.ScrapCollector.Name = Schrottsammler
 Salvage.SubSkill.ScrapCollector.Stat = Schrottsammler: &aVerschrotte bis zu &e{0}&a Gegenst\u00E4nde. Hierbei spielt Gl\u00FCck eine gewisse Rolle.
@@ -856,13 +856,13 @@ Scoreboard.Header.PlayerCooldowns = mcMMO Abklingzeiten
 Scoreboard.Header.PlayerInspect = mcMMO Stats: {0}
 Scoreboard.Header.PlayerRank = mcMMO Bestenlisten
 Scoreboard.Header.PlayerStats = mcMMO Stats
-Scoreboard.Header.PowerLevel = Gesamt Level
+Scoreboard.Header.PowerLevel = Gesamt-Level
 Scoreboard.Misc.Ability = F\u00E4higkeit
 Scoreboard.Misc.Cooldown = &dAbklingzeit
 Scoreboard.Misc.CurrentXP = &aAktuelle XP
 Scoreboard.Misc.Level = &3Level
 Scoreboard.Misc.Overall = &6Insgesamt
-Scoreboard.Misc.PowerLevel = &6Gesamt Level
+Scoreboard.Misc.PowerLevel = &6Gesamt-Level
 Scoreboard.Misc.RemainingXP = Verbliebene XP
 
 Server.ConsoleName = &e[Server]
@@ -884,9 +884,9 @@ Skills.TooTired = Du bist zu m\u00FCde um diese F\u00E4higkeit zu verwenden. &e(
 Skills.TooTired.Extra = &6{0} &eSuperf\u00E4higkeit CDs - {1}
 Skills.TooTired.Named = &7(&6{0}&e {1}s&7)
 
-Smelting.Ability.Locked.0 = Gesperrt bis {0}+ Skill (XP BOOST)
-Smelting.Ability.Locked.1 = Gesperrt bis {0}+ Skill (SCHMELZTIEGEL)
-Smelting.Effect.4 = Vanilla XP Boost
+Smelting.Ability.Locked.0 = Gesperrt bis {0}+ Skill (XP-Boost)
+Smelting.Ability.Locked.1 = Gesperrt bis {0}+ Skill (Schmelztiegel)
+Smelting.Effect.4 = Vanilla XP-Boost
 Smelting.Effect.5 = Erh\u00F6ht die erhaltene Erfahrung beim Schmelzen.
 Smelting.Listener = Schmelzen:
 Smelting.SkillName = Schmelzen
@@ -895,7 +895,7 @@ Smelting.SubSkill.FluxMining.Name = Schmelztiegel
 Smelting.SubSkill.FluxMining.Stat = Schmelztiegel Chance
 Smelting.SubSkill.FuelEfficiency.Description = Erh\u00F6he die Brenndauer des Brennstoffes in \u00D6fen.
 Smelting.SubSkill.FuelEfficiency.Name = Brennstoff Effizienz
-Smelting.SubSkill.FuelEfficiency.Stat = Brennstoff Effizienz Multiplikator: &e{0}x
+Smelting.SubSkill.FuelEfficiency.Stat = Brennstoff Effizienz-Multiplikator: &e{0}x
 Smelting.SubSkill.SecondSmelt.Description = Verdoppelt den Ertrag beim Schmelzen.
 Smelting.SubSkill.SecondSmelt.Name = Extra Schmelzung
 Smelting.SubSkill.SecondSmelt.Stat = Extra Schmelzung Chance
@@ -906,12 +906,12 @@ Smelting.SubSkill.UnderstandingTheArt.Stat = Vanilla Erfahrungsmultiplikator: &e
 Stats.Header.Combat = &6-=Kampfskills=-
 Stats.Header.Gathering = &6-=Sammelskills=-
 Stats.Header.Misc = &6-=Weitere Skills=-
-Stats.Own.Stats = &aSkill Statistik
+Stats.Own.Stats = &aSkill-Statistik
 
-Swords.Ability.Lower = &7&o**Du senkst dein Sschwert.**
+Swords.Ability.Lower = &7&o**Du senkst dein Schwert.**
 Swords.Ability.Ready = &a&o**Du hebst dein Schwert...**
 Swords.Combat.Bleeding = &a**Gegner blutet**
-Swords.Combat.Bleeding.Started = &4 Du blutest!
+Swords.Combat.Bleeding.Started = &4Du blutest!
 Swords.Combat.Bleeding.Stopped = &7Das Bluten hat &aaufgeh\u00F6rt&7!
 Swords.Combat.Counter.Hit = &4Treffer durch Gegenangriff!
 Swords.Combat.Countered = &a**Gegenangriff**
@@ -920,7 +920,7 @@ Swords.Combat.SS.Struck = &4Getroffen von S\u00E4gezahnschlag!
 Swords.Effect.4 = S\u00E4gezahnschlag, Blutung+
 Swords.Effect.5 = {0} Ticks Blutung
 Swords.Listener = Schwert:
-Swords.SkillName = Sschwert
+Swords.SkillName = Schwert
 Swords.Skills.SS.Off = &a&o**S\u00E4gezahnschlag abgenutzt**
 Swords.Skills.SS.On = &a&o**S\u00E4gezahnschlag aktiviert**
 Swords.Skills.SS.Other.Off = {0}s &cS\u00E4gezahnschlag&a ist &aabgenutzt.
@@ -983,7 +983,7 @@ Taming.SubSkill.Pummel.Name = Pummel
 Taming.SubSkill.Pummel.TargetMessage = Du wurdest von einem Wolf zur\u00FCckgeschlagen!
 Taming.SubSkill.SharpenedClaws.Description = Schadens-Bonus
 Taming.SubSkill.SharpenedClaws.Name = Gesch\u00E4rfte Krallen
-Taming.SubSkill.ShockProof.Description = Reduktion von Explosions-Schaden.
+Taming.SubSkill.ShockProof.Description = Reduktion von Explosionsschaden.
 Taming.SubSkill.ShockProof.Name = Schock-Sicher
 Taming.SubSkill.ThickFur.Description = Verminderter Schaden, Feuer-Resistenz
 Taming.SubSkill.ThickFur.Name = Dicker Pelz
@@ -1030,7 +1030,7 @@ Unarmed.SubSkill.SteelArmStyle.Description = Verst\u00E4rkt deinen Arm mit der Z
 Unarmed.SubSkill.SteelArmStyle.Name = St\u00E4hlerner Arm
 Unarmed.SubSkill.UnarmedLimitBreak.Description = Durchbreche deine Grenzen!
 Unarmed.SubSkill.UnarmedLimitBreak.Name = \u00DCberwindung
-Unarmed.SubSkill.UnarmedLimitBreak.Stat = Bonus Schaden durch \u00DCberwindung
+Unarmed.SubSkill.UnarmedLimitBreak.Stat = Bonus-Schaden durch \u00DCberwindung
 
 UpdateChecker.NewAvailable = Eine neue Version von mcMMO ist auf Spigot erh\u00E4ltlich!
 UpdateChecker.Outdated = Du verwendest eine veraltete mcMMO Version!
@@ -1088,9 +1088,9 @@ XPBar.Woodcutting = Holzf\u00E4llen Level: &6{0}
 
 XPRate.Event = &6Es findet derzeit ein Skill-Event statt! Du bekommst aktuell &c{0} &6mal so viel Erfahrung f\u00FCr deine Skills wie normal!
 
-mcMMO.Description = &3\u00DCber das &emcMMO&3 Projekt:,&6mcMMO ist ein &copen source&6 RPG mod erstellt in Februar 2011&6von &9nossr50&6. Das Ziel ist es ein qualitatives RPG Erlebnis zu liefern.,&3Tips:,&6 - &aNutze &c/mcmmo help&a um Befehle zu sehen &6,- &aNutze &c/skillname&a f\u00FCr detaillierte Skill Infos,&3Entwickler:,&6 - &anossr50 &9(Erfinder & Projektleitung),&6 - &aGJ &9(Fr\u00FChere Projektleitung),&6 - &aNuclearW &9(Entwickler),&6 - &abm01 &9(Entwickler),&6 - &aTfT_02 &9(Entwickler),&6 - &aGlitchfinder &9(Entwickler),&6 - &at00thpick1 &9(Entwickler),&6 - &alumis31 &9 (Urspr\u00FCngliche Deutsche \u00DCbersetzung),&6 - &aOverCrave &9 (Neue Deutsche \u00DCbersetzung & \u00DCberarbeitung),&3N\u00FCtzliche Links:,&6 - &ahttps://github.com/mcMMO-Dev/mcMMO/issues&6 Bug Reporting,&6 - &ahttps://discord.gg/EJGVanb &6 Offizieller Discord (Englisch)
+mcMMO.Description = &3\u00DCber das &emcMMO&3 Projekt: &6mcMMO ist ein &copen source&6 RPG-Mod erstellt im Februar 2011 &6von &9nossr50&6. Das Ziel ist es ein qualitatives RPG Erlebnis zu liefern. &3Tipps:&6 - &aNutze &c/mcmmo help&a um Befehle zu sehen, &6 - &aNutze &c/skillname&a f\u00FCr detaillierte Skill Infos, &3Entwickler:&6 - &anossr50 &9(Erfinder & Projektleitung),&6 - &aGJ &9(Fr\u00FChere Projektleitung),&6 - &aNuclearW &9(Entwickler),&6 - &abm01 &9(Entwickler),&6 - &aTfT_02 &9(Entwickler),&6 - &aGlitchfinder &9(Entwickler),&6 - &at00thpick1 &9(Entwickler),&6 - &alumis31 &9(Urspr\u00FCngliche Deutsche \u00DCbersetzung),&6 - &aOverCrave &9(Neue Deutsche \u00DCbersetzung & \u00DCberarbeitung),&6 - &aAnseba &9(\u00DCberarbeitung Deutsche \u00DCbersetzung), &3N\u00FCtzliche Links:&6 - &ahttps://github.com/mcMMO-Dev/mcMMO/issues&6 Bug Reporting,&6 - &ahttps://discord.gg/EJGVanb &6 Offizieller Discord (Englisch)
 mcMMO.Description.FormerDevs = &3Ehemalige Entwickler: &aGJ, NuclearW, bm01, TfT_02, Glitchfinder
-mcMMO.NoInvites = &cDu hast zurzeit keine Einladungen
+mcMMO.NoInvites = &cDu hast zurzeit keine Einladungen.
 mcMMO.NoPermission = &4Unzureichende Berechtigungen.
 mcMMO.NoSkillNote = &8Wenn du keinen Zugriff auf einen Skill hast wird er hier nicht angezeigt.
 mcMMO.Template.Prefix = &6(&amcMMO&6) &7{0}

+ 1 - 0
src/main/resources/upgrades.yml → src/main/resources/upgrades_overhaul.yml

@@ -12,3 +12,4 @@ Upgrades_Finished:
     FIX_NETHERITE_SALVAGE_QUANTITIES: false
     ADD_SQL_2_2: false
     ADD_UUIDS: false
+    SQL_CHARSET_UTF8MB4: false

+ 32 - 0
src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java

@@ -0,0 +1,32 @@
+package com.gmail.nossr50.util.text;
+
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * This Unit Test checks if Adventure was set up correctly and works as expected.
+ * Normally we can rely on this to be the case. However sometimes our dependencies
+ * lack so far behind that things stop working correctly.
+ * This test ensures that basic functionality is guaranteed to work as we would expect.
+ * 
+ * See https://github.com/mcMMO-Dev/mcMMO/pull/4446
+ *
+ */
+public class TextUtilsTest {
+
+    @Test
+    public void testColorizeText() {
+        String inputText = "&4This text should be red.";
+
+        /*
+         * If this method raises an exception, we know Adventure is not set up correctly.
+         * This will also make the test fail and warn us about it.
+         */
+        TextComponent component = TextUtils.colorizeText(inputText);
+
+        Assert.assertEquals("Looks like Adventure is not working correctly.",
+                NamedTextColor.DARK_RED, component.color());
+    }
+}