From c10525ada9ed2c0b77c518089a300e3b4e32e7c1 Mon Sep 17 00:00:00 2001 From: TfT_02 Date: Fri, 1 Aug 2014 20:17:15 +0200 Subject: [PATCH] Add UUID support! - zreed, slipcor, t00thpick1 This commit updates our database managers to use UUIDs instead of usernames for persistent storage. Fixes #1979 --- Changelog.txt | 1 + .../com/gmail/nossr50/api/ExperienceAPI.java | 288 ++++++++- .../database/ConvertDatabaseCommand.java | 2 +- .../gmail/nossr50/config/HiddenConfig.java | 6 + .../nossr50/database/DatabaseManager.java | 33 +- .../database/FlatfileDatabaseManager.java | 176 +++++- .../nossr50/database/SQLDatabaseManager.java | 588 ++++++++++++++---- .../datatypes/database/UpgradeType.java | 3 +- .../nossr50/datatypes/player/McMMOPlayer.java | 11 +- .../datatypes/player/PlayerProfile.java | 31 +- .../database/UUIDFetcherRunnable.java | 51 ++ .../database/UUIDUpdateAsyncTask.java | 73 +++ .../gmail/nossr50/util/uuid/UUIDFetcher.java | 100 +++ src/main/resources/hidden.yml | 3 + 14 files changed, 1225 insertions(+), 141 deletions(-) create mode 100644 src/main/java/com/gmail/nossr50/runnables/database/UUIDFetcherRunnable.java create mode 100644 src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java create mode 100644 src/main/java/com/gmail/nossr50/util/uuid/UUIDFetcher.java diff --git a/Changelog.txt b/Changelog.txt index 6dedfeca8..fc019e944 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -9,6 +9,7 @@ Key: Version 1.5.01-dev + Added new child skill; Salvage + + Added UUID support! + Added new feature to Herbalism. Instantly-regrown crops are protected from being broken for 1 second + Added option to config.yml to show the /mcstats scoreboard automatically after logging in + Added option to config.yml for Alchemy. Skills.Alchemy.Prevent_Hopper_Transfer_Bottles diff --git a/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java b/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java index 3d85b2253..aef29050b 100644 --- a/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java +++ b/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java @@ -1,6 +1,7 @@ package com.gmail.nossr50.api; import java.util.Set; +import java.util.UUID; import org.bukkit.entity.Player; @@ -86,6 +87,14 @@ public final class ExperienceAPI { UserManager.getPlayer(player).applyXpGain(getSkillType(skillType), XP, getXPGainReason(xpGainReason)); } + /** + * Adds raw XP to an offline player. + *
+ * This function is designed for API usage. + * + * @deprecated We're using float for our XP values now + * replaced by {@link #addRawXPOffline(String playerName, String skillType, float XP)} + */ @Deprecated public static void addRawXPOffline(String playerName, String skillType, int XP) { addRawXPOffline(playerName, skillType, (float) XP); @@ -96,6 +105,9 @@ public final class ExperienceAPI { *
* This function is designed for API usage. * + * @deprecated We're using uuids to get an offline player + * replaced by {@link #addRawXPOffline(UUID uuid, String skillType, float XP)} + * * @param playerName The player to add XP to * @param skillType The skill to add XP to * @param XP The amount of XP to add @@ -103,10 +115,27 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static void addRawXPOffline(String playerName, String skillType, float XP) { addOfflineXP(playerName, getSkillType(skillType), (int) Math.floor(XP)); } + /** + * Adds raw XP to an offline player. + *
+ * This function is designed for API usage. + * + * @param uuid The UUID of player to add XP to + * @param skillType The skill to add XP to + * @param XP The amount of XP to add + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + */ + public static void addRawXPOffline(UUID uuid, String skillType, float XP) { + addOfflineXP(uuid, getSkillType(skillType), (int) Math.floor(XP)); + } + /** * Adds XP to the player, calculates for XP Rate only. *
@@ -152,6 +181,7 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static void addMultipliedXPOffline(String playerName, String skillType, int XP) { addOfflineXP(playerName, getSkillType(skillType), (int) (XP * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier())); } @@ -203,6 +233,7 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static void addModifiedXPOffline(String playerName, String skillType, int XP) { SkillType skill = getSkillType(skillType); @@ -273,10 +304,28 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static int getOfflineXP(String playerName, String skillType) { return getOfflineProfile(playerName).getSkillXpLevel(getNonChildSkillType(skillType)); } + /** + * Get the amount of XP an offline player has in a specific skill. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get XP for + * @param skillType The skill to get XP for + * @return the amount of XP in a given skill + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static int getOfflineXP(UUID uuid, String skillType) { + return getOfflineProfile(uuid).getSkillXpLevel(getNonChildSkillType(skillType)); + } + /** * Get the raw amount of XP a player has in a specific skill. *
@@ -306,10 +355,28 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static float getOfflineXPRaw(String playerName, String skillType) { return getOfflineProfile(playerName).getSkillXpLevelRaw(getNonChildSkillType(skillType)); } + /** + * Get the raw amount of XP an offline player has in a specific skill. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get XP for + * @param skillType The skill to get XP for + * @return the amount of XP in a given skill + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static float getOfflineXPRaw(UUID uuid, String skillType) { + return getOfflineProfile(uuid).getSkillXpLevelRaw(getNonChildSkillType(skillType)); + } + /** * Get the total amount of XP needed to reach the next level. *
@@ -339,10 +406,28 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static int getOfflineXPToNextLevel(String playerName, String skillType) { return getOfflineProfile(playerName).getXpToLevel(getNonChildSkillType(skillType)); } + /** + * Get the total amount of XP an offline player needs to reach the next level. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get XP for + * @param skillType The skill to get XP for + * @return the total amount of XP needed to reach the next level + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static int getOfflineXPToNextLevel(UUID uuid, String skillType) { + return getOfflineProfile(uuid).getXpToLevel(getNonChildSkillType(skillType)); + } + /** * Get the amount of XP remaining until the next level. *
@@ -376,14 +461,34 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static int getOfflineXPRemaining(String playerName, String skillType) { SkillType skill = getNonChildSkillType(skillType); - PlayerProfile profile = getOfflineProfile(playerName); return profile.getXpToLevel(skill) - profile.getSkillXpLevel(skill); } + /** + * Get the amount of XP an offline player has left before leveling up. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get XP for + * @param skillType The skill to get XP for + * @return the amount of XP needed to reach the next level + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static float getOfflineXPRemaining(UUID uuid, String skillType) { + SkillType skill = getNonChildSkillType(skillType); + PlayerProfile profile = getOfflineProfile(uuid); + + return profile.getXpToLevel(skill) - profile.getSkillXpLevelRaw(skill); + } + /** * Add levels to a skill. *
@@ -411,6 +516,7 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static void addLevelOffline(String playerName, String skillType, int levels) { PlayerProfile profile = getOfflineProfile(playerName); SkillType skill = getSkillType(skillType); @@ -430,6 +536,37 @@ public final class ExperienceAPI { profile.scheduleAsyncSave(); } + /** + * Add levels to a skill for an offline player. + *
+ * This function is designed for API usage. + * + * @param uuid The player to add levels to + * @param skillType Type of skill to add levels to + * @param levels Number of levels to add + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + */ + public static void addLevelOffline(UUID uuid, String skillType, int levels) { + PlayerProfile profile = getOfflineProfile(uuid); + SkillType skill = getSkillType(skillType); + + if (skill.isChildSkill()) { + Set parentSkills = FamilyTree.getParents(skill); + + for (SkillType parentSkill : parentSkills) { + profile.addLevels(parentSkill, (levels / parentSkills.size())); + } + + profile.scheduleAsyncSave(); + return; + } + + profile.addLevels(skill, levels); + profile.scheduleAsyncSave(); + } + /** * Get the level a player has in a specific skill. *
@@ -457,10 +594,27 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static int getLevelOffline(String playerName, String skillType) { return getOfflineProfile(playerName).getSkillLevel(getSkillType(skillType)); } + /** + * Get the level an offline player has in a specific skill. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get the level for + * @param skillType The skill to get the level for + * @return the level of a given skill + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + */ + public static int getLevelOffline(UUID uuid, String skillType) { + return getOfflineProfile(uuid).getSkillLevel(getSkillType(skillType)); + } + /** * Gets the power level of a player. *
@@ -483,6 +637,7 @@ public final class ExperienceAPI { * * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static int getPowerLevelOffline(String playerName) { int powerLevel = 0; PlayerProfile profile = getOfflineProfile(playerName); @@ -494,6 +649,27 @@ public final class ExperienceAPI { return powerLevel; } + /** + * Gets the power level of an offline player. + *
+ * This function is designed for API usage. + * + * @param uuid The player to get the power level for + * @return the power level of the player + * + * @throws InvalidPlayerException if the given player does not exist in the database + */ + public static int getPowerLevelOffline(UUID uuid) { + int powerLevel = 0; + PlayerProfile profile = getOfflineProfile(uuid); + + for (SkillType type : SkillType.NON_CHILD_SKILLS) { + powerLevel += profile.getSkillLevel(type); + } + + return powerLevel; + } + /** * Get the level cap of a specific skill. *
@@ -533,10 +709,28 @@ public final class ExperienceAPI { * * @return the position on the leaderboard */ + @Deprecated public static int getPlayerRankSkill(String playerName, String skillType) { return mcMMO.getDatabaseManager().readRank(getOfflineProfile(playerName).getPlayerName()).get(getNonChildSkillType(skillType)); } + /** + * Get the position on the leaderboard of a player. + *
+ * This function is designed for API usage. + * + * @param uuid The name of the player to check + * @param skillType The skill to check + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + * + * @return the position on the leaderboard + */ + public static int getPlayerRankSkill(UUID uuid, String skillType) { + return mcMMO.getDatabaseManager().readRank(getOfflineProfile(uuid).getPlayerName()).get(getNonChildSkillType(skillType)); + } /** * Get the position on the power level leaderboard of a player. @@ -549,10 +743,26 @@ public final class ExperienceAPI { * * @return the position on the power level leaderboard */ + @Deprecated public static int getPlayerRankOverall(String playerName) { return mcMMO.getDatabaseManager().readRank(getOfflineProfile(playerName).getPlayerName()).get(null); } + /** + * Get the position on the power level leaderboard of a player. + *
+ * This function is designed for API usage. + * + * @param uuid The name of the player to check + * + * @throws InvalidPlayerException if the given player does not exist in the database + * + * @return the position on the power level leaderboard + */ + public static int getPlayerRankOverall(UUID uuid) { + return mcMMO.getDatabaseManager().readRank(getOfflineProfile(uuid).getPlayerName()).get(null); + } + /** * Sets the level of a player in a specific skill type. *
@@ -580,10 +790,27 @@ public final class ExperienceAPI { * @throws InvalidSkillException if the given skill is not valid * @throws InvalidPlayerException if the given player does not exist in the database */ + @Deprecated public static void setLevelOffline(String playerName, String skillType, int skillLevel) { getOfflineProfile(playerName).modifySkill(getSkillType(skillType), skillLevel); } + /** + * Sets the level of an offline player in a specific skill type. + *
+ * This function is designed for API usage. + * + * @param uuid The player to set the level of + * @param skillType The skill to set the level for + * @param skillLevel The value to set the level to + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + */ + public static void setLevelOffline(UUID uuid, String skillType, int skillLevel) { + getOfflineProfile(uuid).modifySkill(getSkillType(skillType), skillLevel); + } + /** * Sets the XP of a player in a specific skill type. *
@@ -613,10 +840,28 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static void setXPOffline(String playerName, String skillType, int newValue) { getOfflineProfile(playerName).setSkillXpLevel(getNonChildSkillType(skillType), newValue); } + /** + * Sets the XP of an offline player in a specific skill type. + *
+ * This function is designed for API usage. + * + * @param uuid The player to set the XP of + * @param skillType The skill to set the XP for + * @param newValue The value to set the XP to + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static void setXPOffline(UUID uuid, String skillType, int newValue) { + getOfflineProfile(uuid).setSkillXpLevel(getNonChildSkillType(skillType), newValue); + } + /** * Removes XP from a player in a specific skill type. *
@@ -646,12 +891,37 @@ public final class ExperienceAPI { * @throws InvalidPlayerException if the given player does not exist in the database * @throws UnsupportedOperationException if the given skill is a child skill */ + @Deprecated public static void removeXPOffline(String playerName, String skillType, int xp) { getOfflineProfile(playerName).removeXp(getNonChildSkillType(skillType), xp); } - // Utility methods follow. + /** + * Removes XP from an offline player in a specific skill type. + *
+ * This function is designed for API usage. + * + * @param uuid The player to change the XP of + * @param skillType The skill to change the XP for + * @param xp The amount of XP to remove + * + * @throws InvalidSkillException if the given skill is not valid + * @throws InvalidPlayerException if the given player does not exist in the database + * @throws UnsupportedOperationException if the given skill is a child skill + */ + public static void removeXPOffline(UUID uuid, String skillType, int xp) { + getOfflineProfile(uuid).removeXp(getNonChildSkillType(skillType), xp); + } + // Utility methods follow. + private static void addOfflineXP(UUID playerUniqueId, SkillType skill, int XP) { + PlayerProfile profile = getOfflineProfile(playerUniqueId); + + profile.addXp(skill, XP); + profile.save(); + } + + @Deprecated private static void addOfflineXP(String playerName, SkillType skill, int XP) { PlayerProfile profile = getOfflineProfile(playerName); @@ -659,8 +929,20 @@ public final class ExperienceAPI { profile.scheduleAsyncSave(); } + private static PlayerProfile getOfflineProfile(UUID uuid) { + PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, false); + + if (!profile.isLoaded()) { + throw new InvalidPlayerException(); + } + + return profile; + } + + @Deprecated private static PlayerProfile getOfflineProfile(String playerName) { - PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, false); + UUID uuid = mcMMO.p.getServer().getOfflinePlayer(playerName).getUniqueId(); + PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uuid, false); if (!profile.isLoaded()) { throw new InvalidPlayerException(); diff --git a/src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java b/src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java index 2bcea962c..7f85a8977 100644 --- a/src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/database/ConvertDatabaseCommand.java @@ -55,7 +55,7 @@ public class ConvertDatabaseCommand implements CommandExecutor { UserManager.clearAll(); for (Player player : mcMMO.p.getServer().getOnlinePlayers()) { - PlayerProfile profile = oldDatabase.loadPlayerProfile(player.getName(), false); + PlayerProfile profile = oldDatabase.loadPlayerProfile(player.getUniqueId(), false); if (profile.isLoaded()) { mcMMO.getDatabaseManager().saveUser(profile); diff --git a/src/main/java/com/gmail/nossr50/config/HiddenConfig.java b/src/main/java/com/gmail/nossr50/config/HiddenConfig.java index ffb21ca45..124aeb733 100644 --- a/src/main/java/com/gmail/nossr50/config/HiddenConfig.java +++ b/src/main/java/com/gmail/nossr50/config/HiddenConfig.java @@ -12,6 +12,7 @@ public class HiddenConfig { private static int conversionRate; private static boolean useEnchantmentBuffs; private static boolean resendChunksAfterBlockAbility; + private static int uuidConvertAmount; public HiddenConfig(String fileName) { HiddenConfig.fileName = fileName; @@ -33,6 +34,7 @@ public class HiddenConfig { conversionRate = config.getInt("Options.ConversionRate", 1); useEnchantmentBuffs = config.getBoolean("Options.EnchantmentBuffs", true); resendChunksAfterBlockAbility = config.getBoolean("Options.RefreshChunks", false); + uuidConvertAmount = config.getInt("Options.UUIDConvertAmount", 5); } } @@ -51,4 +53,8 @@ public class HiddenConfig { public boolean resendChunksAfterBlockAbility() { return resendChunksAfterBlockAbility; } + + public int getUUIDConvertAmount() { + return uuidConvertAmount; + } } diff --git a/src/main/java/com/gmail/nossr50/database/DatabaseManager.java b/src/main/java/com/gmail/nossr50/database/DatabaseManager.java index 5d99fde64..af0521b58 100644 --- a/src/main/java/com/gmail/nossr50/database/DatabaseManager.java +++ b/src/main/java/com/gmail/nossr50/database/DatabaseManager.java @@ -2,6 +2,7 @@ package com.gmail.nossr50.database; import java.util.List; import java.util.Map; +import java.util.UUID; import com.gmail.nossr50.config.Config; import com.gmail.nossr50.datatypes.database.DatabaseType; @@ -67,19 +68,45 @@ public interface DatabaseManager { * * @param playerName The name of the player to be added to the database */ - public void newUser(String playerName); + public void newUser(String playerName, String uuid); /** * Load a player from the database. * + * @deprecated replaced by {@link #loadPlayerProfile(UUID uuid, boolean createNew)} + * * @param playerName The name of the player to load from the database * @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 */ + @Deprecated public PlayerProfile loadPlayerProfile(String playerName, boolean createNew); + /** + * Load a player from the database. + * + * @param uuid The uuid of the player to load from the database + * @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 PlayerProfile loadPlayerProfile(UUID uuid, boolean createNew); + + /** + * Load a player from the database. Attempt to use uuid, fall back on playername + * + * @param playerName The name of the player to load from the database + * @param uuid The uuid of the player to load from the database + * @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 PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean createNew); + /** * Get all users currently stored in the database. * @@ -95,6 +122,10 @@ public interface DatabaseManager { */ public void convertUsers(DatabaseManager destination); + public boolean saveUserUUID(String userName, UUID uuid); + + public boolean saveUserUUIDs(Map fetchedUUIDs); + /** * Retrieve the type of database in use. Custom databases should return CUSTOM. * diff --git a/src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java b/src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java index 7cd35d99e..e5d4588eb 100644 --- a/src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java +++ b/src/main/java/com/gmail/nossr50/database/FlatfileDatabaseManager.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.UUID; import org.bukkit.OfflinePlayer; @@ -26,6 +27,7 @@ import com.gmail.nossr50.datatypes.database.UpgradeType; import com.gmail.nossr50.datatypes.player.PlayerProfile; import com.gmail.nossr50.datatypes.skills.AbilityType; import com.gmail.nossr50.datatypes.skills.SkillType; +import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.StringUtils; @@ -44,6 +46,10 @@ public final class FlatfileDatabaseManager implements DatabaseManager { usersFile = new File(mcMMO.getUsersFilePath()); checkStructure(); updateLeaderboards(); + + if (mcMMO.getUpgradeManager().shouldUpgrade(UpgradeType.ADD_UUIDS)) { + new UUIDUpdateAsyncTask(mcMMO.p, getStoredUsers()).runTaskAsynchronously(mcMMO.p); + } } public void purgePowerlessUsers() { @@ -209,6 +215,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { public boolean saveUser(PlayerProfile profile) { String playerName = profile.getPlayerName(); + UUID uuid = profile.getUniqueId(); BufferedReader in = null; FileWriter out = null; @@ -223,8 +230,9 @@ public final class FlatfileDatabaseManager implements DatabaseManager { // 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)) { + // Read the line in and copy it to the output if it's not the player we want to edit + String[] character = line.split(":"); + if (!character[41].equalsIgnoreCase(uuid.toString()) && !character[0].equalsIgnoreCase(playerName)) { writer.append(line).append("\r\n"); } else { @@ -271,6 +279,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { writer.append(mobHealthbarType == null ? Config.getInstance().getMobHealthbarDefault().toString() : mobHealthbarType.toString()).append(":"); writer.append(profile.getSkillLevel(SkillType.ALCHEMY)).append(":"); writer.append(profile.getSkillXpLevel(SkillType.ALCHEMY)).append(":"); + writer.append(uuid.toString()).append(":"); writer.append("\r\n"); } } @@ -313,7 +322,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { return skills; } - public void newUser(String playerName) { + public void newUser(String playerName, String uuid) { BufferedWriter out = null; synchronized (fileWritingLock) { try { @@ -362,7 +371,8 @@ public final class FlatfileDatabaseManager implements DatabaseManager { out.append(Config.getInstance().getMobHealthbarDefault().toString()).append(":"); // Mob Healthbar HUD out.append("0:"); // Alchemy out.append("0:"); // AlchemyXp - + out.append(uuid).append(":"); // UUID + // Add more in the same format as the line above out.newLine(); @@ -376,7 +386,20 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } } + @Deprecated public PlayerProfile loadPlayerProfile(String playerName, boolean create) { + return loadPlayerProfile(playerName, "", create); + } + + public PlayerProfile loadPlayerProfile(UUID uuid, boolean create) { + return loadPlayerProfile("", uuid.toString(), create); + } + + public PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean create) { + return loadPlayerProfile(playerName, uuid.toString(), create); + } + + private PlayerProfile loadPlayerProfile(String playerName, String uuid, boolean create) { BufferedReader in = null; String usersFilePath = mcMMO.getUsersFilePath(); @@ -390,17 +413,28 @@ public final class FlatfileDatabaseManager implements DatabaseManager { // Find if the line contains the player we want. String[] character = line.split(":"); - if (!character[0].equalsIgnoreCase(playerName)) { + if (!character[41].equalsIgnoreCase(uuid) && !character[0].equalsIgnoreCase(playerName)) { continue; } + // Update playerName in database after name change + if (!character[0].equalsIgnoreCase(playerName)) { + mcMMO.p.debug("Name change detected: " + character[0] + " => " + playerName); + character[0] = playerName; + } + return loadFromLine(character); } // Didn't find the player, create a new one if (create) { - newUser(playerName); - return new PlayerProfile(playerName, true); + if (uuid.isEmpty()) { + newUser(playerName, uuid); + return new PlayerProfile(playerName, true); + } + + newUser(playerName, uuid); + return new PlayerProfile(playerName, UUID.fromString(uuid), true); } } catch (Exception e) { @@ -421,7 +455,11 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } // Return unloaded profile - return new PlayerProfile(playerName); + if (uuid.isEmpty()) { + return new PlayerProfile(playerName); + } + + return new PlayerProfile(playerName, UUID.fromString(uuid)); } public void convertUsers(DatabaseManager destination) { @@ -458,6 +496,91 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } } + public boolean saveUserUUID(String userName, UUID uuid) { + boolean worked = false; + + BufferedReader in = null; + FileWriter out = null; + String usersFilePath = mcMMO.getUsersFilePath(); + + synchronized (fileWritingLock) { + try { + in = new BufferedReader(new FileReader(usersFilePath)); + StringBuilder writer = new StringBuilder(); + String line; + + while ((line = in.readLine()) != null) { + String[] character = line.split(":"); + if (!worked && character[0].equalsIgnoreCase(userName)) { + if (character.length < 42) { + mcMMO.p.getLogger().severe("Could not update UUID for " + userName + "!"); + mcMMO.p.getLogger().severe("Database entry is invalid."); + break; + } + + line = line.replace(character[41], uuid.toString()); + worked = true; + } + + writer.append(line).append("\r\n"); + } + + out = new FileWriter(usersFilePath); // Write out the new file + out.write(writer.toString()); + } + catch (Exception e) { + mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString()); + } + finally { + tryClose(in); + tryClose(out); + } + } + + return worked; + } + + public boolean saveUserUUIDs(Map fetchedUUIDs) { + BufferedReader in = null; + FileWriter out = null; + String usersFilePath = mcMMO.getUsersFilePath(); + + synchronized (fileWritingLock) { + try { + in = new BufferedReader(new FileReader(usersFilePath)); + StringBuilder writer = new StringBuilder(); + String line; + + while (((line = in.readLine()) != null) && !fetchedUUIDs.isEmpty()) { + String[] character = line.split(":"); + if (fetchedUUIDs.containsKey(character[0])) { + if (character.length < 42) { + mcMMO.p.getLogger().severe("Could not update UUID for " + character[0] + "!"); + mcMMO.p.getLogger().severe("Database entry is invalid."); + return false; + } + + line = line.replace(character[41], fetchedUUIDs.remove(character[0]).toString()); + } + + writer.append(line).append("\r\n"); + } + + out = new FileWriter(usersFilePath); // Write out the new file + out.write(writer.toString()); + } + catch (Exception e) { + mcMMO.p.getLogger().severe("Exception while reading " + usersFilePath + " (Are you sure you formatted it correctly?)" + e.toString()); + } + finally { + tryClose(in); + tryClose(out); + } + } + + return true; + } + public List getStoredUsers() { ArrayList users = new ArrayList(); BufferedReader in = null; @@ -598,6 +721,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { in = new BufferedReader(new FileReader(usersFilePath)); StringBuilder writer = new StringBuilder(); String line; + HashSet usernames = new HashSet(); HashSet players = new HashSet(); while ((line = in.readLine()) != null) { @@ -612,8 +736,13 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } String[] character = line.split(":"); + // Prevent the same username from being present multiple times + if (!usernames.add(character[0])) { + continue; + } + // Prevent the same player from being present multiple times - if (!players.add(character[0])) { + if (character.length == 42 && (!character[41].isEmpty() && !players.add(character[41]))) { continue; } @@ -645,7 +774,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } // If they're valid, rewrite them to the file. - if (character.length == 41) { + if (character.length == 42) { writer.append(line).append("\r\n"); continue; } @@ -699,16 +828,25 @@ public final class FlatfileDatabaseManager implements DatabaseManager { oldVersion = "1.4.08"; } } + if (character.length <= 41) { + // Addition of UUIDs + // Version 1.5.01 + // Add a space because otherwise it gets removed + newLine.append(" :"); + if (oldVersion == null) { + oldVersion = "1.5.01"; + } + } // Remove any blanks that shouldn't be there, and validate the other fields String[] newCharacter = newLine.toString().split(":"); boolean corrupted = false; for (int i = 0; i < newCharacter.length; i++) { - if (newCharacter[i].isEmpty() && !(i == 2 || i == 3 || i == 23 || i == 33)) { + if (newCharacter[i].isEmpty() && !(i == 2 || i == 3 || i == 23 || i == 33 || i == 41)) { corrupted = true; - if (newCharacter.length != 41) { + if (newCharacter.length != 42) { newCharacter = (String[]) ArrayUtils.remove(newCharacter, i); } else { @@ -729,7 +867,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { newCharacter[i] = Config.getInstance().getMobHealthbarDefault().toString(); } - if (!StringUtils.isInt(newCharacter[i]) && !(i == 0 || i == 2 || i == 3 || i == 23 || i == 33 || i == 38)) { + if (!StringUtils.isInt(newCharacter[i]) && !(i == 0 || i == 2 || i == 3 || i == 23 || i == 33 || i == 38 || i == 41)) { corrupted = true; newCharacter[i] = "0"; } @@ -740,7 +878,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager { } if (oldVersion != null) { - mcMMO.p.debug("Updating database line for player " + character[0] + " from before version " + oldVersion); + mcMMO.p.debug("Updating database line from before version " + oldVersion + " for player " + character[0]); } if (corrupted || oldVersion != null) { @@ -869,7 +1007,15 @@ public final class FlatfileDatabaseManager implements DatabaseManager { mobHealthbarType = Config.getInstance().getMobHealthbarDefault(); } - return new PlayerProfile(character[0], skills, skillsXp, skillsDATS, mobHealthbarType); + UUID uuid; + try { + uuid = UUID.fromString(character[41]); + } + catch (Exception e) { + uuid = null; + } + + return new PlayerProfile(character[0], uuid, skills, skillsXp, skillsDATS, mobHealthbarType); } private Map getSkillMapFromLine(String[] character) { diff --git a/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java b/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java index 84e3027f5..0ee9e1b90 100644 --- a/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java +++ b/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.UUID; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.config.Config; @@ -25,6 +26,7 @@ import com.gmail.nossr50.datatypes.skills.AbilityType; import com.gmail.nossr50.datatypes.skills.SkillType; import com.gmail.nossr50.runnables.database.SQLDatabaseKeepaliveTask; import com.gmail.nossr50.runnables.database.SQLReconnectTask; +import com.gmail.nossr50.runnables.database.UUIDUpdateAsyncTask; import com.gmail.nossr50.util.Misc; public final class SQLDatabaseManager implements DatabaseManager { @@ -123,7 +125,7 @@ public final class SQLDatabaseManager implements DatabaseManager { int userId = readId(profile.getPlayerName()); if (userId == -1) { - newUser(profile.getPlayerName()); + newUser(profile.getPlayerName(), profile.getUniqueId().toString()); userId = readId(profile.getPlayerName()); if (userId == -1) { return false; @@ -132,6 +134,7 @@ public final class SQLDatabaseManager implements DatabaseManager { boolean success = true; MobHealthbarType mobHealthbarType = profile.getMobHealthbarType(); + success &= saveUniqueId(userId, profile.getUniqueId().toString()); success &= saveLogin(userId, ((int) (System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR))); success &= saveHuds(userId, (mobHealthbarType == null ? Config.getInstance().getMobHealthbarDefault().toString() : mobHealthbarType.toString())); success &= saveLongs( @@ -317,7 +320,7 @@ public final class SQLDatabaseManager implements DatabaseManager { return skills; } - public void newUser(String playerName) { + public void newUser(String playerName, String uuid) { if (!checkConnected()) { return; } @@ -325,9 +328,10 @@ public final class SQLDatabaseManager implements DatabaseManager { PreparedStatement statement = null; try { - statement = connection.prepareStatement("INSERT INTO " + tablePrefix + "users (user, lastlogin) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS); + statement = connection.prepareStatement("INSERT INTO " + tablePrefix + "users (user, uuid, lastlogin) VALUES (?, ?, ?)", Statement.RETURN_GENERATED_KEYS); statement.setString(1, playerName); - statement.setLong(2, System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR); + statement.setString(2, uuid); + statement.setLong(3, System.currentTimeMillis() / Misc.TIME_CONVERSION_FACTOR); statement.execute(); int id = readId(playerName); @@ -348,13 +352,18 @@ public final class SQLDatabaseManager implements DatabaseManager { } } - public PlayerProfile loadPlayerProfile(String playerName, boolean create) { - return loadPlayerProfile(playerName, create, true); - } - - private PlayerProfile loadPlayerProfile(String playerName, boolean create, boolean retry) { + /** + * This is a fallback method to provide the old way of getting a PlayerProfile + * in case there is no UUID match found + */ + private PlayerProfile loadPlayerNameProfile(String playerName, String uuid, boolean create, boolean retry) { if (!checkConnected()) { - return new PlayerProfile(playerName, false); // return fake profile if not connected + // return fake profile if not connected + if (uuid.isEmpty()) { + return new PlayerProfile(playerName, false); + } + + return new PlayerProfile(playerName, UUID.fromString(uuid), false); } PreparedStatement statement = null; @@ -365,7 +374,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, " + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, " + "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.mobhealthbar " + + "h.mobhealthbar, u.uuid " + "FROM " + tablePrefix + "users u " + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) " @@ -415,8 +424,8 @@ public final class SQLDatabaseManager implements DatabaseManager { if (id == -1) { // There is no such user if (create) { - newUser(playerName); - return loadPlayerProfile(playerName, false, false); + newUser(playerName, uuid); + return loadPlayerNameProfile(playerName, uuid, false, false); } // Return unloaded profile if can't create @@ -425,7 +434,113 @@ public final class SQLDatabaseManager implements DatabaseManager { // There is such a user writeMissingRows(id); // Retry, and abort on re-failure - return loadPlayerProfile(playerName, create, false); + return loadPlayerNameProfile(playerName, uuid, create, false); + } + + @Deprecated + public PlayerProfile loadPlayerProfile(String playerName, boolean create) { + return loadPlayerProfile(playerName, "", create, true); + } + + public PlayerProfile loadPlayerProfile(UUID uuid, boolean create) { + return loadPlayerProfile("", uuid.toString(), create, true); + } + + public PlayerProfile loadPlayerProfile(String playerName, UUID uuid, boolean create) { + return loadPlayerProfile(playerName, uuid.toString(), create, true); + } + + private PlayerProfile loadPlayerProfile(String playerName, String uuid, boolean create, boolean retry) { + if (!checkConnected()) { + // return fake profile if not connected + if (uuid.isEmpty()) { + return new PlayerProfile(playerName, false); + } + + return new PlayerProfile(playerName, UUID.fromString(uuid), false); + } + + 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, s.alchemy, " + + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, " + + "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.mobhealthbar, u.uuid " + + "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.UUID = ?"); + statement.setString(1, uuid); + + ResultSet result = statement.executeQuery(); + + if (result.next()) { + try { + PlayerProfile profile = loadFromResult(playerName, result); + result.close(); + + if (!playerName.isEmpty() && !profile.getPlayerName().isEmpty()) { + statement = connection.prepareStatement( + "UPDATE `" + tablePrefix + "users` " + + "SET user = ? " + + "WHERE UUID = ?"); + statement.setString(1, playerName); + statement.setString(2, uuid); + result = statement.executeQuery(); + result.close(); + } + + return profile; + } + catch (SQLException e) { + } + } + result.close(); + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (statement != null) { + try { + statement.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + + // Problem, nothing was returned + + // Retry the old fashioned way if this is second time around + if (!retry) { + return loadPlayerNameProfile(playerName, uuid, create, true); + } + + // 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, uuid); + return loadPlayerProfile(playerName, uuid, false, false); + } + + // Return unloaded profile if can't create + return new PlayerProfile(playerName, false); + } + // There is such a user + writeMissingRows(id); + // Retry, and abort on re-failure + return loadPlayerProfile(playerName, uuid, create, false); } public void convertUsers(DatabaseManager destination) { @@ -441,7 +556,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, " + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, " + "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.mobhealthbar " + + "h.mobhealthbar, u.uuid " + "FROM " + tablePrefix + "users u " + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) " @@ -483,6 +598,88 @@ public final class SQLDatabaseManager implements DatabaseManager { } + public boolean saveUserUUID(String userName, UUID uuid) { + if (!checkConnected()) { + // return false + return false; + } + + PreparedStatement statement = null; + + try { + statement = connection.prepareStatement( + "UPDATE `" + tablePrefix + "users` SET " + + " uuid = ? WHERE user = ?"); + statement.setString(1, uuid.toString()); + statement.setString(2, userName); + statement.execute(); + return true; + } + catch (SQLException ex) { + printErrors(ex); + return false; + } + finally { + if (statement != null) { + try { + statement.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + + // Problem, nothing was returned + } + + public boolean saveUserUUIDs(Map fetchedUUIDs) { + if (!checkConnected()) { + return false; + } + + PreparedStatement statement = null; + int count = 0; + + try { + statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE user = ?"); + + for (Map.Entry entry : fetchedUUIDs.entrySet()) { + statement.setString(1, entry.getValue().toString()); + statement.setString(2, entry.getKey()); + + statement.addBatch(); + + count++; + + if ((count % 500) == 0) { + statement.executeBatch(); + count = 0; + } + } + + if (count != 0) { + statement.executeBatch(); + } + + return true; + } + catch (SQLException ex) { + printErrors(ex); + return false; + } + finally { + if (statement != null) { + try { + statement.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + } + /** * Check connection status and re-establish if dead or stale. *

@@ -651,6 +848,7 @@ public final class SQLDatabaseManager implements DatabaseManager { write("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` (" + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT," + "`user` varchar(40) NOT NULL," + + "`uuid` varchar(36) NOT NULL DEFAULT ''," + "`lastlogin` int(32) unsigned NOT NULL," + "PRIMARY KEY (`id`)," + "UNIQUE KEY `user` (`user`)) DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;"); @@ -737,122 +935,43 @@ public final class SQLDatabaseManager implements DatabaseManager { } Statement statement = null; - ResultSet resultSet = null; try { statement = connection.createStatement(); switch (upgrade) { case ADD_FISHING: - try { - statement.executeQuery("SELECT `fishing` FROM `" + tablePrefix + "skills` LIMIT 1"); - } - catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "skills` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "experience` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); - } + checkUpgradeAddFishing(statement); break; case ADD_BLAST_MINING_COOLDOWN: - try { - statement.executeQuery("SELECT `blast_mining` FROM `" + tablePrefix + "cooldowns` LIMIT 1"); - } - catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `blast_mining` int(32) NOT NULL DEFAULT '0'"); - } + checkUpgradeAddBlastMiningCooldown(statement); break; case ADD_SQL_INDEXES: - resultSet = statement.executeQuery("SHOW INDEX FROM `" + tablePrefix + "skills` WHERE `Key_name` LIKE 'idx\\_%'"); - resultSet.last(); - - if (resultSet.getRow() != SkillType.NON_CHILD_SKILLS.size()) { - mcMMO.p.getLogger().info("Indexing tables, this may take a while on larger databases"); - - for (SkillType skill : SkillType.NON_CHILD_SKILLS) { - String skill_name = skill.name().toLowerCase(); - - try { - statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_" + skill_name + "` (`" + skill_name + "`) USING BTREE"); - } - catch (SQLException ex) { - // Ignore - } - } - } + checkUpgradeAddSQLIndexes(statement); break; case ADD_MOB_HEALTHBARS: - try { - statement.executeQuery("SELECT `mobhealthbar` FROM `" + tablePrefix + "huds` LIMIT 1"); - } - catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + Config.getInstance().getMobHealthbarDefault() + "'"); - } + checkUpgradeAddMobHealthbars(statement); break; case DROP_SQL_PARTY_NAMES: - try { - resultSet = statement.executeQuery("SELECT * FROM `" + tablePrefix + "users` LIMIT 1"); - - ResultSetMetaData rsmeta = resultSet.getMetaData(); - boolean column_exists = false; - - for (int i = 1; i <= rsmeta.getColumnCount(); i++) { - if (rsmeta.getColumnName(i).equalsIgnoreCase("party")) { - column_exists = true; - break; - } - } - - if (column_exists) { - mcMMO.p.getLogger().info("Removing party name from users table..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`"); - } - } - catch (SQLException ex) { - // Ignore - } + checkUpgradeDropPartyNames(statement); break; case DROP_SPOUT: - try { - resultSet = statement.executeQuery("SELECT * FROM `" + tablePrefix + "huds` LIMIT 1"); - - ResultSetMetaData rsmeta = resultSet.getMetaData(); - boolean column_exists = false; - - for (int i = 1; i <= rsmeta.getColumnCount(); i++) { - if (rsmeta.getColumnName(i).equalsIgnoreCase("hudtype")) { - column_exists = true; - break; - } - } - - if (column_exists) { - mcMMO.p.getLogger().info("Removing Spout HUD type from huds table..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`"); - } - } - catch (SQLException ex) { - // Ignore - } + checkUpgradeDropSpout(statement); break; case ADD_ALCHEMY: - try { - statement.executeQuery("SELECT `alchemy` FROM `" + tablePrefix + "skills` LIMIT 1"); - } - catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy..."); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "skills` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); - statement.executeQuery("ALTER TABLE `" + tablePrefix + "experience` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); - } + checkUpgradeAddAlchemy(statement); break; + case ADD_UUIDS: + checkUpgradeAddUUIDs(statement); + return; + default: break; @@ -861,17 +980,9 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(upgrade); } catch (SQLException ex) { - + printErrors(ex); } finally { - if (resultSet != null) { - try { - resultSet.close(); - } - catch (SQLException e) { - // Ignore - } - } if (statement != null) { try { statement.close(); @@ -1166,6 +1277,32 @@ public final class SQLDatabaseManager implements DatabaseManager { return id; } + private boolean saveUniqueId(int id, String uuid) { + PreparedStatement statement = null; + + try { + statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE id = ?"); + statement.setString(1, uuid); + statement.setInt(2, id); + statement.execute(); + return true; + } + catch (SQLException ex) { + printErrors(ex); + return false; + } + finally { + if (statement != null) { + try { + statement.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + } + private boolean saveLogin(int id, long login) { PreparedStatement statement = null; @@ -1223,6 +1360,7 @@ public final class SQLDatabaseManager implements DatabaseManager { Map skillsXp = new HashMap(); // Skill & XP Map skillsDATS = new HashMap(); // Ability & Cooldown MobHealthbarType mobHealthbarType; + UUID uuid; final int OFFSET_SKILLS = 0; // TODO update these numbers when the query changes (a new skill is added) final int OFFSET_XP = 13; @@ -1277,7 +1415,14 @@ public final class SQLDatabaseManager implements DatabaseManager { mobHealthbarType = Config.getInstance().getMobHealthbarDefault(); } - return new PlayerProfile(playerName, skills, skillsXp, skillsDATS, mobHealthbarType); + try { + uuid = UUID.fromString(result.getString(OFFSET_OTHER + 3)); + } + catch (Exception e) { + uuid = null; + } + + return new PlayerProfile(playerName, uuid, skills, skillsXp, skillsDATS, mobHealthbarType); } private void printErrors(SQLException ex) { @@ -1289,4 +1434,217 @@ public final class SQLDatabaseManager implements DatabaseManager { public DatabaseType getDatabaseType() { return DatabaseType.SQL; } + + private void checkUpgradeAddAlchemy(final Statement statement) throws SQLException { + try { + statement.executeQuery("SELECT `alchemy` FROM `" + tablePrefix + "skills` LIMIT 1"); + } + catch (SQLException ex) { + mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); + } + } + + private void checkUpgradeAddBlastMiningCooldown(final Statement statement) throws SQLException { + try { + statement.executeQuery("SELECT `blast_mining` FROM `" + tablePrefix + "cooldowns` LIMIT 1"); + } + catch (SQLException ex) { + mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `blast_mining` int(32) NOT NULL DEFAULT '0'"); + } + } + + private void checkUpgradeAddFishing(final Statement statement) throws SQLException { + try { + statement.executeQuery("SELECT `fishing` FROM `" + tablePrefix + "skills` LIMIT 1"); + } + catch (SQLException ex) { + mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); + } + } + + private void checkUpgradeAddMobHealthbars(final Statement statement) throws SQLException { + try { + statement.executeQuery("SELECT `mobhealthbar` FROM `" + tablePrefix + "huds` LIMIT 1"); + } + catch (SQLException ex) { + mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + Config.getInstance().getMobHealthbarDefault() + "'"); + } + } + + private void checkUpgradeAddSQLIndexes(final Statement statement) throws SQLException { + ResultSet resultSet = null; + + try { + resultSet = statement.executeQuery("SHOW INDEX FROM `" + tablePrefix + "skills` WHERE `Key_name` LIKE 'idx\\_%'"); + resultSet.last(); + + if (resultSet.getRow() != SkillType.NON_CHILD_SKILLS.size()) { + mcMMO.p.getLogger().info("Indexing tables, this may take a while on larger databases"); + + for (SkillType skill : SkillType.NON_CHILD_SKILLS) { + String skill_name = skill.name().toLowerCase(); + + try { + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_" + skill_name + "` (`" + skill_name + "`) USING BTREE"); + } + catch (SQLException ex) { + // Ignore + } + } + } + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (resultSet != null) { + try { + resultSet.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + } + + private void checkUpgradeAddUUIDs(final Statement statement) { + List names = new ArrayList(); + ResultSet resultSet = null; + + try { + resultSet = statement.executeQuery("SELECT * FROM `" + tablePrefix + "users` LIMIT 1"); + + ResultSetMetaData rsmeta = resultSet.getMetaData(); + boolean column_exists = false; + + for (int i = 1; i <= rsmeta.getColumnCount(); i++) { + if (rsmeta.getColumnName(i).equalsIgnoreCase("uuid")) { + column_exists = true; + break; + } + } + + if (!column_exists) { + mcMMO.p.getLogger().info("Adding UUIDs to mcMMO MySQL user table..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD `uuid` varchar(36) NOT NULL DEFAULT ''"); + } + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (resultSet != null) { + try { + resultSet.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + + try { + resultSet = statement.executeQuery("SELECT `user` FROM `" + tablePrefix + "users` WHERE `uuid` = ''"); + + while (resultSet.next()) { + names.add(resultSet.getString("user")); + } + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (resultSet != null) { + try { + resultSet.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + + if (!names.isEmpty()) { + new UUIDUpdateAsyncTask(mcMMO.p, names).runTaskAsynchronously(mcMMO.p); + } + } + + private void checkUpgradeDropPartyNames(final Statement statement) { + ResultSet resultSet = null; + + try { + resultSet = statement.executeQuery("SELECT * FROM `" + tablePrefix + "users` LIMIT 1"); + + ResultSetMetaData rsmeta = resultSet.getMetaData(); + boolean column_exists = false; + + for (int i = 1; i <= rsmeta.getColumnCount(); i++) { + if (rsmeta.getColumnName(i).equalsIgnoreCase("party")) { + column_exists = true; + break; + } + } + + if (column_exists) { + mcMMO.p.getLogger().info("Removing party name from users table..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`"); + } + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (resultSet != null) { + try { + resultSet.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + } + + private void checkUpgradeDropSpout(final Statement statement) { + ResultSet resultSet = null; + + try { + resultSet = statement.executeQuery("SELECT * FROM `" + tablePrefix + "huds` LIMIT 1"); + + ResultSetMetaData rsmeta = resultSet.getMetaData(); + boolean column_exists = false; + + for (int i = 1; i <= rsmeta.getColumnCount(); i++) { + if (rsmeta.getColumnName(i).equalsIgnoreCase("hudtype")) { + column_exists = true; + break; + } + } + + if (column_exists) { + mcMMO.p.getLogger().info("Removing Spout HUD type from huds table..."); + statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`"); + } + } + catch (SQLException ex) { + printErrors(ex); + } + finally { + if (resultSet != null) { + try { + resultSet.close(); + } + catch (SQLException e) { + // Ignore + } + } + } + } } diff --git a/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java b/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java index 1f901e68a..0d85067df 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/database/UpgradeType.java @@ -7,5 +7,6 @@ public enum UpgradeType { ADD_MOB_HEALTHBARS, DROP_SQL_PARTY_NAMES, DROP_SPOUT, - ADD_ALCHEMY; + ADD_ALCHEMY, + ADD_UUIDS; } diff --git a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java index aa05c978c..148c802d7 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java +++ b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java @@ -3,6 +3,7 @@ package com.gmail.nossr50.datatypes.player; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.UUID; import org.bukkit.GameMode; import org.bukkit.Location; @@ -94,13 +95,18 @@ public class McMMOPlayer { public McMMOPlayer(Player player) { String playerName = player.getName(); + UUID uuid = player.getUniqueId(); this.player = player; playerMetadata = new FixedMetadataValue(mcMMO.p, playerName); - profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, true); + profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, uuid, true); party = PartyManager.getPlayerParty(playerName); ptpRecord = new PartyTeleportRecord(); + if (profile.getUniqueId() == null) { + profile.setUniqueId(uuid); + } + /* * 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. @@ -134,6 +140,7 @@ public class McMMOPlayer { private class RetryProfileLoadingTask extends BukkitRunnable { private static final int MAX_TRIES = 5; private final String playerName = McMMOPlayer.this.player.getName(); + private final UUID uniqueId = McMMOPlayer.this.player.getUniqueId(); private int attempt = 0; // WARNING: ASYNC TASK @@ -154,7 +161,7 @@ public class McMMOPlayer { // Increment attempt counter and try attempt++; - PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(playerName, true); + PlayerProfile profile = mcMMO.getDatabaseManager().loadPlayerProfile(uniqueId, true); // If successful, schedule the apply if (profile.isLoaded()) { new ApplySuccessfulProfile(profile).runTask(mcMMO.p); diff --git a/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java b/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java index 7d1a3941f..c7b95e2a7 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java +++ b/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java @@ -3,6 +3,7 @@ package com.gmail.nossr50.datatypes.player; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.UUID; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.config.Config; @@ -19,6 +20,7 @@ import com.google.common.collect.ImmutableMap; public class PlayerProfile { private final String playerName; + private UUID uuid; private boolean loaded; private volatile boolean changed; @@ -30,7 +32,13 @@ public class PlayerProfile { private final Map skillsXp = new HashMap(); // Skill & XP private final Map abilityDATS = new HashMap(); // Ability & Cooldown + @Deprecated public PlayerProfile(String playerName) { + this(playerName, null); + } + + public PlayerProfile(String playerName, UUID uuid) { + this.uuid = uuid; this.playerName = playerName; mobHealthbarType = Config.getInstance().getMobHealthbarDefault(); @@ -45,13 +53,20 @@ public class PlayerProfile { } } + @Deprecated public PlayerProfile(String playerName, boolean isLoaded) { this(playerName); this.loaded = isLoaded; } - public PlayerProfile(String playerName, Map levelData, Map xpData, Map cooldownData, MobHealthbarType mobHealthbarType) { + public PlayerProfile(String playerName, UUID uuid, boolean isLoaded) { + this(playerName, uuid); + this.loaded = isLoaded; + } + + public PlayerProfile(String playerName, UUID uuid, Map levelData, Map xpData, Map cooldownData, MobHealthbarType mobHealthbarType) { this.playerName = playerName; + this.uuid = uuid; this.mobHealthbarType = mobHealthbarType; skills.putAll(levelData); @@ -71,11 +86,11 @@ public class PlayerProfile { } // TODO should this part be synchronized? - PlayerProfile profileCopy = new PlayerProfile(playerName, ImmutableMap.copyOf(skills), ImmutableMap.copyOf(skillsXp), ImmutableMap.copyOf(abilityDATS), mobHealthbarType); + PlayerProfile profileCopy = new PlayerProfile(playerName, uuid, ImmutableMap.copyOf(skills), ImmutableMap.copyOf(skillsXp), ImmutableMap.copyOf(abilityDATS), mobHealthbarType); changed = !mcMMO.getDatabaseManager().saveUser(profileCopy); if (changed) { - mcMMO.p.getLogger().warning("PlayerProfile for " + playerName + " failed to save"); + mcMMO.p.getLogger().warning("PlayerProfile saving failed for player: " + playerName + " " + uuid); } } @@ -83,6 +98,16 @@ public class PlayerProfile { return playerName; } + public UUID getUniqueId() { + return uuid; + } + + public void setUniqueId(UUID uuid) { + changed = true; + + this.uuid = uuid; + } + public boolean isLoaded() { return loaded; } diff --git a/src/main/java/com/gmail/nossr50/runnables/database/UUIDFetcherRunnable.java b/src/main/java/com/gmail/nossr50/runnables/database/UUIDFetcherRunnable.java new file mode 100644 index 000000000..485e0f855 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/runnables/database/UUIDFetcherRunnable.java @@ -0,0 +1,51 @@ +package com.gmail.nossr50.runnables.database; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import org.bukkit.scheduler.BukkitRunnable; + +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.uuid.UUIDFetcher; + +public class UUIDFetcherRunnable extends BukkitRunnable { + private List names; + + public UUIDFetcherRunnable(List names) { + this.names = names; + } + + public UUIDFetcherRunnable(String name) { + this.names = new ArrayList(); + this.names.add(name); + } + + @Override + public void run() { + try { + Map returns = new UUIDFetcher(this.names).call(); + new CacheReturnedNames(returns).runTask(mcMMO.p); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private class CacheReturnedNames extends BukkitRunnable { + private Map returns; + + public CacheReturnedNames(Map returns) { + this.returns = returns; + } + + @Override + public void run() { + for (Entry entry : this.returns.entrySet()) { + mcMMO.getDatabaseManager().saveUserUUID(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java b/src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java new file mode 100644 index 000000000..e9cde41d4 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/runnables/database/UUIDUpdateAsyncTask.java @@ -0,0 +1,73 @@ +package com.gmail.nossr50.runnables.database; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.bukkit.scheduler.BukkitRunnable; + +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.config.HiddenConfig; +import com.gmail.nossr50.database.DatabaseManager; +import com.gmail.nossr50.datatypes.database.UpgradeType; +import com.gmail.nossr50.util.Misc; +import com.gmail.nossr50.util.uuid.UUIDFetcher; + +public class UUIDUpdateAsyncTask extends BukkitRunnable { + private mcMMO plugin; + private static final int MAX_LOOKUP = Math.max(HiddenConfig.getInstance().getUUIDConvertAmount(), 100); + + private List userNames; + private int size; + private int checkedUsers; + private long startMillis; + + public UUIDUpdateAsyncTask(mcMMO plugin, List userNames) { + this.plugin = plugin; + this.userNames = userNames; + + this.checkedUsers = 0; + this.startMillis = System.currentTimeMillis(); + } + + @Override + public void run() { + size = userNames.size(); + + plugin.getLogger().info("Starting to check and update UUIDs, total amount of users: " + size); + + List userNamesSection; + Map fetchedUUIDs = new HashMap(); + + while (size != 0) { + if (size > MAX_LOOKUP) { + userNamesSection = userNames.subList(size - MAX_LOOKUP, size); + size -= MAX_LOOKUP; + } + else { + userNamesSection = userNames.subList(0, size); + size = 0; + } + + try { + fetchedUUIDs.putAll(new UUIDFetcher(userNamesSection).call()); + } + catch (Exception e) { + plugin.getLogger().severe("Unable to fetch UUIDs!"); + return; + } + + checkedUsers += userNamesSection.size(); + userNamesSection.clear(); + size = userNames.size(); + + Misc.printProgress(checkedUsers, DatabaseManager.progressInterval, startMillis); + } + + if (mcMMO.getDatabaseManager().saveUserUUIDs(fetchedUUIDs)) { + mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UUIDS); + plugin.getLogger().info("UUID upgrade completed!"); + } + } +} diff --git a/src/main/java/com/gmail/nossr50/util/uuid/UUIDFetcher.java b/src/main/java/com/gmail/nossr50/util/uuid/UUIDFetcher.java new file mode 100644 index 000000000..7422bdac6 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/uuid/UUIDFetcher.java @@ -0,0 +1,100 @@ +package com.gmail.nossr50.util.uuid; + +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; + +import com.google.common.collect.ImmutableList; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +public class UUIDFetcher implements Callable> { + private static final double PROFILES_PER_REQUEST = 100; + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private final JSONParser jsonParser = new JSONParser(); + private final List names; + private final boolean rateLimiting; + + public UUIDFetcher(List names, boolean rateLimiting) { + this.names = ImmutableList.copyOf(names); + this.rateLimiting = rateLimiting; + } + + public UUIDFetcher(List names) { + this(names, true); + } + + public Map call() throws Exception { + Map uuidMap = new HashMap(); + int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST); + for (int i = 0; i < requests; i++) { + HttpURLConnection connection = createConnection(); + String body = JSONArray.toJSONString(names.subList(i * 100, Math.min((i + 1) * 100, names.size()))); + writeBody(connection, body); + JSONArray array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream())); + for (Object profile : array) { + JSONObject jsonProfile = (JSONObject) profile; + String id = (String) jsonProfile.get("id"); + String name = (String) jsonProfile.get("name"); + UUID uuid = UUIDFetcher.getUUID(id); + uuidMap.put(name, uuid); + } + if (rateLimiting && i != requests - 1) { + Thread.sleep(100L); + } + } + return uuidMap; + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + OutputStream stream = connection.getOutputStream(); + stream.write(body.getBytes()); + stream.flush(); + stream.close(); + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID getUUID(String id) { + return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); + } + + public static byte[] toBytes(UUID uuid) { + ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + return byteBuffer.array(); + } + + public static UUID fromBytes(byte[] array) { + if (array.length != 16) { + throw new IllegalArgumentException("Illegal byte array length: " + array.length); + } + ByteBuffer byteBuffer = ByteBuffer.wrap(array); + long mostSignificant = byteBuffer.getLong(); + long leastSignificant = byteBuffer.getLong(); + return new UUID(mostSignificant, leastSignificant); + } + + public static UUID getUUIDOf(String name) throws Exception { + return new UUIDFetcher(Arrays.asList(name)).call().get(name); + } +} diff --git a/src/main/resources/hidden.yml b/src/main/resources/hidden.yml index cf0894914..fcc0a6707 100644 --- a/src/main/resources/hidden.yml +++ b/src/main/resources/hidden.yml @@ -11,3 +11,6 @@ Options: EnchantmentBuffs: true # true to enable refreshing of chunks around a player at the end of Super Breaker, Giga Drill Breaker, and Berserk. This should fix blocks being broken client side, but not server-side RefreshChunks: false + + # Amount of users to convert every interval + UUIDConvertAmount: 100