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.
     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.
     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.
     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
 Version 2.1.168
     Fixed an IndexOutOfBoundsException error when trying to access UserBlockTracker from an invalid range (thanks t00thpick1)
     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)
     (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)
         if(isSilent)
             return;
             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)
         if(isSilent)
             return;
             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));
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         }
         else {
         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)
         if(isSilent)
             return;
             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) {
     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) {
     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));
             sender.sendMessage(LocaleLoader.getString("Commands.addlevels.AwardAll.2", playerName));
         }
         }
         else {
         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);
             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) {
     public SkillCommand(@NotNull RootSkill rootSkill) {
         this.rootSkill = CoreSkills.getSkill(primarySkillType);
         this.rootSkill = CoreSkills.getSkill(primarySkillType);
         this.primarySkillType = primarySkillType;
         this.primarySkillType = primarySkillType;
-        skillName = rootSkill.getLocalizedName();
+        skillName = rootSkill.getName();
         skillGuideCommand = new SkillGuideCommand(rootSkill);
         skillGuideCommand = new SkillGuideCommand(rootSkill);
     }
     }
 
 
@@ -177,10 +177,10 @@ public abstract class SkillCommand implements TabExecutor {
             {
             {
                 if(i+1 < parentList.size())
                 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(", ");
                     parentMessage.append(ChatColor.GRAY).append(", ");
                 } else {
                 } 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) {
     public SkillGuideCommand(@NotNull RootSkill rootSkill) {
         this.rootSkill = rootSkill;
         this.rootSkill = rootSkill;
-        header = LocaleLoader.getString("Guides.Header", rootSkill.getLocalizedName());
+        header = LocaleLoader.getString("Guides.Header", rootSkill.getName());
         guide = getGuide(rootSkill);
         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);
                 nonChildSkills.add(skill);
             }
             }
 
 
-            for(SubSkillType subSkillType : skill.subSkillTypes)
-            {
+            for(SubSkillType subSkillType : skill.subSkillTypes) {
                 subSkillNames.add(subSkillType.getNiceNameNoSpaces(subSkillType));
                 subSkillNames.add(subSkillType.getNiceNameNoSpaces(subSkillType));
             }
             }
+
             names.add(skill.getName());
             names.add(skill.getName());
         }
         }
 
 
@@ -234,13 +234,13 @@ public enum PrimarySkillType {
         return null;
         return null;
     }
     }
 
 
-    public String getLocalizedName() {
+    public String getName() {
         return StringUtils.getCapitalized(LocaleLoader.getString(StringUtils.getCapitalized(this.toString()) + ".SkillName"));
         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) {
     public boolean getPermissions(Player player) {
         return Permissions.skillEnabled(player, this);
         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))
                 if(!WorldGuardManager.getInstance().hasMainFlag(player))
                     return;
                     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)
     @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
@@ -169,6 +171,8 @@ public class EntityListener implements Listener {
             EntityType entityType = projectile.getType();
             EntityType entityType = projectile.getType();
 
 
             if (entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) {
             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))
                 if (!projectile.hasMetadata(mcMMO.bowForceKey))
                     projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, 1.0));
                     projectile.setMetadata(mcMMO.bowForceKey, new FixedMetadataValue(pluginRef, 1.0));
 
 
@@ -244,7 +248,6 @@ public class EntityListener implements Listener {
         Entity entity = event.getEntity();
         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
         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
         // When the event is fired for the falling block that changes back to a
         // normal block
         // normal block
         // event.getBlock().getType() returns AIR
         // event.getBlock().getType() returns AIR
@@ -463,13 +466,14 @@ public class EntityListener implements Listener {
             LivingEntity livingEntity = (LivingEntity) entityDamageEvent.getEntity();
             LivingEntity livingEntity = (LivingEntity) entityDamageEvent.getEntity();
 
 
             if(entityDamageEvent.getFinalDamage() >= livingEntity.getHealth()) {
             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);
                 CombatUtils.fixNames(livingEntity);
-                }
             }
             }
+        }
+
+        if(entityDamageEvent.getDamager() instanceof Projectile) {
+            CombatUtils.cleanupArrowMetadata((Projectile) entityDamageEvent.getDamager());
+        }
     }
     }
 
 
     public boolean checkParties(Cancellable event, Player defendingPlayer, Player attackingPlayer) {
     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?
             // Do these ACTUALLY have to be lower case to work properly?
             for (PrimarySkillType skill : PrimarySkillType.values()) {
             for (PrimarySkillType skill : PrimarySkillType.values()) {
                 String skillName = skill.toString().toLowerCase(Locale.ENGLISH);
                 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)) {
                 if (lowerCaseCommand.equals(localizedName)) {
                     event.setMessage(message.replace(command, skillName));
                     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);
             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);
         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 {
         else {
             if(sender instanceof Player) {
             if(sender instanceof Player) {
-                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getLocalizedName()));
+                sender.sendMessage(LocaleLoader.getString("Commands.Skill.Leaderboard", skill.getName()));
             }
             }
             else {
             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) {
     public double distanceXpBonusMultiplier(@NotNull LivingEntity target, @NotNull Entity arrow) {
         //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires
         //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires
         if(!arrow.hasMetadata(mcMMO.arrowDistanceKey))
         if(!arrow.hasMetadata(mcMMO.arrowDistanceKey))
-            return arrow.getLocation().distance(target.getLocation());
-
+            return 1;
 
 
         Location firedLocation = (Location) arrow.getMetadata(mcMMO.arrowDistanceKey).get(0).value();
         Location firedLocation = (Location) arrow.getMetadata(mcMMO.arrowDistanceKey).get(0).value();
         Location targetLocation = target.getLocation();
         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;
             int convertedLureBonus = 0;
 
 
             //This avoids a Minecraft bug where lure levels above 3 break fishing
             //This avoids a Minecraft bug where lure levels above 3 break fishing
-            if(lureLevel > 3) {
+            if(lureLevel > 0) {
                 masterAnglerCompatibilityLayer.setApplyLure(fishHook, false);
                 masterAnglerCompatibilityLayer.setApplyLure(fishHook, false);
                 convertedLureBonus = lureLevel * 100;
                 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() {
     private static void registerSkillCommands() {
         for (PrimarySkillType skill : PrimarySkillType.values()) {
         for (PrimarySkillType skill : PrimarySkillType.values()) {
             String commandName = skill.toString().toLowerCase(Locale.ENGLISH);
             String commandName = skill.toString().toLowerCase(Locale.ENGLISH);
-            String localizedName = skill.getLocalizedName().toLowerCase(Locale.ENGLISH);
+            String localizedName = skill.getName().toLowerCase(Locale.ENGLISH);
 
 
             PluginCommand command;
             PluginCommand command;
 
 

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

@@ -94,7 +94,7 @@ public class ScoreboardManager {
             int i = 0;
             int i = 0;
             for (PrimarySkillType type : PrimarySkillType.values()) {
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
                 // 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) {
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), getShortenedName(colors.get(i) + type.getSuperAbilityType().getLocalizedName()));
                     abilityLabelBuilder.put(type.getSuperAbilityType(), getShortenedName(colors.get(i) + type.getSuperAbilityType().getLocalizedName()));
@@ -116,7 +116,7 @@ public class ScoreboardManager {
         else {
         else {
             for (PrimarySkillType type : PrimarySkillType.values()) {
             for (PrimarySkillType type : PrimarySkillType.values()) {
                 // Include child skills
                 // Include child skills
-                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getLocalizedName()));
+                skillLabelBuilder.put(type, getShortenedName(ChatColor.GREEN + type.getName()));
 
 
                 if (type.getSuperAbilityType() != null) {
                 if (type.getSuperAbilityType() != null) {
                     abilityLabelBuilder.put(type.getSuperAbilityType(), formatAbility(type.getSuperAbilityType().getLocalizedName()));
                     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.compat.layers.persistentdata.MobMetaFlagType;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.gmail.nossr50.util.player.NotificationManager;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap;
+import org.bukkit.Bukkit;
 import org.bukkit.GameMode;
 import org.bukkit.GameMode;
 import org.bukkit.Material;
 import org.bukkit.Material;
 import org.bukkit.attribute.Attribute;
 import org.bukkit.attribute.Attribute;
 import org.bukkit.attribute.AttributeInstance;
 import org.bukkit.attribute.AttributeInstance;
+import org.bukkit.enchantments.Enchantment;
 import org.bukkit.entity.*;
 import org.bukkit.entity.*;
 import org.bukkit.event.entity.EntityDamageByEntityEvent;
 import org.bukkit.event.entity.EntityDamageByEntityEvent;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.event.entity.EntityDamageEvent;
 import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
 import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
 import org.bukkit.event.entity.EntityDamageEvent.DamageModifier;
 import org.bukkit.event.entity.EntityDamageEvent.DamageModifier;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.ItemStack;
+import org.bukkit.metadata.FixedMetadataValue;
 import org.bukkit.metadata.MetadataValue;
 import org.bukkit.metadata.MetadataValue;
 import org.bukkit.potion.PotionEffectType;
 import org.bukkit.potion.PotionEffectType;
 import org.bukkit.projectiles.ProjectileSource;
 import org.bukkit.projectiles.ProjectileSource;
@@ -288,6 +291,7 @@ public final class CombatUtils {
 
 
         //Make sure the profiles been loaded
         //Make sure the profiles been loaded
         if(mmoPlayer == null) {
         if(mmoPlayer == null) {
+            cleanupArrowMetadata(arrow);
             return;
             return;
         }
         }
 
 
@@ -326,8 +330,10 @@ public final class CombatUtils {
                 "Force Multiplier: "+forceMultiplier,
                 "Force Multiplier: "+forceMultiplier,
                 "Initial Damage: "+initialDamage,
                 "Initial Damage: "+initialDamage,
                 "Final Damage: "+finalDamage);
                 "Final Damage: "+finalDamage);
-
         processCombatXP(mmoPlayer, target, PrimarySkillType.ARCHERY, distanceMultiplier);
         processCombatXP(mmoPlayer, target, PrimarySkillType.ARCHERY, distanceMultiplier);
+
+        //Clean data
+        cleanupArrowMetadata(arrow);
     }
     }
 
 
     private static void processCrossbowCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event, Projectile arrow) {
     private static void processCrossbowCombat(LivingEntity target, Player player, EntityDamageByEntityEvent event, Projectile arrow) {
@@ -499,19 +505,22 @@ public final class CombatUtils {
 
 
                 //Has metadata
                 //Has metadata
                 if(arrow.getMetadata(mcMMO.PROJECTILE_ORIGIN_METAKEY).size() > 0) {
                 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)) {
                             if (!Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.ARCHERY.getPermissions(player)) {
                                 processArcheryCombat(target, player, event, arrow);
                                 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)) {
                             if (!Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.CROSSBOWS.getPermissions(player)) {
                                 processCrossbowCombat(target, player, event, arrow);
                                 processCrossbowCombat(target, player, event, arrow);
                             }
                             }
                         }
                         }
                     }
                     }
+                } else {
+                    //Cleanup Arrow
+                    cleanupArrowMetadata(arrow);
                 }
                 }
 
 
                 if (target.getType() != EntityType.CREEPER && !Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.TAMING.getPermissions(player)) {
                 if (target.getType() != EntityType.CREEPER && !Misc.isNPCEntityExcludingVillagers(player) && PrimarySkillType.TAMING.getPermissions(player)) {
@@ -1122,4 +1131,32 @@ public final class CombatUtils {
             attributeInstance.setBaseValue(normalSpeed * multiplier);
             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);
+//    }
+//}