Browse Source

Pull changes from dev-dbman (commit f63c5e3)

riking 12 years ago
parent
commit
23729f45ee
21 changed files with 968 additions and 601 deletions
  1. 2 0
      Changelog.txt
  2. 2 1
      src/main/java/com/gmail/nossr50/api/ExperienceAPI.java
  3. 1 2
      src/main/java/com/gmail/nossr50/commands/database/McremoveCommand.java
  4. 42 0
      src/main/java/com/gmail/nossr50/commands/database/MmoshowdbCommand.java
  5. 101 12
      src/main/java/com/gmail/nossr50/commands/database/MmoupdateCommand.java
  6. 2 1
      src/main/java/com/gmail/nossr50/commands/experience/ExperienceCommand.java
  7. 1 2
      src/main/java/com/gmail/nossr50/commands/experience/SkillresetCommand.java
  8. 2 1
      src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java
  9. 1 2
      src/main/java/com/gmail/nossr50/commands/player/McrankCommand.java
  10. 17 7
      src/main/java/com/gmail/nossr50/database/DatabaseManager.java
  11. 57 0
      src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java
  12. 441 283
      src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java
  13. 206 174
      src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java
  14. 2 2
      src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java
  15. 21 70
      src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java
  16. 1 0
      src/main/java/com/gmail/nossr50/runnables/PlayerUpdateInventoryTask.java
  17. 41 0
      src/main/java/com/gmail/nossr50/runnables/database/ConversionTask.java
  18. 0 37
      src/main/java/com/gmail/nossr50/runnables/database/SQLConversionTask.java
  19. 13 2
      src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java
  20. 9 4
      src/main/resources/locale/locale_en_US.properties
  21. 6 1
      src/main/resources/plugin.yml

+ 2 - 0
Changelog.txt

@@ -29,6 +29,8 @@ Version 1.4.06-dev
  + Added information about /party itemshare and /party expshare to the party help page
  + Added option to use scoreboards for power level display instead of Spout.
  + Added permission node to prevent inspecting hidden players
+ + Added SQL to Flatfile database conversion
+ + Added ability to use custom database managers and convert to/from them
  = Fixed bug which could cause the server to hang for a minute when checking for updates. (Thanks to Riking)
  = Fixed bug where spawned arrows could throw ArrayIndexOutOfBoundsException
  = Fixed bug where custom Spout titles were overwritten by mcMMO.

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

@@ -4,6 +4,7 @@ import java.util.Set;
 
 import org.bukkit.entity.Player;
 
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.api.exceptions.InvalidPlayerException;
 import com.gmail.nossr50.api.exceptions.InvalidSkillException;
 import com.gmail.nossr50.config.Config;
@@ -552,7 +553,7 @@ public final class ExperienceAPI {
     }
 
     private static PlayerProfile getOfflineProfile(String playerName) {
-        PlayerProfile profile = new PlayerProfile(playerName, false);
+        PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, false);
 
         if (!profile.isLoaded()) {
             throw new InvalidPlayerException();

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

@@ -10,7 +10,6 @@ import org.bukkit.command.TabExecutor;
 import org.bukkit.util.StringUtil;
 
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.locale.LocaleLoader;
 import com.gmail.nossr50.util.commands.CommandUtils;
 import com.gmail.nossr50.util.player.UserManager;
@@ -22,7 +21,7 @@ public class McremoveCommand implements TabExecutor {
     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
         switch (args.length) {
             case 1:
-                if (UserManager.getPlayer(args[0]) == null && CommandUtils.unloadedProfile(sender, new PlayerProfile(args[0], false))) {
+                if (UserManager.getPlayer(args[0]) == null && CommandUtils.unloadedProfile(sender, mcMMO.getDatabaseManager().loadPlayerProfile(args[0], false))) {
                     return true;
                 }
 

+ 42 - 0
src/main/java/com/gmail/nossr50/commands/database/MmoshowdbCommand.java

@@ -0,0 +1,42 @@
+package com.gmail.nossr50.commands.database;
+
+import java.util.List;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+
+import com.gmail.nossr50.config.Config;
+import com.gmail.nossr50.database.DatabaseManagerFactory;
+import com.gmail.nossr50.locale.LocaleLoader;
+import com.google.common.collect.ImmutableList;
+
+public class MmoshowdbCommand implements TabExecutor {
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        if (args.length != 0) {
+            return false;
+        }
+        else {
+            Class<?> clazz = DatabaseManagerFactory.getCustomDatabaseManagerClass();
+            if (clazz != null) {
+                sender.sendMessage(LocaleLoader.getString("Commands.mmoshowdb", clazz.getName()));
+                return true;
+            }
+            else {
+                if (Config.getInstance().getUseMySQL()) {
+                    sender.sendMessage(LocaleLoader.getString("Commands.mmoshowdb", "sql"));
+                }
+                else {
+                    sender.sendMessage(LocaleLoader.getString("Commands.mmoshowdb", "flatfile"));
+                }
+                return true;
+            }
+        }
+    }
+
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        return ImmutableList.of();
+    }
+}

+ 101 - 12
src/main/java/com/gmail/nossr50/commands/database/MmoupdateCommand.java

@@ -9,41 +9,130 @@ import org.bukkit.entity.Player;
 
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.config.Config;
+import com.gmail.nossr50.database.DatabaseManager;
+import com.gmail.nossr50.database.DatabaseManagerFactory;
+import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.locale.LocaleLoader;
-import com.gmail.nossr50.runnables.database.SQLConversionTask;
+import com.gmail.nossr50.runnables.database.ConversionTask;
 import com.gmail.nossr50.util.player.UserManager;
 
 import com.google.common.collect.ImmutableList;
 
 public class MmoupdateCommand implements TabExecutor {
+
     @Override
     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
-        if (!Config.getInstance().getUseMySQL()) {
-            sender.sendMessage("SQL Mode is not enabled."); // TODO: Localize
-            return true;
-        }
-
         switch (args.length) {
-            case 0:
-                sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.Start"));
+            case 1:
+                String argType = args[0];
+                String oldType = validateName(sender, args[0]);
+                if (oldType == null) {
+                    return true;
+                }
+
+                String newType = getCurrentDb();
+
+                if (newType.equals(oldType)) {
+                    sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.Same", argType));
+                    return true;
+                }
+
+                DatabaseManager oldDb;
+                if (oldType == "sql") {
+                    oldDb = DatabaseManagerFactory.createSQLDatabaseManager();
+                }
+                else if (oldType == "flatfile") {
+                    oldDb = DatabaseManagerFactory.createFlatfileDatabaseManager();
+                }
+                else try {
+                    @SuppressWarnings("unchecked")
+                    Class<? extends DatabaseManager> clazz = (Class<? extends DatabaseManager>) Class.forName(oldType);
+                    oldDb = DatabaseManagerFactory.createCustomDatabaseManager((Class<? extends DatabaseManager>) clazz);
+
+                    oldType = clazz.getSimpleName(); // For pretty-printing; we have the database now
+                }
+                catch (Throwable e) {
+                    return false;
+                }
+
+                sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.Start", oldType, newType));
+
+                // Convert the online players right away, without waiting
+                // first, flush out the current data
                 UserManager.saveAll();
                 UserManager.clearAll();
-                new SQLConversionTask().runTaskLaterAsynchronously(mcMMO.p, 1);
 
                 for (Player player : mcMMO.p.getServer().getOnlinePlayers()) {
+                    // Get the profile from the old database and save it in the new
+                    PlayerProfile profile = oldDb.loadPlayerProfile(player.getName(), false);
+                    if (profile.isLoaded()) {
+                        mcMMO.getDatabaseManager().saveUser(profile);
+                    }
+
+                    // Reload from the current database via UserManager
                     UserManager.addUser(player);
                 }
 
-                sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.Finish"));
+                // Schedule the task for all users
+                new ConversionTask(oldDb, sender, oldType, newType).runTaskAsynchronously(mcMMO.p);
+
                 return true;
 
             default:
-                return false;
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * @return null - if type not recognized / class not found
+     *         empty string - if type is same as current
+     *         normalized string - if type is recognized
+     */
+    private String validateName(CommandSender sender, String type) {
+        if (type.equalsIgnoreCase("sql") || type.equalsIgnoreCase("mysql")) {
+            return "sql";
+        }
+
+        if (type.equalsIgnoreCase("flatfile") || type.equalsIgnoreCase("file")) {
+            return "flatfile";
+        }
+
+        try {
+            Class<?> clazz = Class.forName(type);
+
+            if (!DatabaseManager.class.isAssignableFrom(clazz)) {
+                sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.InvalidType", type));
+                return null;
+            }
+
+            return type;
+        }
+        catch (Exception e) {
+            sender.sendMessage(LocaleLoader.getString("Commands.mmoupdate.InvalidType", type));
+            return null;
+        }
+    }
+
+    private String getCurrentDb() {
+        if (DatabaseManagerFactory.getCustomDatabaseManagerClass() != null) {
+            return DatabaseManagerFactory.getCustomDatabaseManagerClass().getSimpleName();
+        }
+
+        if (Config.getInstance().getUseMySQL()) {
+            return "sql";
+        }
+        else {
+            return "flatfile";
         }
     }
 
     @Override
     public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
-        return ImmutableList.of();
+        Class<?> clazz = DatabaseManagerFactory.getCustomDatabaseManagerClass();
+        if (clazz != null) {
+            return ImmutableList.of("flatfile", "sql", clazz.getName());
+        }
+        return ImmutableList.of("flatfile", "sql");
     }
 }

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

@@ -10,6 +10,7 @@ import org.bukkit.command.TabExecutor;
 import org.bukkit.entity.Player;
 import org.bukkit.util.StringUtil;
 
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.skills.SkillType;
@@ -68,7 +69,7 @@ public abstract class ExperienceCommand implements TabExecutor {
 
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 if (mcMMOPlayer == null) {
-                    profile = new PlayerProfile(args[0], false);
+                    profile = mcMMO.getDatabaseManager().loadPlayerProfile(args[0], false);
 
                     if (CommandUtils.unloadedProfile(sender, profile)) {
                         return true;

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

@@ -5,7 +5,6 @@ import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 
 import com.gmail.nossr50.mcMMO;
-import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.datatypes.skills.SkillType;
 import com.gmail.nossr50.events.experience.McMMOPlayerLevelUpEvent;
 import com.gmail.nossr50.locale.LocaleLoader;
@@ -61,7 +60,7 @@ public class SkillresetCommand extends ExperienceCommand {
 
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 if (mcMMOPlayer == null) {
-                    profile = new PlayerProfile(args[0], false);
+                    profile = mcMMO.getDatabaseManager().loadPlayerProfile(args[0], false);
 
                     if (CommandUtils.unloadedProfile(sender, profile)) {
                         return true;

+ 2 - 1
src/main/java/com/gmail/nossr50/commands/player/InspectCommand.java

@@ -10,6 +10,7 @@ import org.bukkit.command.TabExecutor;
 import org.bukkit.entity.Player;
 import org.bukkit.util.StringUtil;
 
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
@@ -35,7 +36,7 @@ public class InspectCommand implements TabExecutor {
 
                 // If the mcMMOPlayer doesn't exist, create a temporary profile and check if it's present in the database. If it's not, abort the process.
                 if (mcMMOPlayer == null) {
-                    PlayerProfile profile = new PlayerProfile(args[0], false); // Temporary Profile
+                    PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(args[0], false); // Temporary Profile
 
                     if (CommandUtils.inspectOffline(sender, profile, Permissions.inspectOffline(sender))) {
                         return true;

+ 1 - 2
src/main/java/com/gmail/nossr50/commands/player/McrankCommand.java

@@ -13,7 +13,6 @@ import org.bukkit.util.StringUtil;
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.config.Config;
 import com.gmail.nossr50.datatypes.player.McMMOPlayer;
-import com.gmail.nossr50.datatypes.player.PlayerProfile;
 import com.gmail.nossr50.runnables.commands.McrankCommandAsyncTask;
 import com.gmail.nossr50.util.Permissions;
 import com.gmail.nossr50.util.commands.CommandUtils;
@@ -61,7 +60,7 @@ public class McrankCommand implements TabExecutor {
                         return true;
                     }
                 }
-                else if (CommandUtils.inspectOffline(sender, new PlayerProfile(playerName, false), Permissions.mcrankOffline(sender))) {
+                else if (CommandUtils.inspectOffline(sender, mcMMO.getDatabaseManager().loadPlayerProfile(playerName, false), Permissions.mcrankOffline(sender))) {
                     return true;
                 }
 

+ 17 - 7
src/main/java/com/gmail/nossr50/database/DatabaseManager.java

@@ -8,6 +8,7 @@ import com.gmail.nossr50.datatypes.database.PlayerStat;
 import com.gmail.nossr50.datatypes.player.PlayerProfile;
 
 public interface DatabaseManager {
+    // One month in milliseconds
     public final long PURGE_TIME = 2630000000L * Config.getInstance().getOldUsersCutoff();
 
     /**
@@ -64,16 +65,25 @@ public interface DatabaseManager {
      * Load a player from the database.
      *
      * @param playerName The name of the player to load from the database
-     * @return The player's data
+     * @param createNew Whether to create a new record if the player is not
+     *          found
+     * @return The player's data, or an unloaded PlayerProfile if not found
+     *          and createNew is false
      */
-    public List<String> loadPlayerData(String playerName);
+    public PlayerProfile loadPlayerProfile(String playerName, boolean createNew);
 
     /**
-     * Convert player data to a different storage format.
+     * Get all users currently stored in the database.
      *
-     * @param data The player's data
-     * @return true if the conversion was successful, false otherwise
-     * @throws Exception
+     * @return list of playernames
      */
-    public boolean convert(String[] data) throws Exception;
+    public List<String> getStoredUsers();
+
+    /**
+     * Convert all users from this database to the provided database using
+     * {@link #saveUser(PlayerProfile)}.
+     *
+     * @param the DatabaseManager to save to
+     */
+    public void convertUsers(DatabaseManager destination);
 }

+ 57 - 0
src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java

@@ -1,9 +1,66 @@
 package com.gmail.nossr50.database;
 
+import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.config.Config;
 
 public class DatabaseManagerFactory {
+    private static Class<? extends DatabaseManager> customManager = null;
+
     public static DatabaseManager getDatabaseManager() {
+        if (customManager != null) {
+            try {
+                return createCustomDatabaseManager(customManager);
+            } catch (Exception e) {
+                mcMMO.p.debug("Could not create custom database manager");
+                e.printStackTrace();
+            } catch (Throwable e) {
+                mcMMO.p.debug("Failed to create custom database manager");
+                e.printStackTrace();
+            }
+            mcMMO.p.debug("Falling back on " + (Config.getInstance().getUseMySQL() ? "SQL" : "Flatfile") + " database");
+        }
         return Config.getInstance().getUseMySQL() ? new SQLDatabaseManager() : new FlatfileDatabaseManager();
     }
+
+    /**
+     * Sets the custom DatabaseManager class for McMMO to use. This should be
+     * called prior to mcMMO enabling.
+     * <p>
+     * The provided class must have an empty constructor, which is the one
+     * that will be used.
+     * <p>
+     * This method is intended for API use, but it should not be considered
+     * stable. This method is subject to change and/or removal in future
+     * versions.
+     *
+     * @param man the DatabaseManager class to use
+     * @throws IllegalArgumentException if the provided class does not have
+     *             an empty constructor
+     */
+    public static void setCustomDatabaseManagerClass(Class<? extends DatabaseManager> clazz) {
+        try {
+            clazz.getConstructor((Class<?>) null);
+        } catch (Throwable e) {
+            throw new IllegalArgumentException("Provided database manager class must have an empty constructor", e);
+        }
+        customManager = clazz;
+    }
+
+    public static Class<? extends DatabaseManager> getCustomDatabaseManagerClass() {
+        return customManager;
+    }
+
+    // For data conversion purposes
+
+    public static FlatfileDatabaseManager createFlatfileDatabaseManager() {
+        return new FlatfileDatabaseManager();
+    }
+
+    public static SQLDatabaseManager createSQLDatabaseManager() {
+        return new SQLDatabaseManager();
+    }
+
+    public static DatabaseManager createCustomDatabaseManager(Class<? extends DatabaseManager> clazz) throws Throwable {
+        return customManager.getConstructor((Class<?>) null).newInstance((Object[]) null);
+    }
 }

+ 441 - 283
src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java

@@ -2,6 +2,7 @@ package com.gmail.nossr50.database;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
+import java.io.Closeable;
 import java.io.File;
 import java.io.FileReader;
 import java.io.FileWriter;
@@ -13,6 +14,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.bukkit.Bukkit;
 import org.bukkit.OfflinePlayer;
 
 import com.gmail.nossr50.mcMMO;
@@ -32,6 +34,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
 
     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());
@@ -44,9 +47,49 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
 
         mcMMO.p.getLogger().info("Purging powerless users...");
 
-        for (PlayerStat stat : powerLevels) {
-            if (stat.statVal == 0 && mcMMO.p.getServer().getPlayerExact(stat.name) == null && removeUser(stat.name)) {
-                purgedUsers++;
+        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<SkillType, 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++;
+                        Misc.profileCleanup(character[0]);
+                    }
+                }
+
+                // 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 {
+                tryClose(in);
+                tryClose(out);
             }
         }
 
@@ -59,9 +102,46 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
 
         mcMMO.p.getLogger().info("Purging old users...");
 
-        for (OfflinePlayer player : mcMMO.p.getServer().getOfflinePlayers()) {
-            if (!player.isOnline() && (currentTime - player.getLastPlayed() > PURGE_TIME && removeUser(player.getName()))) {
-                removedPlayers++;
+
+        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[0];
+                    OfflinePlayer player = Bukkit.getOfflinePlayer(name);
+                    boolean old = true;
+                    if (player != null) {
+                        old = (currentTime - player.getLastPlayed()) > PURGE_TIME;
+                    }
+
+                    if (!old) {
+                        writer.append(line).append("\r\n");
+                    }
+                    else {
+                        removedPlayers++;
+                        Misc.profileCleanup(name);
+                    }
+                }
+
+                // 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 {
+                tryClose(in);
+                tryClose(out);
             }
         }
 
@@ -75,45 +155,32 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
         FileWriter out = null;
         String usersFilePath = mcMMO.getUsersFilePath();
 
-        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(":")[0].equalsIgnoreCase(playerName)) {
-                    mcMMO.p.getLogger().info("User found, removing...");
-                    worked = true;
-                    continue; // Skip the player
-                }
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line = "";
 
-                writer.append(line).append("\r\n");
-            }
+                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(":")[0].equalsIgnoreCase(playerName)) {
+                        mcMMO.p.getLogger().info("User found, removing...");
+                        worked = true;
+                        continue; // Skip the player
+                    }
 
-            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 ex) {
-                    ex.printStackTrace();
+                    writer.append(line).append("\r\n");
                 }
-            }
 
-            if (out != null) {
-                try {
-                    out.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
+                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 {
+                tryClose(in);
+                tryClose(out);
             }
         }
 
@@ -129,89 +196,76 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
         FileWriter out = null;
         String usersFilePath = mcMMO.getUsersFilePath();
 
-        try {
-            // Open the file
-            in = new BufferedReader(new FileReader(usersFilePath));
-            StringBuilder writer = new StringBuilder();
-            String line;
-
-            // While not at the end of the file
-            while ((line = in.readLine()) != null) {
-                // Read the line in and copy it to the output it's not the player we want to edit
-                if (!line.split(":")[0].equalsIgnoreCase(playerName)) {
-                    writer.append(line).append("\r\n");
-                }
-                else {
-                    // Otherwise write the new player information
-                    writer.append(playerName).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.MINING)).append(":");
-                    writer.append(":");
-                    writer.append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.MINING)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.WOODCUTTING)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.WOODCUTTING)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.REPAIR)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.UNARMED)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.HERBALISM)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.EXCAVATION)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.ARCHERY)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.SWORDS)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.AXES)).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.ACROBATICS)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.REPAIR)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.UNARMED)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.HERBALISM)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.EXCAVATION)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.ARCHERY)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.SWORDS)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.AXES)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.ACROBATICS)).append(":");
-                    writer.append(":");
-                    writer.append(profile.getSkillLevel(SkillType.TAMING)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.TAMING)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.BERSERK)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.GIGA_DRILL_BREAKER)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.TREE_FELLER)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.GREEN_TERRA)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.SERRATED_STRIKES)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.SKULL_SPLITTER)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.SUPER_BREAKER)).append(":");
-                    HudType hudType = profile.getHudType();
-                    writer.append(hudType == null ? "STANDARD" : hudType.toString()).append(":");
-                    writer.append(profile.getSkillLevel(SkillType.FISHING)).append(":");
-                    writer.append(profile.getSkillXpLevel(SkillType.FISHING)).append(":");
-                    writer.append((int) profile.getSkillDATS(AbilityType.BLAST_MINING)).append(":");
-                    writer.append(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR).append(":");
-                    MobHealthbarType mobHealthbarType = profile.getMobHealthbarType();
-                    writer.append(mobHealthbarType == null ? Config.getInstance().getMobHealthbarDefault().toString() : mobHealthbarType.toString()).append(":");
-                    writer.append("\r\n");
+        synchronized (fileWritingLock) {
+            try {
+                // Open the file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                StringBuilder writer = new StringBuilder();
+                String line;
+
+                // While not at the end of the file
+                while ((line = in.readLine()) != null) {
+                    // Read the line in and copy it to the output it's not the player we want to edit
+                    if (!line.split(":")[0].equalsIgnoreCase(playerName)) {
+                        writer.append(line).append("\r\n");
+                    }
+                    else {
+                        // Otherwise write the new player information
+                        writer.append(playerName).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.MINING)).append(":");
+                        writer.append(":");
+                        writer.append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.MINING)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.WOODCUTTING)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.WOODCUTTING)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.REPAIR)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.UNARMED)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.HERBALISM)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.EXCAVATION)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.ARCHERY)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.SWORDS)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.AXES)).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.ACROBATICS)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.REPAIR)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.UNARMED)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.HERBALISM)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.EXCAVATION)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.ARCHERY)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.SWORDS)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.AXES)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.ACROBATICS)).append(":");
+                        writer.append(":");
+                        writer.append(profile.getSkillLevel(SkillType.TAMING)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.TAMING)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.BERSERK)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.GIGA_DRILL_BREAKER)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.TREE_FELLER)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.GREEN_TERRA)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.SERRATED_STRIKES)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.SKULL_SPLITTER)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.SUPER_BREAKER)).append(":");
+                        HudType hudType = profile.getHudType();
+                        writer.append(hudType == null ? "STANDARD" : hudType.toString()).append(":");
+                        writer.append(profile.getSkillLevel(SkillType.FISHING)).append(":");
+                        writer.append(profile.getSkillXpLevel(SkillType.FISHING)).append(":");
+                        writer.append((int) profile.getSkillDATS(AbilityType.BLAST_MINING)).append(":");
+                        writer.append(System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR).append(":");
+                        MobHealthbarType mobHealthbarType = profile.getMobHealthbarType();
+                        writer.append(mobHealthbarType == null ? Config.getInstance().getMobHealthbarDefault().toString() : mobHealthbarType.toString()).append(":");
+                        writer.append("\r\n");
+                    }
                 }
-            }
 
-            // Write the new file
-            out = new FileWriter(usersFilePath);
-            out.write(writer.toString());
-        }
-        catch (Exception e) {
-            e.printStackTrace();
-        }
-        finally {
-            if (in != null) {
-                try {
-                    in.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
+                // Write the new file
+                out = new FileWriter(usersFilePath);
+                out.write(writer.toString());
             }
-
-            if (out != null) {
-                try {
-                    out.close();
-                }
-                catch (IOException ex) {
-                    ex.printStackTrace();
-                }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                tryClose(in);
+                tryClose(out);
             }
         }
     }
@@ -239,145 +293,168 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
     }
 
     public void newUser(String playerName) {
-        try {
-            // Open the file to write the player
-            BufferedWriter out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true));
-
-            // Add the player to the end
-            out.append(playerName).append(":");
-            out.append("0:"); // Mining
-            out.append(":");
-            out.append(":");
-            out.append("0:"); // Xp
-            out.append("0:"); // Woodcutting
-            out.append("0:"); // WoodCuttingXp
-            out.append("0:"); // Repair
-            out.append("0:"); // Unarmed
-            out.append("0:"); // Herbalism
-            out.append("0:"); // Excavation
-            out.append("0:"); // Archery
-            out.append("0:"); // Swords
-            out.append("0:"); // Axes
-            out.append("0:"); // 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("0:"); // 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("STANDARD").append(":"); // HUD
-            out.append("0:"); // 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
-
-            // Add more in the same format as the line above
-
-            out.newLine();
-            out.close();
-        }
-        catch (Exception e) {
-            e.printStackTrace();
+        BufferedWriter out = null;
+        synchronized (fileWritingLock) {
+            try {
+                // Open the file to write the player
+                out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true));
+
+                // Add the player to the end
+                out.append(playerName).append(":");
+                out.append("0:"); // Mining
+                out.append(":");
+                out.append(":");
+                out.append("0:"); // Xp
+                out.append("0:"); // Woodcutting
+                out.append("0:"); // WoodCuttingXp
+                out.append("0:"); // Repair
+                out.append("0:"); // Unarmed
+                out.append("0:"); // Herbalism
+                out.append("0:"); // Excavation
+                out.append("0:"); // Archery
+                out.append("0:"); // Swords
+                out.append("0:"); // Axes
+                out.append("0:"); // 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("0:"); // 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("STANDARD").append(":"); // HUD
+                out.append("0:"); // 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
+
+                // Add more in the same format as the line above
+
+                out.newLine();
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                tryClose(out);
+            }
         }
     }
 
-    public List<String> loadPlayerData(String playerName) {
-        List<String> playerData = new ArrayList<String>();
-        try {
-            // Open the user file
-            FileReader file = new FileReader(mcMMO.getUsersFilePath());
-            BufferedReader in = new BufferedReader(file);
-            String line;
+    public PlayerProfile loadPlayerProfile(String playerName, boolean create) {
+        BufferedReader in = null;
+        String usersFilePath = mcMMO.getUsersFilePath();
 
-            while ((line = in.readLine()) != null) {
-                // Find if the line contains the player we want.
-                String[] character = line.split(":");
+        synchronized (fileWritingLock) {
+            try {
+                // Open the user file
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line;
 
-                if (!character[0].equalsIgnoreCase(playerName)) {
-                    continue;
-                }
+                while ((line = in.readLine()) != null) {
+                    // Find if the line contains the player we want.
+                    String[] character = line.split(":");
 
-                // Skill levels
-                playerData.add(character[24]); // Taming
-                playerData.add(character[1]); // Mining
-                playerData.add(character[7]); // Repair
-                playerData.add(character[5]); // Woodcutting
-                playerData.add(character[8]); // Unarmed
-                playerData.add(character[9]); // Herbalism
-                playerData.add(character[10]); // Excavation
-                playerData.add(character[11]); // Archery
-                playerData.add(character[12]); // Swords
-                playerData.add(character[13]); // Axes
-                playerData.add(character[14]); // Acrobatics
-                playerData.add(character[34]); // Fishing
-
-                // Experience
-                playerData.add(character[25]); // Taming
-                playerData.add(character[4]); // Mining
-                playerData.add(character[15]); // Repair
-                playerData.add(character[6]); // Woodcutting
-                playerData.add(character[16]); // Unarmed
-                playerData.add(character[17]); // Herbalism
-                playerData.add(character[18]); // Excavation
-                playerData.add(character[19]); // Archery
-                playerData.add(character[20]); // Swords
-                playerData.add(character[21]); // Axes
-                playerData.add(character[22]); // Acrobatics
-                playerData.add(character[35]); // Fishing
-
-                // Cooldowns
-                playerData.add(null); // Taming
-                playerData.add(character[32]); // SuperBreaker
-                playerData.add(null); // Repair
-                playerData.add(character[28]); // Tree Feller
-                playerData.add(character[26]); // Beserk
-                playerData.add(character[29]); // Green Terra
-                playerData.add(character[27]); // Giga Drill Breaker
-                playerData.add(null); // Archery
-                playerData.add(character[30]); // Serrated Strikes
-                playerData.add(character[31]); // Skull Splitter
-                playerData.add(null); // Acrobatics
-                playerData.add(character[36]); // Blast Mining
-
-                playerData.add(character.length > 33 ? character[33] : null); // HudType
-                playerData.add(character.length > 38 ? character[38] : null); // MobHealthBar
-            }
+                    if (!character[0].equalsIgnoreCase(playerName)) {
+                        continue;
+                    }
 
-            in.close();
-        }
-        catch (Exception e) {
-            e.printStackTrace();
+                    PlayerProfile p = loadFromLine(character);
+                    in.close();
+                    return p;
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                tryClose(in);
+            }
         }
 
-        return playerData;
+        if (create) {
+            newUser(playerName);
+            return new PlayerProfile(playerName, true);
+        }
+        return new PlayerProfile(playerName);
     }
 
-    public boolean convert(String[] character) throws Exception {
-        // Not implemented
-        return false;
+    public void convertUsers(DatabaseManager destination) {
+        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(":");
+
+                    try {
+                        destination.saveUser(loadFromLine(character));
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                tryClose(in);
+            }
+        }
     }
 
     public boolean checkConnected() {
         // Not implemented
-        return false;
+        return true;
+    }
+
+    public List<String> getStoredUsers() {
+        ArrayList<String> users = new ArrayList<String>();
+        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[0]);
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+            finally {
+                tryClose(in);
+            }
+        }
+        return users;
     }
 
     /**
-* Update the leader boards.
-*/
+     * 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) {
@@ -402,43 +479,50 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
         List<PlayerStat> taming = new ArrayList<PlayerStat>();
         List<PlayerStat> fishing = new ArrayList<PlayerStat>();
 
+        BufferedReader in = null;
         // Read from the FlatFile database and fill our arrays with information
-        try {
-            BufferedReader in = new BufferedReader(new FileReader(usersFilePath));
-            String line = "";
-            ArrayList<String> players = new ArrayList<String>();
-
-            while ((line = in.readLine()) != null) {
-                String[] data = line.split(":");
-                String playerName = data[0];
-                int powerLevel = 0;
-
-                // Prevent the same player from being added multiple times (I'd like to note that this shouldn't happen...)
-                if (players.contains(playerName)) {
-                    continue;
+        synchronized (fileWritingLock) {
+            try {
+                in = new BufferedReader(new FileReader(usersFilePath));
+                String line = "";
+                ArrayList<String> players = new ArrayList<String>();
+
+                while ((line = in.readLine()) != null) {
+                    String[] data = line.split(":");
+                    String playerName = data[0];
+                    int powerLevel = 0;
+
+                    // Prevent the same player from being added multiple times (I'd like to note that this shouldn't happen...)
+                    if (players.contains(playerName)) {
+                        continue;
+                    }
+
+                    players.add(playerName);
+
+                    Map<SkillType, Integer> skills = getSkillMapFromLine(data);
+
+                    powerLevel += putStat(acrobatics, playerName, skills.get(SkillType.ACROBATICS));
+                    powerLevel += putStat(archery, playerName, skills.get(SkillType.ARCHERY));
+                    powerLevel += putStat(axes, playerName, skills.get(SkillType.AXES));
+                    powerLevel += putStat(excavation, playerName, skills.get(SkillType.EXCAVATION));
+                    powerLevel += putStat(fishing, playerName, skills.get(SkillType.FISHING));
+                    powerLevel += putStat(herbalism, playerName, skills.get(SkillType.HERBALISM));
+                    powerLevel += putStat(mining, playerName, skills.get(SkillType.MINING));
+                    powerLevel += putStat(repair, playerName, skills.get(SkillType.REPAIR));
+                    powerLevel += putStat(swords, playerName, skills.get(SkillType.SWORDS));
+                    powerLevel += putStat(taming, playerName, skills.get(SkillType.TAMING));
+                    powerLevel += putStat(unarmed, playerName, skills.get(SkillType.UNARMED));
+                    powerLevel += putStat(woodcutting, playerName, skills.get(SkillType.WOODCUTTING));
+
+                    putStat(powerLevels, playerName, powerLevel);
                 }
-
-                players.add(playerName);
-
-                powerLevel += loadStat(mining, playerName, data, 1);
-                powerLevel += loadStat(woodcutting, playerName, data, 5);
-                powerLevel += loadStat(repair, playerName, data, 7);
-                powerLevel += loadStat(unarmed, playerName, data, 8);
-                powerLevel += loadStat(herbalism, playerName, data, 9);
-                powerLevel += loadStat(excavation, playerName, data, 10);
-                powerLevel += loadStat(archery, playerName, data, 11);
-                powerLevel += loadStat(swords, playerName, data, 12);
-                powerLevel += loadStat(axes, playerName, data, 13);
-                powerLevel += loadStat(acrobatics, playerName, data, 14);
-                powerLevel += loadStat(taming, playerName, data, 24);
-                powerLevel += loadStat(fishing, playerName, data, 34);
-
-                powerLevels.add(new PlayerStat(playerName, powerLevel));
             }
-            in.close();
-        }
-        catch (Exception e) {
-            mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
+            catch (Exception e) {
+                mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString());
+            }
+            finally {
+                tryClose(in);
+            }
         }
 
         SkillComparator c = new SkillComparator();
@@ -487,6 +571,15 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
         }
     }
 
+    private void tryClose(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
     private Integer getPlayerRank(String playerName, List<PlayerStat> statsList) {
         if (statsList == null) {
             return null;
@@ -505,14 +598,8 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
         return null;
     }
 
-    private int loadStat(List<PlayerStat> statList, String playerName, String[] data, int dataIndex) {
-        if (data.length <= dataIndex) {
-            return 0;
-        }
-
-        int statValue = Integer.parseInt(data[dataIndex]);
+    private int putStat(List<PlayerStat> statList, String playerName, int statValue) {
         statList.add(new PlayerStat(playerName, statValue));
-
         return statValue;
     }
 
@@ -522,4 +609,75 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
             return (o2.statVal - o1.statVal);
         }
     }
+
+    private PlayerProfile loadFromLine(String[] character) throws Exception {
+        Map<SkillType, Integer>   skills     = getSkillMapFromLine(character);      // Skill levels
+        Map<SkillType, Float>     skillsXp   = new HashMap<SkillType, Float>();     // Skill & XP
+        Map<AbilityType, Integer> skillsDATS = new HashMap<AbilityType, Integer>(); // Ability & Cooldown
+        HudType hudType;
+        MobHealthbarType mobHealthbarType;
+
+        // TODO on updates, put new values in a try{} ?
+
+        skillsXp.put(SkillType.TAMING, (float) Integer.valueOf(character[25]));
+        skillsXp.put(SkillType.MINING, (float) Integer.valueOf(character[4]));
+        skillsXp.put(SkillType.REPAIR, (float) Integer.valueOf(character[15]));
+        skillsXp.put(SkillType.WOODCUTTING, (float) Integer.valueOf(character[6]));
+        skillsXp.put(SkillType.UNARMED, (float) Integer.valueOf(character[16]));
+        skillsXp.put(SkillType.HERBALISM, (float) Integer.valueOf(character[17]));
+        skillsXp.put(SkillType.EXCAVATION, (float) Integer.valueOf(character[18]));
+        skillsXp.put(SkillType.ARCHERY, (float) Integer.valueOf(character[19]));
+        skillsXp.put(SkillType.SWORDS, (float) Integer.valueOf(character[20]));
+        skillsXp.put(SkillType.AXES, (float) Integer.valueOf(character[21]));
+        skillsXp.put(SkillType.ACROBATICS, (float) Integer.valueOf(character[22]));
+        skillsXp.put(SkillType.FISHING, (float) Integer.valueOf(character[35]));
+
+        // Taming - Unused
+        skillsDATS.put(AbilityType.SUPER_BREAKER, Integer.valueOf(character[32]));
+        // Repair - Unused
+        skillsDATS.put(AbilityType.TREE_FELLER, Integer.valueOf(character[28]));
+        skillsDATS.put(AbilityType.BERSERK, Integer.valueOf(character[26]));
+        skillsDATS.put(AbilityType.GREEN_TERRA, Integer.valueOf(character[29]));
+        skillsDATS.put(AbilityType.GIGA_DRILL_BREAKER, Integer.valueOf(character[27]));
+        // Archery - Unused
+        skillsDATS.put(AbilityType.SERRATED_STRIKES, Integer.valueOf(character[30]));
+        skillsDATS.put(AbilityType.SKULL_SPLITTER, Integer.valueOf(character[31]));
+        // Acrobatics - Unused
+        skillsDATS.put(AbilityType.BLAST_MINING, Integer.valueOf(character[36]));
+
+        try {
+            hudType = HudType.valueOf(character[33]);
+        }
+        catch (Exception e) {
+            hudType = HudType.STANDARD; // Shouldn't happen unless database is being tampered with
+        }
+
+        try {
+            mobHealthbarType = MobHealthbarType.valueOf(character[38]);
+        }
+        catch (Exception e) {
+            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
+        }
+
+        return new PlayerProfile(character[0], skills, skillsXp, skillsDATS, hudType, mobHealthbarType);
+    }
+
+    private Map<SkillType, Integer> getSkillMapFromLine(String[] character) {
+        Map<SkillType, Integer> skills = new HashMap<SkillType, Integer>();   // Skill & Level
+
+        skills.put(SkillType.TAMING, Integer.valueOf(character[24]));
+        skills.put(SkillType.MINING, Integer.valueOf(character[1]));
+        skills.put(SkillType.REPAIR, Integer.valueOf(character[7]));
+        skills.put(SkillType.WOODCUTTING, Integer.valueOf(character[5]));
+        skills.put(SkillType.UNARMED, Integer.valueOf(character[8]));
+        skills.put(SkillType.HERBALISM, Integer.valueOf(character[9]));
+        skills.put(SkillType.EXCAVATION, Integer.valueOf(character[10]));
+        skills.put(SkillType.ARCHERY, Integer.valueOf(character[11]));
+        skills.put(SkillType.SWORDS, Integer.valueOf(character[12]));
+        skills.put(SkillType.AXES, Integer.valueOf(character[13]));
+        skills.put(SkillType.ACROBATICS, Integer.valueOf(character[14]));
+        skills.put(SkillType.FISHING, Integer.valueOf(character[34]));
+
+        return skills;
+    }
 }

+ 206 - 174
src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java

@@ -5,12 +5,14 @@ import java.sql.DriverManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Statement;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.logging.Level;
 
 import com.gmail.nossr50.mcMMO;
 import com.gmail.nossr50.config.Config;
@@ -23,7 +25,6 @@ import com.gmail.nossr50.datatypes.skills.SkillType;
 import com.gmail.nossr50.datatypes.spout.huds.HudType;
 import com.gmail.nossr50.runnables.database.SQLReconnectTask;
 import com.gmail.nossr50.util.Misc;
-import com.gmail.nossr50.util.StringUtils;
 
 public final class SQLDatabaseManager implements DatabaseManager {
     private String connectionString;
@@ -102,7 +103,16 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     public void saveUser(PlayerProfile profile) {
+        checkConnected();
         int userId = readId(profile.getPlayerName());
+        if (userId == -1) {
+            newUser(profile.getPlayerName());
+            userId = readId(profile.getPlayerName());
+            if (userId == -1) {
+                mcMMO.p.getLogger().log(Level.WARNING, "Failed to save user " + profile.getPlayerName());
+                return;
+            }
+        }
         MobHealthbarType mobHealthbarType = profile.getMobHealthbarType();
         HudType hudType = profile.getHudType();
 
@@ -297,7 +307,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
             statement.setLong(2, System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR);
             statement.execute();
 
-            writeMissingRows(readId(playerName));
+            int id = statement.getGeneratedKeys().getInt("id");
+            writeMissingRows(id);
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -314,8 +325,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
         }
     }
 
-    public List<String> loadPlayerData(String playerName) {
-        List<String> playerData = null;
+    public PlayerProfile loadPlayerProfile(String playerName, boolean create) {
         PreparedStatement statement = null;
 
         try {
@@ -333,22 +343,17 @@ public final class SQLDatabaseManager implements DatabaseManager {
                     + "WHERE u.user = ?");
             statement.setString(1, playerName);
 
-            playerData = readRow(statement);
+            ResultSet result = statement.executeQuery();
 
-            if (playerData == null || playerData.size() == 0) {
-                int userId = readId(playerName);
-
-                // Check if user doesn't exist
-                if (userId == 0) {
-                    return playerData;
+            if (result.next()) {
+                try {
+                    PlayerProfile ret = loadFromResult(playerName, result);
+                    result.close();
+                    return ret;
                 }
-
-                // Write missing table rows
-                writeMissingRows(userId);
-
-                // Re-read data
-                playerData = loadPlayerData(playerName);
+                catch (SQLException e) {}
             }
+            result.close();
         }
         catch (SQLException ex) {
             printErrors(ex);
@@ -363,108 +368,74 @@ public final class SQLDatabaseManager implements DatabaseManager {
                 }
             }
         }
-        return playerData;
-    }
 
-    public boolean convert(String[] data) throws Exception {
-        String playerName = data[0];
+        // Problem, nothing was returned
 
-        // Check for things we don't want put in the DB
-        if (playerName == null || playerName.equalsIgnoreCase("null") || playerName.length() > 16) {
-            return false;
+        // First, read User Id - this is to check for orphans
+
+        int id = readId(playerName);
+
+        if (id == -1) {
+            // There is no such user
+            if (create) {
+                newUser(playerName);
+                return new PlayerProfile(playerName, true);
+            }
+            else {
+                return new PlayerProfile(playerName, false);
+            }
         }
+        // There is such a user
+        writeMissingRows(id);
+        // Retry, and abort on re-failure
+        return loadPlayerProfile(playerName, false);
+    }
 
-        String mining = (data.length > 1) ? data[1] : null;
-        String woodcutting = (data.length > 5) ? data[5] : null;
-        String repair = (data.length > 7) ? data[7] : null;
-        String unarmed = (data.length > 8) ? data[8] : null;
-        String herbalism = (data.length > 9) ? data[9] : null;
-        String excavation = (data.length > 10) ? data[10] : null;
-        String archery = (data.length > 11) ? data[11] : null;
-        String swords = (data.length > 12) ? data[12] : null;
-        String axes = (data.length > 13) ? data[13] : null;
-        String acrobatics = (data.length > 14) ? data[14] : null;
-        String taming = (data.length > 24) ? data[24] : null;
-        String fishing = (data.length > 34) ? data[34] : null;
-
-        String miningXP = (data.length > 4) ? data[4] : null;
-        String woodCuttingXP = (data.length > 6) ? data[6] : null;;
-        String repairXP = (data.length > 15) ? data[15] : null;
-        String unarmedXP = (data.length > 16) ? data[16] : null;
-        String herbalismXP = (data.length > 17) ? data[17] : null;
-        String excavationXP = (data.length > 18) ? data[18] : null;
-        String archeryXP = (data.length > 19) ? data[19] : null;
-        String swordsXP = (data.length > 20) ? data[20] : null;
-        String axesXP = (data.length > 21) ? data[21] : null;
-        String acrobaticsXP = (data.length > 22) ? data[22] : null;
-        String tamingXP = (data.length > 25) ? data[25] : null;
-        String fishingXP = (data.length > 35) ? data[35] : null;
-
-        String superBreakerCooldown = (data.length > 32) ? data[32] : null;
-        String treeFellerCooldown = (data.length > 28) ? data[28] : null;
-        String berserkCooldown = (data.length > 26) ? data[26] : null;
-        String greenTerraCooldown = (data.length > 29) ? data[29] : null;
-        String gigaDrillBreakerCooldown = (data.length > 27) ? data[27] : null;
-        String serratedStrikesCooldown = (data.length > 30) ? data[30] : null;
-        String skullSplitterCooldown = (data.length > 31) ? data[31] : null;
-        String blastMiningCooldown = (data.length > 36) ? data[36] : null;
-
-        String hudType = (data.length > 33) ? data[33] : null;
-        String mobHealthbarType = (data.length > 38 ? data[38] : null);
-        long lastLogin = mcMMO.p.getServer().getOfflinePlayer(playerName).getLastPlayed();
-
-        int id = readId(playerName); // Check to see if the user is in the DB
-
-        // Create the user if they don't exist
-        if (id == 0) {
-            newUser(playerName);
-            id = readId(playerName);
+    public void convertUsers(DatabaseManager destination) {
+        PreparedStatement statement = null;
+
+        try {
+            statement = connection.prepareStatement(
+                    "SELECT "
+                    + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, "
+                    + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, "
+                    + "c.taming, c.mining, c.repair, c.woodcutting, c.unarmed, c.herbalism, c.excavation, c.archery, c.swords, c.axes, c.acrobatics, c.blast_mining, "
+                    + "h.hudtype, h.mobhealthbar "
+                    + "FROM " + tablePrefix + "users u "
+                    + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) "
+                    + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) "
+                    + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) "
+                    + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) "
+                    + "WHERE u.user = ?");
+            List<String> usernames = getStoredUsers();
+            ResultSet result = null;
+            for (String playerName : usernames) {
+                statement.setString(1, playerName);
+                try {
+                    result = statement.executeQuery();
+                    result.next();
+                    destination.saveUser(loadFromResult(playerName, result));
+                    result.close();
+                }
+                catch (SQLException e) {
+                    // Ignore
+                }
+            }
+        }
+        catch (SQLException e) {
+            printErrors(e);
+        }
+        finally {
+            if (statement != null) {
+                try {
+                    statement.close();
+                } catch (SQLException e) {
+                    // Ignore
+                }
+            }
         }
 
-        saveLogin(id, lastLogin);
-        saveIntegers(
-                "UPDATE " + tablePrefix + "skills SET "
-                        + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
-                        + ", unarmed = ?, herbalism = ?, excavation = ?"
-                        + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
-                        + ", fishing = ? WHERE user_id = ?",
-                StringUtils.getInt(taming), StringUtils.getInt(mining),
-                StringUtils.getInt(repair), StringUtils.getInt(woodcutting),
-                StringUtils.getInt(unarmed), StringUtils.getInt(herbalism),
-                StringUtils.getInt(excavation), StringUtils.getInt(archery),
-                StringUtils.getInt(swords), StringUtils.getInt(axes),
-                StringUtils.getInt(acrobatics), StringUtils.getInt(fishing),
-                id);
-        saveIntegers(
-                "UPDATE " + tablePrefix + "experience SET "
-                        + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
-                        + ", unarmed = ?, herbalism = ?, excavation = ?"
-                        + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
-                        + ", fishing = ? WHERE user_id = ?",
-                StringUtils.getInt(tamingXP), StringUtils.getInt(miningXP),
-                StringUtils.getInt(repairXP), StringUtils.getInt(woodCuttingXP),
-                StringUtils.getInt(unarmedXP), StringUtils.getInt(herbalismXP),
-                StringUtils.getInt(excavationXP), StringUtils.getInt(archeryXP),
-                StringUtils.getInt(swordsXP), StringUtils.getInt(axesXP),
-                StringUtils.getInt(acrobaticsXP), StringUtils.getInt(fishingXP),
-                id);
-        saveLongs(
-                "UPDATE " + tablePrefix + "cooldowns SET "
-                        + " taming = ?, mining = ?, repair = ?, woodcutting = ?"
-                        + ", unarmed = ?, herbalism = ?, excavation = ?"
-                        + ", archery = ?, swords = ?, axes = ?, acrobatics = ?"
-                        + ", blast_mining = ? WHERE user_id = ?",
-                id,
-                StringUtils.getLong(null), StringUtils.getLong(superBreakerCooldown),
-                StringUtils.getLong(null), StringUtils.getInt(treeFellerCooldown),
-                StringUtils.getLong(berserkCooldown), StringUtils.getLong(greenTerraCooldown),
-                StringUtils.getLong(gigaDrillBreakerCooldown), StringUtils.getLong(null),
-                StringUtils.getLong(serratedStrikesCooldown), StringUtils.getLong(skullSplitterCooldown),
-                StringUtils.getLong(null), StringUtils.getLong(blastMiningCooldown));
-        saveHuds(id, hudType, mobHealthbarType);
-        return true;
     }
-
     /**
     * Check connection status and re-establish if dead or stale.
     *
@@ -555,9 +526,35 @@ public final class SQLDatabaseManager implements DatabaseManager {
         return false;
     }
 
+    public List<String> getStoredUsers() {
+        ArrayList<String> users = new ArrayList<String>();
+        Statement stmt = null;
+        try {
+            stmt = connection.createStatement();
+            ResultSet result = stmt.executeQuery("SELECT user FROM " + tablePrefix + "users");
+            while (result.next()) {
+                users.add(result.getString("user"));
+            }
+            result.close();
+        }
+        catch (SQLException e) {
+            printErrors(e);
+        }
+        finally {
+            if (stmt != null) {
+                try {
+                    stmt.close();
+                } catch (SQLException e) {
+                    // Ignore
+                }
+            }
+        }
+        return users;
+    }
+
     /**
-    * Attempt to connect to the mySQL database.
-    */
+     * Attempt to connect to the mySQL database.
+     */
     private void connect() {
         connectionString = "jdbc:mysql://" + Config.getInstance().getMySQLServerName() + ":" + Config.getInstance().getMySQLServerPort() + "/" + Config.getInstance().getMySQLDatabaseName();
 
@@ -592,8 +589,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     /**
-    * Attempt to create the database structure.
-    */
+     * Attempt to create the database structure.
+     */
     private void createStructure() {
         write("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` ("
                 + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT,"
@@ -662,10 +659,10 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     /**
-    * Check database structure for missing values.
-    *
-    * @param update Type of data to check updates for
-    */
+     * Check database structure for missing values.
+     *
+     * @param update Type of data to check updates for
+     */
     private void checkDatabaseStructure(DatabaseUpdateType update) {
         String sql = "";
 
@@ -793,11 +790,11 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     /**
-    * Attempt to write the SQL query.
-    *
-    * @param sql Query to write.
-    * @return true if the query was successfully written, false otherwise.
-    */
+     * Attempt to write the SQL query.
+     *
+     * @param sql Query to write.
+     * @return true if the query was successfully written, false otherwise.
+     */
     private boolean write(String sql) {
         if (!checkConnected()) {
             return false;
@@ -828,11 +825,11 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     /**
-    * Returns the number of rows affected by either a DELETE or UPDATE query
-    *
-    * @param sql SQL query to execute
-    * @return the number of rows affected
-    */
+     * Returns the number of rows affected by either a DELETE or UPDATE query
+     *
+     * @param sql SQL query to execute
+     * @return the number of rows affected
+     */
     private int update(String sql) {
         int rows = 0;
 
@@ -862,11 +859,11 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     /**
-    * Read SQL query.
-    *
-    * @param sql SQL query to read
-    * @return the rows in this SQL query
-    */
+     * Read SQL query.
+     *
+     * @param sql SQL query to read
+     * @return the rows in this SQL query
+     */
     private HashMap<Integer, ArrayList<String>> read(String sql) {
         HashMap<Integer, ArrayList<String>> rows = new HashMap<Integer, ArrayList<String>>();
 
@@ -906,45 +903,12 @@ public final class SQLDatabaseManager implements DatabaseManager {
         return rows;
     }
 
-    private ArrayList<String> readRow(PreparedStatement statement) {
-        ArrayList<String> playerData = new ArrayList<String>();
-
-        if (checkConnected()) {
-            ResultSet resultSet = null;
-
-            try {
-                resultSet = statement.executeQuery();
-
-                if (resultSet.next()) {
-                    for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) {
-                        playerData.add(resultSet.getString(i));
-                    }
-                }
-            }
-            catch (SQLException ex) {
-                printErrors(ex);
-            }
-            finally {
-                if (statement != null) {
-                    try {
-                        statement.close();
-                    }
-                    catch (SQLException e) {
-                        // Ignore
-                    }
-                }
-            }
-        }
-
-        return playerData;
-    }
-
     /**
-    * Get the Integer. Only return first row / first field.
-    *
-    * @param sql SQL query to execute
-    * @return the value in the first row / first field
-    */
+     * Get the Integer. Only return first row / first field.
+     *
+     * @param sql SQL query to execute
+     * @return the value in the first row / first field
+     */
     private int readInt(PreparedStatement statement) {
         int result = 0;
 
@@ -1079,7 +1043,7 @@ public final class SQLDatabaseManager implements DatabaseManager {
     }
 
     private int readId(String playerName) {
-        int id = 0;
+        int id = -1;
 
         try {
             PreparedStatement statement = connection.prepareStatement("SELECT id FROM " + tablePrefix + "users WHERE user = ?");
@@ -1142,6 +1106,74 @@ public final class SQLDatabaseManager implements DatabaseManager {
         }
     }
 
+    private PlayerProfile loadFromResult(String playerName, ResultSet result) throws SQLException {
+        Map<SkillType, Integer>   skills     = new HashMap<SkillType, Integer>();   // Skill & Level
+        Map<SkillType, Float>     skillsXp   = new HashMap<SkillType, Float>();     // Skill & XP
+        Map<AbilityType, Integer> skillsDATS = new HashMap<AbilityType, Integer>(); // Ability & Cooldown
+        HudType hudType;
+        MobHealthbarType mobHealthbarType;
+
+        final int OFFSET_SKILLS = 0; // TODO update these numbers when the query changes (a new skill is added)
+        final int OFFSET_XP = 12;
+        final int OFFSET_DATS = 24;
+        final int OFFSET_OTHER = 36;
+
+        skills.put(SkillType.TAMING, result.getInt(OFFSET_SKILLS + 1));
+        skills.put(SkillType.MINING, result.getInt(OFFSET_SKILLS + 2));
+        skills.put(SkillType.REPAIR, result.getInt(OFFSET_SKILLS + 3));
+        skills.put(SkillType.WOODCUTTING, result.getInt(OFFSET_SKILLS + 4));
+        skills.put(SkillType.UNARMED, result.getInt(OFFSET_SKILLS + 5));
+        skills.put(SkillType.HERBALISM, result.getInt(OFFSET_SKILLS + 6));
+        skills.put(SkillType.EXCAVATION, result.getInt(OFFSET_SKILLS + 7));
+        skills.put(SkillType.ARCHERY, result.getInt(OFFSET_SKILLS + 8));
+        skills.put(SkillType.SWORDS, result.getInt(OFFSET_SKILLS + 9));
+        skills.put(SkillType.AXES, result.getInt(OFFSET_SKILLS + 10));
+        skills.put(SkillType.ACROBATICS, result.getInt(OFFSET_SKILLS + 11));
+        skills.put(SkillType.FISHING, result.getInt(OFFSET_SKILLS + 12));
+
+        skillsXp.put(SkillType.TAMING, result.getFloat(OFFSET_XP + 1));
+        skillsXp.put(SkillType.MINING, result.getFloat(OFFSET_XP + 2));
+        skillsXp.put(SkillType.REPAIR, result.getFloat(OFFSET_XP + 3));
+        skillsXp.put(SkillType.WOODCUTTING, result.getFloat(OFFSET_XP + 4));
+        skillsXp.put(SkillType.UNARMED, result.getFloat(OFFSET_XP + 5));
+        skillsXp.put(SkillType.HERBALISM, result.getFloat(OFFSET_XP + 6));
+        skillsXp.put(SkillType.EXCAVATION, result.getFloat(OFFSET_XP + 7));
+        skillsXp.put(SkillType.ARCHERY, result.getFloat(OFFSET_XP + 8));
+        skillsXp.put(SkillType.SWORDS, result.getFloat(OFFSET_XP + 9));
+        skillsXp.put(SkillType.AXES, result.getFloat(OFFSET_XP + 10));
+        skillsXp.put(SkillType.ACROBATICS, result.getFloat(OFFSET_XP + 11));
+        skillsXp.put(SkillType.FISHING, result.getFloat(OFFSET_XP + 12));
+
+        // Taming - Unused - result.getInt(OFFSET_DATS + 1)
+        skillsDATS.put(AbilityType.SUPER_BREAKER, result.getInt(OFFSET_DATS + 2));
+        // Repair - Unused - result.getInt(OFFSET_DATS + 3)
+        skillsDATS.put(AbilityType.TREE_FELLER, result.getInt(OFFSET_DATS + 4));
+        skillsDATS.put(AbilityType.BERSERK, result.getInt(OFFSET_DATS + 5));
+        skillsDATS.put(AbilityType.GREEN_TERRA, result.getInt(OFFSET_DATS + 6));
+        skillsDATS.put(AbilityType.GIGA_DRILL_BREAKER, result.getInt(OFFSET_DATS + 7));
+        // Archery - Unused - result.getInt(OFFSET_DATS + 8)
+        skillsDATS.put(AbilityType.SERRATED_STRIKES, result.getInt(OFFSET_DATS + 9));
+        skillsDATS.put(AbilityType.SKULL_SPLITTER, result.getInt(OFFSET_DATS + 10));
+        // Acrobatics - Unused - result.getInt(OFFSET_DATS + 11)
+        skillsDATS.put(AbilityType.BLAST_MINING, result.getInt(OFFSET_DATS + 12));
+
+        try {
+            hudType = HudType.valueOf(result.getString(OFFSET_OTHER + 1));
+        }
+        catch (Exception e) {
+            hudType = HudType.STANDARD; // Shouldn't happen unless database is being tampered with
+        }
+
+        try {
+            mobHealthbarType = MobHealthbarType.valueOf(result.getString(OFFSET_OTHER + 2));
+        }
+        catch (Exception e) {
+            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
+        }
+
+        return new PlayerProfile(playerName, skills, skillsXp, skillsDATS, hudType, mobHealthbarType);
+    }
+
     private void printErrors(SQLException ex) {
         mcMMO.p.getLogger().severe("SQLException: " + ex.getMessage());
         mcMMO.p.getLogger().severe("SQLState: " + ex.getSQLState());

+ 2 - 2
src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java

@@ -92,10 +92,10 @@ public class McMMOPlayer {
         String playerName = player.getName();
 
         this.player = player;
-        profile = new PlayerProfile(playerName, true);
+        profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, true);
         party = PartyManager.getPlayerParty(playerName);
 
-        /* 
+        /*
          * I'm using this method because it makes code shorter and safer (we don't have to add all SkillTypes manually),
          * but I actually have no idea about the performance impact, if there is any.
          * If in the future someone wants to remove this, don't forget to also remove what is in the SkillType enum. - bm01

+ 21 - 70
src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java

@@ -1,7 +1,6 @@
 package com.gmail.nossr50.datatypes.player;
 
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -32,7 +31,7 @@ public class PlayerProfile {
     private final Map<SkillType, Float>     skillsXp   = new HashMap<SkillType, Float>();     // Skill & XP
     private final Map<AbilityType, Integer> skillsDATS = new HashMap<AbilityType, Integer>(); // Ability & Cooldown
 
-    public PlayerProfile(String playerName, boolean addNew) {
+    public PlayerProfile(String playerName) {
         this.playerName = playerName;
 
         hudType = mcMMO.isSpoutEnabled() ? SpoutConfig.getInstance().getDefaultHudType() : HudType.DISABLED;
@@ -46,11 +45,27 @@ public class PlayerProfile {
             skills.put(skillType, 0);
             skillsXp.put(skillType, 0F);
         }
+    }
 
-        if (!loadPlayer() && addNew) {
-            mcMMO.getDatabaseManager().newUser(playerName);
-            loaded = true;
-        }
+    public PlayerProfile(String playerName, boolean isLoaded) {
+        this(playerName);
+        this.loaded = isLoaded;
+    }
+
+    /**
+     * Calling this constructor is considered loading the profile.
+     */
+    public PlayerProfile(String playerName, Map<SkillType, Integer> argSkills, Map<SkillType, Float> argSkillsXp, Map<AbilityType, Integer> argSkillsDats, HudType hudType, MobHealthbarType mobHealthbarType) {
+        this(playerName, true);
+
+        this.hudType = hudType;
+        this.mobHealthbarType = mobHealthbarType;
+
+        this.skills.putAll(argSkills);
+        this.skillsXp.putAll(argSkillsXp);
+        this.skillsDATS.putAll(argSkillsDats);
+
+        loaded = true;
     }
 
     public void save() {
@@ -254,68 +269,4 @@ public class PlayerProfile {
 
         return sum / parents.size();
     }
-
-    private boolean loadPlayer() {
-        List<String> playerData = mcMMO.getDatabaseManager().loadPlayerData(playerName);
-        
-        if (playerData == null || playerData.isEmpty()) {
-            return false;
-        }
-
-        skills.put(SkillType.TAMING, Integer.valueOf(playerData.get(0)));
-        skills.put(SkillType.MINING, Integer.valueOf(playerData.get(1)));
-        skills.put(SkillType.REPAIR, Integer.valueOf(playerData.get(2)));
-        skills.put(SkillType.WOODCUTTING, Integer.valueOf(playerData.get(3)));
-        skills.put(SkillType.UNARMED, Integer.valueOf(playerData.get(4)));
-        skills.put(SkillType.HERBALISM, Integer.valueOf(playerData.get(5)));
-        skills.put(SkillType.EXCAVATION, Integer.valueOf(playerData.get(6)));
-        skills.put(SkillType.ARCHERY, Integer.valueOf(playerData.get(7)));
-        skills.put(SkillType.SWORDS, Integer.valueOf(playerData.get(8)));
-        skills.put(SkillType.AXES, Integer.valueOf(playerData.get(9)));
-        skills.put(SkillType.ACROBATICS, Integer.valueOf(playerData.get(10)));
-        skills.put(SkillType.FISHING, Integer.valueOf(playerData.get(11)));
-
-        skillsXp.put(SkillType.TAMING, (float) Integer.valueOf(playerData.get(12)));
-        skillsXp.put(SkillType.MINING, (float) Integer.valueOf(playerData.get(13)));
-        skillsXp.put(SkillType.REPAIR, (float) Integer.valueOf(playerData.get(14)));
-        skillsXp.put(SkillType.WOODCUTTING, (float) Integer.valueOf(playerData.get(15)));
-        skillsXp.put(SkillType.UNARMED, (float) Integer.valueOf(playerData.get(16)));
-        skillsXp.put(SkillType.HERBALISM, (float) Integer.valueOf(playerData.get(17)));
-        skillsXp.put(SkillType.EXCAVATION, (float) Integer.valueOf(playerData.get(18)));
-        skillsXp.put(SkillType.ARCHERY, (float) Integer.valueOf(playerData.get(19)));
-        skillsXp.put(SkillType.SWORDS, (float) Integer.valueOf(playerData.get(20)));
-        skillsXp.put(SkillType.AXES, (float) Integer.valueOf(playerData.get(21)));
-        skillsXp.put(SkillType.ACROBATICS, (float) Integer.valueOf(playerData.get(22)));
-        skillsXp.put(SkillType.FISHING, (float) Integer.valueOf(playerData.get(23)));
-
-        // Taming 24 - Unused
-        skillsDATS.put(AbilityType.SUPER_BREAKER, Integer.valueOf(playerData.get(25)));
-        // Repair 26 - Unused
-        skillsDATS.put(AbilityType.TREE_FELLER, Integer.valueOf(playerData.get(27)));
-        skillsDATS.put(AbilityType.BERSERK, Integer.valueOf(playerData.get(28)));
-        skillsDATS.put(AbilityType.GREEN_TERRA, Integer.valueOf(playerData.get(29)));
-        skillsDATS.put(AbilityType.GIGA_DRILL_BREAKER, Integer.valueOf(playerData.get(30)));
-        // Archery 31 - Unused
-        skillsDATS.put(AbilityType.SERRATED_STRIKES, Integer.valueOf(playerData.get(32)));
-        skillsDATS.put(AbilityType.SKULL_SPLITTER, Integer.valueOf(playerData.get(33)));
-        // Acrobatics 34 - Unused
-        skillsDATS.put(AbilityType.BLAST_MINING, Integer.valueOf(playerData.get(35)));
-
-        try {
-            hudType = HudType.valueOf(playerData.get(36));
-        }
-        catch (Exception e) {
-            hudType = HudType.STANDARD; // Shouldn't happen unless database is being tampered with
-        }
-
-        try {
-            mobHealthbarType = MobHealthbarType.valueOf(playerData.get(37));
-        }
-        catch (Exception e) {
-            mobHealthbarType = Config.getInstance().getMobHealthbarDefault();
-        }
-
-        loaded = true;
-        return true;
-    }
 }

+ 1 - 0
src/main/java/com/gmail/nossr50/runnables/PlayerUpdateInventoryTask.java

@@ -3,6 +3,7 @@ package com.gmail.nossr50.runnables;
 import org.bukkit.entity.Player;
 import org.bukkit.scheduler.BukkitRunnable;
 
+@SuppressWarnings("deprecation")
 public class PlayerUpdateInventoryTask extends BukkitRunnable {
     private Player player;
 

+ 41 - 0
src/main/java/com/gmail/nossr50/runnables/database/ConversionTask.java

@@ -0,0 +1,41 @@
+package com.gmail.nossr50.runnables.database;
+
+import java.util.logging.Level;
+
+import org.bukkit.command.CommandSender;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import com.gmail.nossr50.mcMMO;
+import com.gmail.nossr50.database.DatabaseManager;
+import com.gmail.nossr50.locale.LocaleLoader;
+
+public class ConversionTask extends BukkitRunnable {
+    private final DatabaseManager sourceDb;
+    private final CommandSender sender;
+    private final String message;
+
+    public ConversionTask(DatabaseManager from, CommandSender sendback, String oldType, String newType) {
+        sourceDb = from;
+        sender = sendback;
+        message = LocaleLoader.getString("Commands.mmoupdate.Finish", oldType, newType);
+    }
+
+    @Override
+    public void run() {
+        sourceDb.convertUsers(mcMMO.getDatabaseManager());
+
+        // Announce completeness
+        mcMMO.p.getServer().getScheduler().runTask(mcMMO.p, new CompleteAnnouncement());
+    }
+
+    public class CompleteAnnouncement implements Runnable {
+        @Override
+        public void run() {
+            try {
+                sender.sendMessage(message);
+            } catch (Exception e) {
+                mcMMO.p.getLogger().log(Level.WARNING, "Exception sending database conversion completion message to " + sender.getName(), e);
+            }
+        }
+    }
+}

+ 0 - 37
src/main/java/com/gmail/nossr50/runnables/database/SQLConversionTask.java

@@ -1,37 +0,0 @@
-package com.gmail.nossr50.runnables.database;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-
-import org.bukkit.scheduler.BukkitRunnable;
-
-import com.gmail.nossr50.mcMMO;
-
-public class SQLConversionTask extends BukkitRunnable {
-
-    @Override
-    public void run() {
-        String location = mcMMO.getUsersFilePath();
-
-        try {
-            BufferedReader in = new BufferedReader(new FileReader(location));
-            String line = "";
-            int converted = 0;
-
-            while ((line = in.readLine()) != null) {
-
-                // Find if the line contains the player we want.
-                String[] playerData = line.split(":");
-                if (mcMMO.getDatabaseManager().convert(playerData)) {
-                    converted++;
-                }
-            }
-
-            mcMMO.p.getLogger().info("MySQL Updated from users file, " + converted + " items added/updated to MySQL DB");
-            in.close();
-        }
-        catch (Exception e) {
-            mcMMO.p.getLogger().severe("Exception while reading " + location + " (Are you sure you formatted it correctly?)" + e.toString());
-        }
-    }
-}

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

@@ -19,6 +19,7 @@ import com.gmail.nossr50.commands.chat.AdminChatCommand;
 import com.gmail.nossr50.commands.chat.PartyChatCommand;
 import com.gmail.nossr50.commands.database.McpurgeCommand;
 import com.gmail.nossr50.commands.database.McremoveCommand;
+import com.gmail.nossr50.commands.database.MmoshowdbCommand;
 import com.gmail.nossr50.commands.database.MmoupdateCommand;
 import com.gmail.nossr50.commands.experience.AddlevelsCommand;
 import com.gmail.nossr50.commands.experience.AddxpCommand;
@@ -276,11 +277,20 @@ public final class CommandRegistrationManager {
         PluginCommand command = mcMMO.p.getCommand("mmoupdate");
         command.setDescription(LocaleLoader.getString("Commands.Description.mmoupdate"));
         command.setPermission("mcmmo.commands.mmoupdate");
-        command.setPermissionMessage(permissionsMessage);
-        command.setUsage(LocaleLoader.getString("Commands.Usage.0", "mmoupdate"));
+        command.setPermissionMessage(LocaleLoader.getString("Commands.mmoupdate.OpOnly"));
+        command.setUsage(LocaleLoader.getString("Commands.Usage.1", "mmoupdate", "<confirm|flatfile|sql|" + LocaleLoader.getString("Commands.Usage.FullClassName") + ">"));
         command.setExecutor(new MmoupdateCommand());
     }
 
+    private static void registerMmoshowdbCommand() {
+        PluginCommand command = mcMMO.p.getCommand("mmoshowdb");
+        command.setDescription(LocaleLoader.getString("Commands.Description.mmoshowdb"));
+        command.setPermission("mcmmo.commands.mmoshowdb");
+        command.setPermissionMessage(permissionsMessage);
+        command.setUsage(LocaleLoader.getString("Commands.Usage.0", "mmoshowdb"));
+        command.setExecutor(new MmoshowdbCommand());
+    }
+
     private static void registerAdminChatCommand() {
         PluginCommand command = mcMMO.p.getCommand("adminchat");
         command.setDescription(LocaleLoader.getString("Commands.Description.adminchat"));
@@ -421,6 +431,7 @@ public final class CommandRegistrationManager {
         registerMcpurgeCommand();
         registerMcremoveCommand();
         registerMmoupdateCommand();
+        registerMmoshowdbCommand();
 
         // Experience Commands
         registerAddlevelsCommand();

+ 9 - 4
src/main/resources/locale/locale_en_US.properties

@@ -442,8 +442,11 @@ Commands.mmoedit=[player] <skill> <newvalue> [[RED]] - Modify target
 Commands.mmoedit.AllSkills.1=[[GREEN]]Your level in all skills was set to {0}!
 Commands.mmoedit.Modified.1=[[GREEN]]Your level in {0} was set to {1}!
 Commands.mmoedit.Modified.2=[[RED]]{0} has been modified for {1}.
-Commands.mmoupdate.Start=[[GRAY]]Starting conversion...
-Commands.mmoupdate.Finish=[[GREEN]]Conversion finished!
+Commands.mmoupdate.Same=[[RED]]You are already using the {0} database!
+Commands.mmoupdate.InvalidType=[[RED]]{0} is not a valid database type.
+Commands.mmoupdate.Start=[[GRAY]]Starting conversion from {0} to {1}...
+Commands.mmoupdate.Finish=[[GRAY]]Database migration complete; the {1} database now has all data from the {0} database.
+Commands.mmoshowdb=[[YELLOW]]The currently used database is [[GREEN]]{0}
 Commands.ModDescription=[[RED]]- Read brief mod description
 Commands.NoConsole=This command does not support console usage.
 Commands.Notifications.Off=Ability notifications toggled [[RED]]off
@@ -505,6 +508,7 @@ Commands.Usage.0=[[RED]]Proper usage is /{0}
 Commands.Usage.1=[[RED]]Proper usage is /{0} {1}
 Commands.Usage.2=[[RED]]Proper usage is /{0} {1} {2}
 Commands.Usage.3=[[RED]]Proper usage is /{0} {1} {2} {3}
+Commands.Usage.FullClassName=classname
 Commands.Usage.Level=level
 Commands.Usage.Message=message
 Commands.Usage.Page=page
@@ -751,7 +755,8 @@ Commands.Description.mcremove=Remove a user from the mcMMO database
 Commands.Description.mcstats=Show your mcMMO levels and XP
 Commands.Description.mctop=Show mcMMO leader boards
 Commands.Description.mmoedit=Edit mcMMO levels for a user
-Commands.Description.mmoupdate=Convert mcMMO database from Flatfile to MySQL
+Commands.Description.mmoupdate=Migrate mcMMO database from an old database into the current one
+Commands.Description.mmoshowdb=Show the name of the current database type (for later use with /mmoupdate)
 Commands.Description.party=Control various mcMMO party settings
 Commands.Description.partychat=Toggle mcMMO party chat on/off or send party chat messages
 Commands.Description.ptp=Teleport to an mcMMO party member
@@ -774,4 +779,4 @@ Scoreboard.Misc.PowerLevel=Power Level
 Scoreboard.Misc.Level=Level
 Scoreboard.Misc.CurrentXP=Current XP
 Scoreboard.Misc.RemainingXP=Remaining XP
-Scoreboard.Misc.Overall=Overall
+Scoreboard.Misc.Overall=Overall

+ 6 - 1
src/main/resources/plugin.yml

@@ -58,7 +58,9 @@ commands:
     inspect:
         description: View detailed mcMMO info on another player
     mmoupdate:
-        description: Convert from Flat File to MySQL
+        description: Migrate mcMMO database from an old database type to the current
+    mmoshowdb:
+        description: Show the name of the current database type (for later use with /mmoupdate)
     partychat:
         aliases: [pc, p]
         description: Toggle Party chat or send party chat messages
@@ -717,6 +719,7 @@ permissions:
             mcmmo.commands.mmoedit: true
             mcmmo.commands.mmoedit.others: true
             mcmmo.commands.mmoupdate: true
+            mcmmo.commands.mmoshowdb: true
             mcmmo.commands.ptp.world.all: true
             mcmmo.commands.skillreset.all: true
             mcmmo.commands.vampirism.all: true
@@ -908,6 +911,8 @@ permissions:
         description: Allows access to the mmoedit command for other players
     mcmmo.commands.mmoupdate:
         description: Allows access to the mmoupdate command
+    mcmmo.commands.mmoshowdb:
+        description: Allows access to the mmoshowdb command
     mcmmo.commands.mobhealth:
          default: true
          description: Allows access to the mobhealth command