Browse Source

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

nossr50 4 years ago
parent
commit
fc10243d6f
21 changed files with 1740 additions and 167 deletions
  1. 9 0
      Changelog.txt
  2. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/AddlevelsCommand.java
  3. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/AddxpCommand.java
  4. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java
  5. 1 1
      src/main/java/com/gmail/nossr50/commands/experience/MmoeditCommand.java
  6. 2 2
      src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java
  7. 1 1
      src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreCommand.java
  8. 3 3
      src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java
  9. 1 1
      src/main/java/com/gmail/nossr50/commands/skills/SkillGuideCommand.java
  10. 1524 0
      src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java
  11. 6 6
      src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java
  12. 23 19
      src/main/java/com/gmail/nossr50/listeners/EntityListener.java
  13. 1 1
      src/main/java/com/gmail/nossr50/listeners/PlayerListener.java
  14. 1 1
      src/main/java/com/gmail/nossr50/runnables/commands/McrankCommandDisplayTask.java
  15. 2 2
      src/main/java/com/gmail/nossr50/runnables/commands/MctopCommandDisplayTask.java
  16. 1 2
      src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java
  17. 1 1
      src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java
  18. 1 1
      src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
  19. 2 2
      src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardManager.java
  20. 42 5
      src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java
  21. 116 116
      src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java

+ 9 - 0
Changelog.txt

@@ -99,6 +99,15 @@ 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.170
+    Reverted a change that broke compatibility with the mcMMO papi ecloud thingy
+
+Version 2.1.169
+    Fixed a few memory leaks involving arrows
+    Fixed mcMMO inappropriately assigning metadata to projectiles not fired from players
+    Fix mctop not working if locale was set to something other than en_US
+    mcMMO will now always emulate lure in order to stack it correctly and avoid vanilla bugs
+
 Version 2.1.168
     Fixed an IndexOutOfBoundsException error when trying to access UserBlockTracker from an invalid range (thanks t00thpick1)
     (API) UserBlockTracker is now the interface by which our block-tracker will be known (thanks t00thpick1)

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/AddlevelsCommand.java

@@ -45,6 +45,6 @@ public class AddlevelsCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, rootSkill.getLocalizedName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.1", value, rootSkill.getName()));
     }
 }

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/AddxpCommand.java

@@ -46,6 +46,6 @@ public class AddxpCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, rootSkill.getLocalizedName()));
+        player.sendMessage(LocaleLoader.getString("Commands.addxp.AwardSkill", value, rootSkill.getName()));
     }
 }

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

@@ -161,7 +161,7 @@ public abstract class ExperienceCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getLocalizedName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
         }
     }
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/experience/MmoeditCommand.java

@@ -51,6 +51,6 @@ public class MmoeditCommand extends ExperienceCommand {
         if(isSilent)
             return;
 
-        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", rootSkill.getLocalizedName(), value));
+        player.sendMessage(LocaleLoader.getString("Commands.mmoedit.Modified.1", rootSkill.getName(), value));
     }
 }

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

@@ -142,7 +142,7 @@ public class SkillresetCommand implements TabExecutor {
     }
 
     protected void handlePlayerMessageSkill(Player player, RootSkill rootSkill) {
-        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", rootSkill.getLocalizedName()));
+        player.sendMessage(LocaleLoader.getString("Commands.Reset.Single", rootSkill.getName()));
     }
 
     private boolean validateArguments(CommandSender sender, String skillName) {
@@ -154,7 +154,7 @@ public class SkillresetCommand implements TabExecutor {
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         else {
-            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getLocalizedName(), playerName));
+            sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardSkill.2", rootSkill.getName(), playerName));
         }
     }
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/hardcore/HardcoreCommand.java

@@ -59,6 +59,6 @@ public class HardcoreCommand extends HardcoreModeCommand {
             rootSkill.setHardcoreStatLossEnabled(enable);
         }
 
-        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (rootSkill == null ? "all skills" : rootSkill.getLocalizedName())));
+        mcMMO.p.getServer().broadcastMessage(LocaleLoader.getString("Hardcore.Mode." + (enable ? "Enabled" : "Disabled"), LocaleLoader.getString("Hardcore.DeathStatLoss.Name"), (rootSkill == null ? "all skills" : rootSkill.getName())));
     }
 }

+ 3 - 3
src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java

@@ -49,7 +49,7 @@ public abstract class SkillCommand implements TabExecutor {
     public SkillCommand(@NotNull RootSkill rootSkill) {
         this.rootSkill = CoreSkills.getSkill(primarySkillType);
         this.primarySkillType = primarySkillType;
-        skillName = rootSkill.getLocalizedName();
+        skillName = rootSkill.getName();
         skillGuideCommand = new SkillGuideCommand(rootSkill);
     }
 
@@ -177,10 +177,10 @@ public abstract class SkillCommand implements TabExecutor {
             {
                 if(i+1 < parentList.size())
                 {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getLocalizedName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                     parentMessage.append(ChatColor.GRAY).append(", ");
                 } else {
-                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getLocalizedName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
+                    parentMessage.append(LocaleLoader.getString("Effects.Child.ParentList", parentList.get(i).getName(), mcMMOPlayer.getSkillLevel(parentList.get(i))));
                 }
             }
 

+ 1 - 1
src/main/java/com/gmail/nossr50/commands/skills/SkillGuideCommand.java

@@ -22,7 +22,7 @@ public class SkillGuideCommand implements CommandExecutor {
 
     public SkillGuideCommand(@NotNull RootSkill rootSkill) {
         this.rootSkill = rootSkill;
-        header = LocaleLoader.getString("Guides.Header", rootSkill.getLocalizedName());
+        header = LocaleLoader.getString("Guides.Header", rootSkill.getName());
         guide = getGuide(rootSkill);
     }
 

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

@@ -0,0 +1,1524 @@
+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.getSkillName() + " 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
+                    }
+                }
+            }
+        }
+    }
+}

+ 6 - 6
src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java

@@ -99,10 +99,10 @@ public enum PrimarySkillType {
                 nonChildSkills.add(skill);
             }
 
-            for(SubSkillType subSkillType : skill.subSkillTypes)
-            {
+            for(SubSkillType subSkillType : skill.subSkillTypes) {
                 subSkillNames.add(subSkillType.getNiceNameNoSpaces(subSkillType));
             }
+
             names.add(skill.getName());
         }
 
@@ -234,13 +234,13 @@ public enum PrimarySkillType {
         return null;
     }
 
-    public String getLocalizedName() {
+    public String getName() {
         return StringUtils.getCapitalized(LocaleLoader.getString(StringUtils.getCapitalized(this.toString()) + ".SkillName"));
     }
 
-    public String getName() {
-        return StringUtils.getCapitalized(StringUtils.getCapitalized(this.toString()));
-    }
+//    public String getName() {
+//        return StringUtils.getCapitalized(StringUtils.getCapitalized(this.toString()));
+//    }
 
     public boolean getPermissions(Player player) {
         return Permissions.skillEnabled(player, this);

+ 23 - 19
src/main/java/com/gmail/nossr50/listeners/EntityListener.java

@@ -124,24 +124,26 @@ public class EntityListener implements Listener {
                 if(!WorldGuardManager.getInstance().hasMainFlag(player))
                     return;
             }
-        }
 
-        Entity projectile = event.getProjectile();
+            Entity projectile = event.getProjectile();
 
-        //Should be noted that there are API changes regarding Arrow from 1.13.2 to current versions of the game
-        if (!(projectile instanceof Arrow)) {
-            return;
-        }
+            //Should be noted that there are API changes regarding Arrow from 1.13.2 to current versions of the game
+            if (!(projectile instanceof Arrow)) {
+                return;
+            }
 
-        ItemStack bow = event.getBow();
+            ItemStack bow = event.getBow();
 
-        if (bow != null
-                && bow.containsEnchantment(Enchantment.ARROW_INFINITE)) {
-            projectile.setMetadata(mcMMO.infiniteArrowKey, mcMMO.metadataValue);
-        }
+            if (bow != null
+                    && bow.containsEnchantment(Enchantment.ARROW_INFINITE)) {
+                projectile.setMetadata(mcMMO.infiniteArrowKey, mcMMO.metadataValue);
+            }
 
-        projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * AdvancedConfig.getInstance().getForceMultiplier(), 1.0)));
-        projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(pluginRef, projectile.getLocation()));
+            projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * AdvancedConfig.getInstance().getForceMultiplier(), 1.0)));
+            projectile.setMetadata(mcMMO.arrowDistanceKey, new FixedMetadataValue(pluginRef, projectile.getLocation()));
+            //Cleanup metadata in 1 minute in case normal collection falls through
+            CombatUtils.cleanupArrowMetadata((Projectile) projectile);
+        }
     }
 
     @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
@@ -169,6 +171,8 @@ public class EntityListener implements Listener {
             EntityType entityType = projectile.getType();
 
             if (entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) {
+                CombatUtils.delayArrowMetaCleanup(projectile); //Cleans up metadata 1 minute from now in case other collection methods fall through
+
                 if (!projectile.hasMetadata(mcMMO.bowForceKey))
                     projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, 1.0));
 
@@ -244,7 +248,6 @@ public class EntityListener implements Listener {
         Entity entity = event.getEntity();
         Material notYetReplacedType = block.getState().getType(); //because its from getState() this is the block that hasn't been changed yet, which is likely air/lava/water etc
 
-
         // When the event is fired for the falling block that changes back to a
         // normal block
         // event.getBlock().getType() returns AIR
@@ -463,13 +466,14 @@ public class EntityListener implements Listener {
             LivingEntity livingEntity = (LivingEntity) entityDamageEvent.getEntity();
 
             if(entityDamageEvent.getFinalDamage() >= livingEntity.getHealth()) {
-
-                /*
-                 * This sets entity names back to whatever they are supposed to be
-                 */
+                //This sets entity names back to whatever they are supposed to be
                 CombatUtils.fixNames(livingEntity);
-                }
             }
+        }
+
+        if(entityDamageEvent.getDamager() instanceof Projectile) {
+            CombatUtils.cleanupArrowMetadata((Projectile) entityDamageEvent.getDamager());
+        }
     }
 
     public boolean checkParties(Cancellable event, Player defendingPlayer, Player attackingPlayer) {

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

@@ -839,7 +839,7 @@ public class PlayerListener implements Listener {
             // Do these ACTUALLY have to be lower case to work properly?
             for (PrimarySkillType skill : PrimarySkillType.values()) {
                 String skillName = skill.toString().toLowerCase(Locale.ENGLISH);
-                String localizedName = skill.getLocalizedName().toLowerCase(Locale.ENGLISH);
+                String localizedName = skill.getName().toLowerCase(Locale.ENGLISH);
 
                 if (lowerCaseCommand.equals(localizedName)) {
                     event.setMessage(message.replace(command, skillName));

+ 1 - 1
src/main/java/com/gmail/nossr50/runnables/commands/McrankCommandDisplayTask.java

@@ -53,7 +53,7 @@ public class McrankCommandDisplayTask extends BukkitRunnable {
 //            }
 
             rank = skills.get(skill);
-            sender.sendMessage(LocaleLoader.getString("Commands.mcrank.Skill", skill.getLocalizedName(), (rank == null ? LocaleLoader.getString("Commands.mcrank.Unranked") : rank)));
+            sender.sendMessage(LocaleLoader.getString("Commands.mcrank.Skill", skill.getName(), (rank == null ? LocaleLoader.getString("Commands.mcrank.Unranked") : rank)));
         }
 
         rank = skills.get(null);

+ 2 - 2
src/main/java/com/gmail/nossr50/runnables/commands/MctopCommandDisplayTask.java

@@ -63,10 +63,10 @@ public class MctopCommandDisplayTask extends BukkitRunnable {
         }
         else {
             if(sender instanceof Player) {
-                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getLocalizedName()));
+                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getName()));
             }
             else {
-                sender.sendMessage(ChatColor.stripColor(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getLocalizedName())));
+                sender.sendMessage(ChatColor.stripColor(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getName())));
             }
         }
 

+ 1 - 2
src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java

@@ -56,8 +56,7 @@ public class ArcheryManager extends SkillManager {
     public double distanceXpBonusMultiplier(@NotNull LivingEntity target, @NotNull Entity arrow) {
         //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires
         if(!arrow.hasMetadata(mcMMO.arrowDistanceKey))
-            return arrow.getLocation().distance(target.getLocation());
-
+            return 1;
 
         Location firedLocation = (Location) arrow.getMetadata(mcMMO.arrowDistanceKey).get(0).value();
         Location targetLocation = target.getLocation();

+ 1 - 1
src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java

@@ -265,7 +265,7 @@ public class FishingManager extends SkillManager {
             int convertedLureBonus = 0;
 
             //This avoids a Minecraft bug where lure levels above 3 break fishing
-            if(lureLevel > 3) {
+            if(lureLevel > 0) {
                 masterAnglerCompatibilityLayer.setApplyLure(fishHook, false);
                 convertedLureBonus = lureLevel * 100;
             }

+ 1 - 1
src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java

@@ -37,7 +37,7 @@ public final class CommandRegistrationManager {
     private static void registerSkillCommands() {
         for (PrimarySkillType skill : PrimarySkillType.values()) {
             String commandName = skill.toString().toLowerCase(Locale.ENGLISH);
-            String localizedName = skill.getLocalizedName().toLowerCase(Locale.ENGLISH);
+            String localizedName = skill.getName().toLowerCase(Locale.ENGLISH);
 
             PluginCommand command;
 

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

@@ -94,7 +94,7 @@ public class ScoreboardManager {
             int i = 0;
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
-                skillLabelBuilder.put(type, getShortenedName(colors.get(i) + type.getLocalizedName(), false));
+                skillLabelBuilder.put(type, getShortenedName(colors.get(i) + type.getName(), false));
 
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), getShortenedName(colors.get(i) + type.getSuperAbilityType().getLocalizedName()));
@@ -116,7 +116,7 @@ public class ScoreboardManager {
         else {
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
-                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getLocalizedName()));
+                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getName()));
 
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), formatAbility(type.getSuperAbilityType().getLocalizedName()));

+ 42 - 5
src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java

@@ -24,16 +24,19 @@ import com.gmail.nossr50.util.compat.layers.persistentdata.AbstractPersistentDat
 import com.gmail.nossr50.util.compat.layers.persistentdata.MobMetaFlagType;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.google.common.collect.ImmutableMap;
+import org.bukkit.Bukkit;
 import org.bukkit.GameMode;
 import org.bukkit.Material;
 import org.bukkit.attribute.Attribute;
 import org.bukkit.attribute.AttributeInstance;
+import org.bukkit.enchantments.Enchantment;
 import org.bukkit.entity.*;
 import org.bukkit.event.entity.EntityDamageByEntityEvent;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
 import org.bukkit.event.entity.EntityDamageEvent.DamageModifier;
 import org.bukkit.inventory.ItemStack;
+import org.bukkit.metadata.FixedMetadataValue;
 import org.bukkit.metadata.MetadataValue;
 import org.bukkit.potion.PotionEffectType;
 import org.bukkit.projectiles.ProjectileSource;
@@ -288,6 +291,7 @@ public final class CombatUtils {
 
         //Make sure the profiles been loaded
         if(mmoPlayer == null) {
+            cleanupArrowMetadata(arrow);
             return;
         }
 
@@ -326,8 +330,10 @@ public final class CombatUtils {
                 "Force Multiplier: "+forceMultiplier,
                 "Initial Damage: "+initialDamage,
                 "Final Damage: "+finalDamage);
-
         processCombatXP(mmoPlayer, target, PrimarySkillType.ARCHERY, distanceMultiplier);
+
+        //Clean data
+        cleanupArrowMetadata(arrow);
     }
 
     private static void processCrossbowCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event, Projectile arrow) {
@@ -499,19 +505,22 @@ public final class CombatUtils {
 
                 //Has metadata
                 if(arrow.getMetadata(mcMMO.PROJECTILE_ORIGIN_METAKEY).size() > 0) {
-                    if(isProjectileFromBow(arrow)) {
-                        if(PrimarySkillType.ARCHERY.shouldProcess(target)) {
+                    if (isProjectileFromBow(arrow)) {
+                        if (PrimarySkillType.ARCHERY.shouldProcess(target)) {
                             if (!Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.ARCHERY.getPermissions(player)) {
                                 processArcheryCombat(target, player, event, arrow);
                             }
                         }
-                    } else if(isProjectileFromCrossbow(arrow)) {
-                        if(PrimarySkillType.CROSSBOWS.shouldProcess(target)) {
+                    } else if (isProjectileFromCrossbow(arrow)) {
+                        if (PrimarySkillType.CROSSBOWS.shouldProcess(target)) {
                             if (!Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.CROSSBOWS.getPermissions(player)) {
                                 processCrossbowCombat(target, player, event, arrow);
                             }
                         }
                     }
+                } else {
+                    //Cleanup Arrow
+                    cleanupArrowMetadata(arrow);
                 }
 
                 if (target.getType() != EntityType.CREEPER && !Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.TAMING.getPermissions(player)) {
@@ -1122,4 +1131,32 @@ public final class CombatUtils {
             attributeInstance.setBaseValue(normalSpeed * multiplier);
         }
     }
+
+    /**
+     * Clean up metadata from a projectile
+     *
+     * @param entity projectile
+     */
+    public static void cleanupArrowMetadata(@NotNull Projectile entity) {
+        if(entity.hasMetadata(mcMMO.infiniteArrowKey)) {
+            entity.removeMetadata(mcMMO.infiniteArrowKey, mcMMO.p);
+        }
+
+        if(entity.hasMetadata(mcMMO.bowForceKey)) {
+            entity.removeMetadata(mcMMO.bowForceKey, mcMMO.p);
+        }
+
+        if(entity.hasMetadata(mcMMO.arrowDistanceKey)) {
+            entity.removeMetadata(mcMMO.arrowDistanceKey, mcMMO.p);
+        }
+    }
+
+    /**
+     * Clean up metadata from a projectile after a minute has passed
+     *
+     * @param entity the projectile
+     */
+    public static void delayArrowMetaCleanup(@NotNull Projectile entity) {
+        Bukkit.getServer().getScheduler().runTaskLater(mcMMO.p, () -> { cleanupArrowMetadata(entity);}, 20*60);
+    }
 }

+ 116 - 116
src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java

@@ -1,116 +1,116 @@
-package com.gmail.nossr50.util.random;
-
-import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
-import com.gmail.nossr50.datatypes.skills.SubSkillType;
-import com.gmail.nossr50.util.Permissions;
-import com.gmail.nossr50.util.player.UserManager;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-
-import static org.mockito.Mockito.mock;
-
-//TODO: Rewrite the entire com.gmail.nossr50.util.random package, it was written in haste and it disgusts me
-//TODO: Add more tests for the other types of random dice rolls
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({RandomChanceUtil.class, UserManager.class})
-public class RandomChanceTest {
-
-    private Player luckyPlayer;
-    private McMMOPlayer mmoPlayerLucky;
-
-    private Player normalPlayer;
-    private McMMOPlayer mmoPlayerNormal;
-
-    private SubSkillType subSkillType;
-    private PrimarySkillType primarySkillType;
-
-    private final String testASCIIHeader = "---- mcMMO Tests ----";
-
-    @Before
-    public void setUpMock() {
-        primarySkillType = PrimarySkillType.HERBALISM;
-        subSkillType = SubSkillType.HERBALISM_GREEN_THUMB;
-
-        //TODO: Likely needs to be changed per skill if more tests were added
-        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaximumProbability", subSkillType.getClass())).toReturn(100D);
-        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaxBonusLevelCap", subSkillType.getClass())).toReturn(1000D);
-
-        normalPlayer = mock(Player.class);
-        luckyPlayer = mock(Player.class);
-
-        mmoPlayerNormal = mock(McMMOPlayer.class);
-        mmoPlayerLucky = mock(McMMOPlayer.class);
-
-        PowerMockito.mockStatic(UserManager.class);
-        Mockito.when(UserManager.getPlayer(normalPlayer)).thenReturn(mmoPlayerNormal);
-        Mockito.when(UserManager.getPlayer(luckyPlayer)).thenReturn(mmoPlayerLucky);
-
-        Mockito.when(mmoPlayerNormal.getPlayer()).thenReturn(normalPlayer);
-        Mockito.when(mmoPlayerLucky.getPlayer()).thenReturn(luckyPlayer);
-
-        //Lucky player has the lucky permission
-        //Normal player doesn't have any lucky permission
-        Mockito.when(Permissions.lucky(luckyPlayer, primarySkillType)).thenReturn(true);
-        Mockito.when(Permissions.lucky(normalPlayer, primarySkillType)).thenReturn(false);
-
-        Mockito.when(mmoPlayerNormal.getSkillLevel(primarySkillType)).thenReturn(800);
-        Mockito.when(mmoPlayerLucky.getSkillLevel(primarySkillType)).thenReturn(800);
-    }
-
-    @Test
-    public void testLuckyChance() {
-        System.out.println(testASCIIHeader);
-        System.out.println("Testing success odds to fall within expected values...");
-        assertEquals(80D, getSuccessChance(mmoPlayerNormal),0D);
-        assertEquals(80D * RandomChanceUtil.LUCKY_MODIFIER, getSuccessChance(mmoPlayerLucky),0D);
-    }
-
-    @Test
-    public void testNeverFailsSuccessLuckyPlayer() {
-        System.out.println(testASCIIHeader);
-        System.out.println("Test - Lucky Player with 80% base success should never fail (10,000 iterations)");
-        for(int x = 0; x < 10000; x++) {
-            Assert.assertTrue(RandomChanceUtil.checkRandomChanceExecutionSuccess(luckyPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true));
-            if(x == 10000-1)
-                System.out.println("They never failed!");
-        }
-    }
-
-    @Test
-    public void testFailsAboutExpected() {
-        System.out.println(testASCIIHeader);
-        System.out.println("Test - Player with 800 skill should fail about 20% of the time (100,000 iterations)");
-        double ratioDivisor = 1000; //1000 because we run the test 100,000 times
-        double expectedFailRate = 20D;
-
-        double win = 0, loss = 0;
-        for(int x = 0; x < 100000; x++) {
-            if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true)) {
-                win++;
-            } else {
-                loss++;
-            }
-        }
-
-        double lossRatio = (loss / ratioDivisor);
-        Assert.assertEquals(lossRatio, expectedFailRate, 1D);
-    }
-
-    private double getSuccessChance(@NotNull McMMOPlayer mmoPlayer) {
-        RandomChanceSkill randomChanceSkill = new RandomChanceSkill(mmoPlayer.getPlayer(), subSkillType, true);
-        return RandomChanceUtil.calculateChanceOfSuccess(randomChanceSkill);
-    }
-
-    private void assertEquals(double expected, double actual, double delta) {
-        Assert.assertEquals(expected, actual, delta);
-    }
-}
+//package com.gmail.nossr50.util.random;
+//
+//import com.gmail.nossr50.datatypes.player.McMMOPlayer;
+//import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
+//import com.gmail.nossr50.datatypes.skills.SubSkillType;
+//import com.gmail.nossr50.util.Permissions;
+//import com.gmail.nossr50.util.player.UserManager;
+//import org.bukkit.entity.Player;
+//import org.jetbrains.annotations.NotNull;
+//import org.junit.Assert;
+//import org.junit.Before;
+//import org.junit.Test;
+//import org.junit.runner.RunWith;
+//import org.mockito.Mockito;
+//import org.powermock.api.mockito.PowerMockito;
+//import org.powermock.core.classloader.annotations.PrepareForTest;
+//import org.powermock.modules.junit4.PowerMockRunner;
+//
+//import static org.mockito.Mockito.mock;
+//
+////TODO: Rewrite the entire com.gmail.nossr50.util.random package, it was written in haste and it disgusts me
+////TODO: Add more tests for the other types of random dice rolls
+//@RunWith(PowerMockRunner.class)
+//@PrepareForTest({RandomChanceUtil.class, UserManager.class})
+//public class RandomChanceTest {
+//
+//    private Player luckyPlayer;
+//    private McMMOPlayer mmoPlayerLucky;
+//
+//    private Player normalPlayer;
+//    private McMMOPlayer mmoPlayerNormal;
+//
+//    private SubSkillType subSkillType;
+//    private PrimarySkillType primarySkillType;
+//
+//    private final String testASCIIHeader = "---- mcMMO Tests ----";
+//
+//    @Before
+//    public void setUpMock() {
+//        primarySkillType = PrimarySkillType.HERBALISM;
+//        subSkillType = SubSkillType.HERBALISM_GREEN_THUMB;
+//
+//        //TODO: Likely needs to be changed per skill if more tests were added
+//        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaximumProbability", subSkillType.getClass())).toReturn(100D);
+//        PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaxBonusLevelCap", subSkillType.getClass())).toReturn(1000D);
+//
+//        normalPlayer = mock(Player.class);
+//        luckyPlayer = mock(Player.class);
+//
+//        mmoPlayerNormal = mock(McMMOPlayer.class);
+//        mmoPlayerLucky = mock(McMMOPlayer.class);
+//
+//        PowerMockito.mockStatic(UserManager.class);
+//        Mockito.when(UserManager.getPlayer(normalPlayer)).thenReturn(mmoPlayerNormal);
+//        Mockito.when(UserManager.getPlayer(luckyPlayer)).thenReturn(mmoPlayerLucky);
+//
+//        Mockito.when(mmoPlayerNormal.getPlayer()).thenReturn(normalPlayer);
+//        Mockito.when(mmoPlayerLucky.getPlayer()).thenReturn(luckyPlayer);
+//
+//        //Lucky player has the lucky permission
+//        //Normal player doesn't have any lucky permission
+//        Mockito.when(Permissions.lucky(luckyPlayer, primarySkillType)).thenReturn(true);
+//        Mockito.when(Permissions.lucky(normalPlayer, primarySkillType)).thenReturn(false);
+//
+//        Mockito.when(mmoPlayerNormal.getSkillLevel(primarySkillType)).thenReturn(800);
+//        Mockito.when(mmoPlayerLucky.getSkillLevel(primarySkillType)).thenReturn(800);
+//    }
+//
+//    @Test
+//    public void testLuckyChance() {
+//        System.out.println(testASCIIHeader);
+//        System.out.println("Testing success odds to fall within expected values...");
+//        assertEquals(80D, getSuccessChance(mmoPlayerNormal),0D);
+//        assertEquals(80D * RandomChanceUtil.LUCKY_MODIFIER, getSuccessChance(mmoPlayerLucky),0D);
+//    }
+//
+//    @Test
+//    public void testNeverFailsSuccessLuckyPlayer() {
+//        System.out.println(testASCIIHeader);
+//        System.out.println("Test - Lucky Player with 80% base success should never fail (10,000 iterations)");
+//        for(int x = 0; x < 10000; x++) {
+//            Assert.assertTrue(RandomChanceUtil.checkRandomChanceExecutionSuccess(luckyPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true));
+//            if(x == 10000-1)
+//                System.out.println("They never failed!");
+//        }
+//    }
+//
+//    @Test
+//    public void testFailsAboutExpected() {
+//        System.out.println(testASCIIHeader);
+//        System.out.println("Test - Player with 800 skill should fail about 20% of the time (100,000 iterations)");
+//        double ratioDivisor = 1000; //1000 because we run the test 100,000 times
+//        double expectedFailRate = 20D;
+//
+//        double win = 0, loss = 0;
+//        for(int x = 0; x < 100000; x++) {
+//            if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true)) {
+//                win++;
+//            } else {
+//                loss++;
+//            }
+//        }
+//
+//        double lossRatio = (loss / ratioDivisor);
+//        Assert.assertEquals(lossRatio, expectedFailRate, 1D);
+//    }
+//
+//    private double getSuccessChance(@NotNull McMMOPlayer mmoPlayer) {
+//        RandomChanceSkill randomChanceSkill = new RandomChanceSkill(mmoPlayer.getPlayer(), subSkillType, true);
+//        return RandomChanceUtil.calculateChanceOfSuccess(randomChanceSkill);
+//    }
+//
+//    private void assertEquals(double expected, double actual, double delta) {
+//        Assert.assertEquals(expected, actual, delta);
+//    }
+//}