diff --git a/.gitignore b/.gitignore index 36866d907..44ea23779 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ /Essentials/kits.yml /Essentials/userdata/testplayer1.yml /Essentials/usermap.csv +/Essentials/usermap.bin +/Essentials/uuids.bin # Build files .gradle/ diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 4b03e0aa6..a118891b7 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/Essentials/src/main/java/com/earth2me/essentials/BalanceTopImpl.java b/Essentials/src/main/java/com/earth2me/essentials/BalanceTopImpl.java index 07787715e..2c65ef7ed 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/BalanceTopImpl.java +++ b/Essentials/src/main/java/com/earth2me/essentials/BalanceTopImpl.java @@ -28,8 +28,8 @@ public class BalanceTopImpl implements BalanceTop { private void calculateBalanceTopMap() { final List entries = new LinkedList<>(); BigDecimal newTotal = BigDecimal.ZERO; - for (UUID u : ess.getUserMap().getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(u); + for (UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user != null) { if (!ess.getSettings().isNpcsInBalanceRanking() && user.isNPC()) { // Don't list NPCs in output diff --git a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java index 2b5584c69..c9369255d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Essentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Essentials.java @@ -39,6 +39,7 @@ import com.earth2me.essentials.textreader.IText; import com.earth2me.essentials.textreader.KeywordReplacer; import com.earth2me.essentials.textreader.SimpleTextInput; import com.earth2me.essentials.updatecheck.UpdateChecker; +import com.earth2me.essentials.userstorage.ModernUserMap; import com.earth2me.essentials.utils.FormatUtil; import com.earth2me.essentials.utils.VersionUtil; import io.papermc.lib.PaperLib; @@ -155,7 +156,9 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { private transient CustomItemResolver customItemResolver; private transient PermissionsHandler permissionsHandler; private transient AlternativeCommandsHandler alternativeCommandsHandler; - private transient UserMap userMap; + @Deprecated + private transient UserMap legacyUserMap; + private transient ModernUserMap userMap; private transient BalanceTopImpl balanceTop; private transient ExecuteTimer execTimer; private transient MailService mail; @@ -222,7 +225,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { LOGGER.log(Level.INFO, dataFolder.toString()); settings = new Settings(this); mail = new MailServiceImpl(this); - userMap = new UserMap(this); + userMap = new ModernUserMap(this); balanceTop = new BalanceTopImpl(this); permissionsHandler = new PermissionsHandler(this, false); Economy.setEss(this); @@ -301,11 +304,14 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { confList.add(settings); execTimer.mark("Settings"); + upgrade.preModules(); + execTimer.mark("Upgrade2"); + mail = new MailServiceImpl(this); execTimer.mark("Init(Mail)"); - userMap = new UserMap(this); - confList.add(userMap); + userMap = new ModernUserMap(this); + legacyUserMap = new UserMap(userMap); execTimer.mark("Init(Usermap)"); balanceTop = new BalanceTopImpl(this); @@ -317,7 +323,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { execTimer.mark("Kits"); upgrade.afterSettings(); - execTimer.mark("Upgrade2"); + execTimer.mark("Upgrade3"); warps = new Warps(this.getDataFolder()); confList.add(warps); @@ -588,7 +594,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { Economy.setEss(null); Trade.closeLog(); - getUserMap().getUUIDMap().shutdown(); + getUsers().shutdown(); HandlerList.unregisterAll(this); } @@ -1067,7 +1073,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { if (getSettings().isDebug()) { LOGGER.log(Level.INFO, "Constructing new userfile from base player " + base.getName()); } - user = new User(base, this); + user = userMap.loadUncachedUser(base); } else { user.update(base); } @@ -1203,7 +1209,13 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials { } @Override + @Deprecated public UserMap getUserMap() { + return legacyUserMap; + } + + @Override + public ModernUserMap getUsers() { return userMap; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index 53f661f80..8ec2521c6 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -308,6 +308,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { ess.getBackup().onPlayerJoin(); final User dUser = ess.getUser(player); + dUser.update(player); dUser.startTransaction(); if (dUser.isNPC()) { @@ -367,7 +368,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { } else if (ess.getSettings().isCustomJoinMessage()) { final String msg = (newUsername ? ess.getSettings().getCustomNewUsernameMessage() : ess.getSettings().getCustomJoinMessage()) .replace("{PLAYER}", player.getDisplayName()).replace("{USERNAME}", player.getName()) - .replace("{UNIQUE}", NumberFormat.getInstance().format(ess.getUserMap().getUniqueUsers())) + .replace("{UNIQUE}", NumberFormat.getInstance().format(ess.getUsers().getUserCount())) .replace("{ONLINE}", NumberFormat.getInstance().format(ess.getOnlinePlayers().size())) .replace("{UPTIME}", DateUtil.formatDateDiff(ManagementFactory.getRuntimeMXBean().getStartTime())) .replace("{PREFIX}", FormatUtil.replaceFormat(ess.getPermissionsHandler().getPrefix(player))) @@ -527,6 +528,7 @@ public class EssentialsPlayerListener implements Listener, FakeAccessor { public void onPlayerLogin(final PlayerLoginEvent event) { if (event.getResult() == Result.KICK_FULL) { final User kfuser = ess.getUser(event.getPlayer()); + kfuser.update(event.getPlayer()); if (kfuser.isAuthorized("essentials.joinfullserver")) { event.allow(); return; diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java index 3c4cce66b..ab3d46185 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsUpgrade.java @@ -4,9 +4,9 @@ import com.earth2me.essentials.config.ConfigurateUtil; import com.earth2me.essentials.config.EssentialsConfiguration; import com.earth2me.essentials.config.EssentialsUserConfiguration; import com.earth2me.essentials.craftbukkit.BanLookup; +import com.earth2me.essentials.userstorage.ModernUUIDCache; import com.earth2me.essentials.utils.StringUtil; import com.google.common.base.Charsets; -import com.google.common.collect.Maps; import com.google.common.io.Files; import com.google.gson.reflect.TypeToken; import net.ess3.api.IEssentials; @@ -31,10 +31,10 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.security.DigestInputStream; import java.security.MessageDigest; -import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -47,13 +47,12 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.earth2me.essentials.I18n.tl; public class EssentialsUpgrade { - private static final FileFilter YML_FILTER = pathname -> pathname.isFile() && pathname.getName().endsWith(".yml"); + public static final FileFilter YML_FILTER = pathname -> pathname.isFile() && pathname.getName().endsWith(".yml"); private static final String PATTERN_CONFIG_UUID_REGEX = "(?mi)^uuid:\\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\s*$"; private static final Pattern PATTERN_CONFIG_UUID = Pattern.compile(PATTERN_CONFIG_UUID_REGEX); private static final String PATTERN_CONFIG_NAME_REGEX = "(?mi)^lastAccountName:\\s*[\"\']?(\\w+)[\"\']?\\s*$"; @@ -93,7 +92,6 @@ public class EssentialsUpgrade { final int showProgress = countFiles % 250; if (showProgress == 0) { - ess.getUserMap().getUUIDMap().forceWriteUUIDMap(); ess.getLogger().info("Converted " + countFiles + "/" + userdir.list().length); } @@ -103,7 +101,8 @@ public class EssentialsUpgrade { final EssentialsUserConfiguration config; UUID uuid = null; try { - uuid = UUID.fromString(name); + //noinspection ResultOfMethodCallIgnored + UUID.fromString(name); } catch (final IllegalArgumentException ex) { final File file = new File(userdir, string); final EssentialsConfiguration conf = new EssentialsConfiguration(file); @@ -115,6 +114,7 @@ public class EssentialsUpgrade { final String uuidString = conf.getString(uuidConf, null); + //noinspection ConstantConditions for (int i = 0; i < 4; i++) { try { uuid = UUID.fromString(uuidString); @@ -122,7 +122,7 @@ public class EssentialsUpgrade { break; } catch (final Exception ex2) { if (conf.getBoolean("npc", false)) { - uuid = UUID.nameUUIDFromBytes(("NPC:" + name).getBytes(Charsets.UTF_8)); + uuid = UUID.nameUUIDFromBytes(("NPC:" + (ess.getSettings().isSafeUsermap() ? StringUtil.safeString(name) : name)).getBytes(Charsets.UTF_8)); break; } @@ -130,23 +130,24 @@ public class EssentialsUpgrade { uuid = player.getUniqueId(); } + //noinspection ConstantConditions if (uuid != null) { countBukkit++; break; } } + //noinspection ConstantConditions if (uuid != null) { conf.blockingSave(); config = new EssentialsUserConfiguration(name, uuid, new File(userdir, uuid + ".yml")); config.convertLegacyFile(); - ess.getUserMap().trackUUID(uuid, name, false); + ess.getUsers().loadUncachedUser(uuid); continue; } countFails++; } } - ess.getUserMap().getUUIDMap().forceWriteUUIDMap(); ess.getLogger().info("Converted " + countFiles + "/" + countFiles + ". Conversion complete."); ess.getLogger().info("Converted via cache: " + countEssCache + " :: Converted via lookup: " + countBukkit + " :: Failed to convert: " + countFails); @@ -913,74 +914,76 @@ public class EssentialsUpgrade { Bukkit.getBanList(BanList.Type.NAME).addBan(playerName, banReason, banTimeout == 0 ? null : new Date(banTimeout), Console.NAME); } - private void repairUserMap() { - if (doneFile.getBoolean("userMapRepaired", false)) { + public void generateUidCache() { + if (doneFile.getBoolean("newUidCacheBuilt", false)) { return; } - ess.getLogger().info("Starting usermap repair"); + final File usermapFile = new File(ess.getDataFolder(), "usermap.bin"); + final File uidsFile = new File(ess.getDataFolder(), "uuids.bin"); final File userdataFolder = new File(ess.getDataFolder(), "userdata"); - if (!userdataFolder.isDirectory()) { + + if (!userdataFolder.isDirectory() || usermapFile.exists() || uidsFile.exists()) { ess.getLogger().warning("Missing userdata folder, aborting"); + doneFile.setProperty("newUidCacheBuilt", true); + doneFile.save(); return; } - final File[] files = userdataFolder.listFiles(YML_FILTER); - final DecimalFormat format = new DecimalFormat("#0.00"); - final Map names = Maps.newHashMap(); - - for (int index = 0; index < files.length; index++) { - final File file = files[index]; - try { - UUID uuid = null; - final String filename = file.getName(); - final String configData = new String(java.nio.file.Files.readAllBytes(file.toPath()), Charsets.UTF_8); - - if (filename.length() > 36) { - try { - // ".yml" ending has 4 chars... - uuid = UUID.fromString(filename.substring(0, filename.length() - 4)); - } catch (final IllegalArgumentException ignored) { - } - } - - final Matcher uuidMatcher = PATTERN_CONFIG_UUID.matcher(configData); - if (uuidMatcher.find()) { - try { - uuid = UUID.fromString(uuidMatcher.group(1)); - } catch (final IllegalArgumentException ignored) { - } - } - - if (uuid == null) { - // Don't import - continue; - } - - final Matcher nameMatcher = PATTERN_CONFIG_NAME.matcher(configData); - if (nameMatcher.find()) { - final String username = nameMatcher.group(1); - if (username != null && username.length() > 0) { - names.put(StringUtil.safeString(username), uuid); - } - } - - if (index % 1000 == 0) { - ess.getLogger().info("Reading: " + format.format((100d * (double) index) / files.length) - + "%"); - } - } catch (final IOException e) { - ess.getLogger().log(Level.SEVERE, "Error while reading file: ", e); + try { + if (!usermapFile.createNewFile() || !uidsFile.createNewFile()) { + ess.getLogger().warning("Couldn't create usermap.bin or uuids.bin, aborting"); return; } + + final Map uuids = new HashMap<>(); + final Map nameToUuidMap = new HashMap<>(); + + final File[] files = userdataFolder.listFiles(YML_FILTER); + if (files != null) { + for (final File file : files) { + try { + final String fileName = file.getName(); + final UUID uuid = UUID.fromString(fileName.substring(0, fileName.length() - 4)); + final EssentialsConfiguration config = new EssentialsConfiguration(file); + config.load(); + String name = config.getString("last-account-name", null); + name = ess.getSettings().isSafeUsermap() ? StringUtil.safeString(name) : name; + final long time = config.getLong("timestamps.logout", 0L); + + if (name != null) { + if (nameToUuidMap.containsKey(name)) { + final UUID oldUuid = nameToUuidMap.get(name); + if (oldUuid.version() < uuid.version() || (oldUuid.version() == uuid.version() && uuids.get(oldUuid) < time)) { + ess.getLogger().warning("New UUID found for " + name + ": " + uuid + " (old: " + oldUuid + "). Replacing."); + uuids.remove(oldUuid); + } else { + ess.getLogger().warning("Found UUID for " + name + ": " + uuid + " (old: " + oldUuid + "). Skipping."); + continue; + } + } + + uuids.put(uuid, config.getLong("timestamps.logout", 0L)); + nameToUuidMap.put(name, uuid); + } + } catch (IllegalArgumentException | IndexOutOfBoundsException ignored) { + } + } + } + + if (!nameToUuidMap.isEmpty()) { + ModernUUIDCache.writeNameUuidMap(usermapFile, nameToUuidMap); + } + + if (!uuids.isEmpty()) { + ModernUUIDCache.writeUuidCache(uidsFile, uuids.keySet()); + } + + doneFile.setProperty("newUidCacheBuilt", true); + doneFile.save(); + } catch (final IOException e) { + ess.getLogger().log(Level.SEVERE, "Error while generating initial uuids/names cache", e); } - - ess.getUserMap().getNames().putAll(names); - ess.getUserMap().reloadConfig(); - - doneFile.setProperty("userMapRepaired", true); - doneFile.save(); - ess.getLogger().info("Completed usermap repair."); } public void beforeSettings() { @@ -991,6 +994,10 @@ public class EssentialsUpgrade { moveMotdRulesToFile("rules"); } + public void preModules() { + generateUidCache(); + } + public void afterSettings() { sanitizeAllUserFilenames(); updateUsersPowerToolsFormat(); @@ -1001,7 +1008,6 @@ public class EssentialsUpgrade { uuidFileChange(); banFormatChange(); warnMetrics(); - repairUserMap(); convertIgnoreList(); convertStupidCamelCaseUserdataKeys(); convertMailList(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java index 464fec918..92492d3e7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IEssentials.java @@ -7,6 +7,7 @@ import com.earth2me.essentials.commands.IEssentialsCommand; import com.earth2me.essentials.commands.PlayerNotFoundException; import com.earth2me.essentials.perm.PermissionsHandler; import com.earth2me.essentials.updatecheck.UpdateChecker; +import com.earth2me.essentials.userstorage.IUserMap; import net.ess3.nms.refl.providers.ReflOnlineModeProvider; import net.ess3.provider.ContainerProvider; import net.ess3.provider.FormattedCommandAliasProvider; @@ -119,6 +120,9 @@ public interface IEssentials extends Plugin { IItemDb getItemDb(); + IUserMap getUsers(); + + @Deprecated UserMap getUserMap(); BalanceTop getBalanceTop(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java index f74c7bd03..617b88565 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java @@ -330,4 +330,8 @@ public interface IUser { this.time = time; } } + + List getPastUsernames(); + + void addPastUsername(String username); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/UUIDMap.java b/Essentials/src/main/java/com/earth2me/essentials/UUIDMap.java index 54385a512..df284cbf4 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/UUIDMap.java +++ b/Essentials/src/main/java/com/earth2me/essentials/UUIDMap.java @@ -1,155 +1,19 @@ package com.earth2me.essentials; -import com.google.common.io.Files; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.regex.Pattern; - +@Deprecated public class UUIDMap { - private static final ScheduledExecutorService writeScheduler = Executors.newScheduledThreadPool(1); - private static boolean pendingWrite; - private static boolean loading = false; - private final transient net.ess3.api.IEssentials ess; - private final File userList; - private final transient Pattern splitPattern = Pattern.compile(","); - private final Runnable writeTaskRunnable; - - public UUIDMap(final net.ess3.api.IEssentials ess) { - this.ess = ess; - userList = new File(ess.getDataFolder(), "usermap.csv"); - pendingWrite = false; - writeTaskRunnable = () -> { - if (pendingWrite) { - try { - new WriteRunner(ess.getDataFolder(), userList, ess.getUserMap().getNames()).run(); - } catch (final Throwable t) { // bad code to prevent task from being suppressed - t.printStackTrace(); - } - } - }; - writeScheduler.scheduleWithFixedDelay(writeTaskRunnable, 5, 5, TimeUnit.SECONDS); - } - - public void loadAllUsers(final ConcurrentSkipListMap names, final ConcurrentSkipListMap> history) { - try { - if (!userList.exists()) { - userList.createNewFile(); - } - - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "Reading usermap from disk"); - } - - if (loading) { - return; - } - - names.clear(); - history.clear(); - loading = true; - - try (final BufferedReader reader = new BufferedReader(new FileReader(userList))) { - while (true) { - final String line = reader.readLine(); - if (line == null) { - break; - } else { - final String[] values = splitPattern.split(line); - if (values.length == 2) { - final String name = values[0]; - final UUID uuid = UUID.fromString(values[1]); - names.put(name, uuid); - if (!history.containsKey(uuid)) { - final ArrayList list = new ArrayList<>(); - list.add(name); - history.put(uuid, list); - } else { - final ArrayList list = history.get(uuid); - if (!list.contains(name)) { - list.add(name); - } - } - } - } - } - } - loading = false; - } catch (final IOException ex) { - Essentials.getWrappedLogger().log(Level.SEVERE, ex.getMessage(), ex); - } + public UUIDMap() { } public void writeUUIDMap() { - pendingWrite = true; + //no-op } public void forceWriteUUIDMap() { - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.INFO, "Forcing usermap write to disk"); - } - pendingWrite = true; - writeTaskRunnable.run(); + //no-op } public void shutdown() { - writeScheduler.submit(writeTaskRunnable); - writeScheduler.shutdown(); - } - - private static final class WriteRunner implements Runnable { - private final File location; - private final File endFile; - private final Map names; - - private WriteRunner(final File location, final File endFile, final Map names) { - this.location = location; - this.endFile = endFile; - this.names = new HashMap<>(names); - } - - @Override - public void run() { - pendingWrite = false; - if (loading || names.isEmpty()) { - return; - } - File configFile = null; - - try { - configFile = File.createTempFile("usermap", ".tmp.csv", location); - - final BufferedWriter bWriter = new BufferedWriter(new FileWriter(configFile)); - for (final Map.Entry entry : names.entrySet()) { - bWriter.write(entry.getKey() + "," + entry.getValue().toString()); - bWriter.newLine(); - } - - bWriter.close(); - Files.move(configFile, endFile); - } catch (final IOException ex) { - try { - if (configFile != null && configFile.exists()) { - Files.move(configFile, new File(endFile.getParentFile(), "usermap.bak.csv")); - } - } catch (final Exception ex2) { - Essentials.getWrappedLogger().log(Level.SEVERE, ex2.getMessage(), ex2); - } - Essentials.getWrappedLogger().log(Level.WARNING, ex.getMessage(), ex); - } - } + //no-op } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index 96ea5a0e6..d89ca38b1 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -106,7 +106,7 @@ public class User extends UserData implements Comparable, IMessageRecipien this.messageRecipient = new SimpleMessageRecipient(ess, this); } - void update(final Player base) { + public void update(final Player base) { setBase(base); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/UserData.java b/Essentials/src/main/java/com/earth2me/essentials/UserData.java index 2e3fb2a65..693efea4d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/UserData.java +++ b/Essentials/src/main/java/com/earth2me/essentials/UserData.java @@ -5,6 +5,7 @@ import com.earth2me.essentials.config.EssentialsUserConfiguration; import com.earth2me.essentials.config.entities.CommandCooldown; import com.earth2me.essentials.config.entities.LazyLocation; import com.earth2me.essentials.config.holders.UserConfigHolder; +import com.earth2me.essentials.userstorage.ModernUserMap; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.StringUtil; import com.google.common.base.Charsets; @@ -43,19 +44,14 @@ public abstract class UserData extends PlayerExtension implements IConf { super(base); this.ess = ess; final File folder = new File(ess.getDataFolder(), "userdata"); - if (!folder.exists()) { - folder.mkdirs(); + if (!folder.exists() && !folder.mkdirs()) { + throw new RuntimeException("Unable to create userdata folder!"); } - String filename; - try { - filename = base.getUniqueId().toString(); - } catch (final Throwable ex) { - ess.getLogger().warning("Falling back to old username system for " + base.getName()); - filename = base.getName(); - } - - config = new EssentialsUserConfiguration(base.getName(), base.getUniqueId(), new File(folder, filename + ".yml")); + config = new EssentialsUserConfiguration(base.getName(), base.getUniqueId(), new File(folder, base.getUniqueId() + ".yml")); + config.setSaveHook(() -> { + config.setRootHolder(UserConfigHolder.class, holder); + }); reloadConfig(); if (config.getUsername() == null) { @@ -65,12 +61,15 @@ public abstract class UserData extends PlayerExtension implements IConf { public final void reset() { config.blockingSave(); - config.getFile().delete(); + if (!config.getFile().delete()) { + ess.getLogger().warning("Unable to delete data file for " + config.getFile().getName()); + } if (config.getUsername() != null) { - ess.getUserMap().removeUser(config.getUsername()); + final ModernUserMap users = (ModernUserMap) ess.getUsers(); + users.invalidate(config.getUuid()); if (isNPC()) { - final String uuid = UUID.nameUUIDFromBytes(("NPC:" + StringUtil.safeString(config.getUsername())).getBytes(Charsets.UTF_8)).toString(); - ess.getUserMap().removeUserUUID(uuid); + final String name = ess.getSettings().isSafeUsermap() ? StringUtil.safeString(config.getUsername()) : config.getUsername(); + users.invalidate(UUID.nameUUIDFromBytes(("NPC:" + name).getBytes(Charsets.UTF_8))); } } } @@ -88,14 +87,6 @@ public abstract class UserData extends PlayerExtension implements IConf { ess.getLogger().log(Level.SEVERE, "Error while reading user config: " + config.getFile().getName(), e); throw new RuntimeException(e); } - config.setSaveHook(() -> { - try { - config.getRootNode().set(UserConfigHolder.class, holder); - } catch (SerializationException e) { - ess.getLogger().log(Level.SEVERE, "Error while saving user config: " + config.getFile().getName(), e); - throw new RuntimeException(e); - } - }); money = _getMoney(); } @@ -589,9 +580,13 @@ public abstract class UserData extends PlayerExtension implements IConf { } public void setLastAccountName(final String lastAccountName) { + if (getLastAccountName() != null && !getLastAccountName().equals(lastAccountName)) { + final List usernames = holder.pastUsernames(); + usernames.add(0, getLastAccountName()); + holder.pastUsernames(usernames); + } holder.lastAccountName(lastAccountName); config.save(); - ess.getUserMap().trackUUID(getConfigUUID(), lastAccountName, true); } public boolean arePowerToolsEnabled() { @@ -722,6 +717,17 @@ public abstract class UserData extends PlayerExtension implements IConf { config.save(); } + public List getPastUsernames() { + return holder.pastUsernames(); + } + + public void addPastUsername(String username) { + final List usernames = holder.pastUsernames(); + usernames.add(0, username); + holder.pastUsernames(usernames); + config.save(); + } + public boolean isShouting() { if (holder.shouting() == null) { holder.shouting(ess.getSettings().isShoutDefault()); diff --git a/Essentials/src/main/java/com/earth2me/essentials/UserMap.java b/Essentials/src/main/java/com/earth2me/essentials/UserMap.java index 35f5eefe6..c65d91a39 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/UserMap.java +++ b/Essentials/src/main/java/com/earth2me/essentials/UserMap.java @@ -1,383 +1,70 @@ package com.earth2me.essentials; import com.earth2me.essentials.api.UserDoesNotExistException; -import com.earth2me.essentials.utils.StringUtil; -import com.google.common.base.Charsets; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.util.concurrent.UncheckedExecutionException; -import net.ess3.api.IEssentials; -import net.ess3.api.MaxMoneyException; +import com.earth2me.essentials.userstorage.ModernUserMap; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import java.io.File; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.regex.Pattern; -public class UserMap extends CacheLoader implements IConf { - private static boolean legacy = false; - private static Method getLegacy; - private final transient IEssentials ess; - private final transient ConcurrentSkipListSet keys = new ConcurrentSkipListSet<>(); - private final transient ConcurrentSkipListMap names = new ConcurrentSkipListMap<>(); - private final transient ConcurrentSkipListMap> history = new ConcurrentSkipListMap<>(); - private final UUIDMap uuidMap; - private final transient Cache users; - private final Pattern validUserPattern = Pattern.compile("^[a-zA-Z0-9_]{2,16}$"); +@Deprecated +public class UserMap { + private final transient ModernUserMap userMap; + private final transient UUIDMap uuidMap; - private static final String WARN_UUID_NOT_REPLACE = "Found UUID {0} for player {1}, but player already has a UUID ({2}). Not replacing UUID in usermap."; - - public UserMap(final IEssentials ess) { - super(); - this.ess = ess; - uuidMap = new UUIDMap(ess); - //RemovalListener remListener = new UserMapRemovalListener(); - //users = CacheBuilder.newBuilder().maximumSize(ess.getSettings().getMaxUserCacheCount()).softValues().removalListener(remListener).build(this); - final CacheBuilder cacheBuilder = CacheBuilder.newBuilder(); - final int maxCount = ess.getSettings().getMaxUserCacheCount(); - try { - cacheBuilder.maximumSize(maxCount); - } catch (final NoSuchMethodError nsme) { - legacy = true; - legacyMaximumSize(cacheBuilder, maxCount); - } - cacheBuilder.softValues(); - if (!legacy) { - users = cacheBuilder.build(this); - } else { - users = legacyBuild(cacheBuilder); - } - } - - private void loadAllUsersAsync(final IEssentials ess) { - ess.runTaskAsynchronously(() -> { - synchronized (users) { - final File userdir = new File(ess.getDataFolder(), "userdata"); - if (!userdir.exists()) { - return; - } - keys.clear(); - users.invalidateAll(); - for (final String string : userdir.list()) { - if (!string.endsWith(".yml")) { - continue; - } - final String name = string.substring(0, string.length() - 4); - try { - keys.add(UUID.fromString(name)); - } catch (final IllegalArgumentException ex) { - //Ignore these users till they rejoin. - } - } - uuidMap.loadAllUsers(names, history); - } - }); - } - - public boolean userExists(final UUID uuid) { - return keys.contains(uuid); + public UserMap(final ModernUserMap userMap) { + this.userMap = userMap; + this.uuidMap = new UUIDMap(); } public User getUser(final String name) { - final String sanitizedName = StringUtil.safeString(name); - try { - if (ess.getSettings().isDebug()) { - ess.getLogger().warning("Looking up username " + name + " (" + sanitizedName + ") ..."); - } - - if (names.containsKey(sanitizedName)) { - final UUID uuid = names.get(sanitizedName); - return getUser(uuid); - } - - if (ess.getSettings().isDebug()) { - ess.getLogger().warning(name + "(" + sanitizedName + ") has no known usermap entry"); - } - - final File userFile = getUserFileFromString(sanitizedName); - if (userFile.exists()) { - ess.getLogger().info("Importing user " + name + " to usermap."); - final User user = new User(new OfflinePlayer(sanitizedName, ess.getServer()), ess); - trackUUID(user.getBase().getUniqueId(), user.getName(), true); - return user; - } - return null; - } catch (final UncheckedExecutionException ex) { - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.WARNING, ex, () -> String.format("Exception while getting user for %s (%s)", name, sanitizedName)); - } - return null; - } + return userMap.getUser(name); } public User getUser(final UUID uuid) { - try { - if (!legacy) { - return ((LoadingCache) users).get(uuid.toString()); - } else { - return legacyCacheGet(uuid); - } - } catch (final ExecutionException | UncheckedExecutionException ex) { - if (ess.getSettings().isDebug()) { - ess.getLogger().log(Level.WARNING, ex, () -> "Exception while getting user for " + uuid); - } - return null; - } + return userMap.getUser(uuid); } public void trackUUID(final UUID uuid, final String name, final boolean replace) { - if (uuid != null) { - keys.add(uuid); - if (name != null && name.length() > 0) { - final String keyName = ess.getSettings().isSafeUsermap() ? StringUtil.safeString(name) : name; - if (!names.containsKey(keyName)) { - names.put(keyName, uuid); - uuidMap.writeUUIDMap(); - } else if (!isUUIDMatch(uuid, keyName)) { - if (replace) { - ess.getLogger().info("Found new UUID for " + name + ". Replacing " + names.get(keyName).toString() + " with " + uuid.toString()); - names.put(keyName, uuid); - uuidMap.writeUUIDMap(); - } else { - ess.getLogger().log(Level.INFO, MessageFormat.format(WARN_UUID_NOT_REPLACE, uuid.toString(), name, names.get(keyName).toString()), new RuntimeException()); - } - } - } - } + // no-op } - public boolean isUUIDMatch(final UUID uuid, final String name) { - return names.containsKey(name) && names.get(name).equals(uuid); - } - - @Override public User load(final String stringUUID) throws Exception { - final UUID uuid = UUID.fromString(stringUUID); - Player player = ess.getServer().getPlayer(uuid); - if (player != null) { - final User user = new User(player, ess); - trackUUID(uuid, user.getName(), true); - return user; - } - - final File userFile = getUserFileFromID(uuid); - - if (userFile.exists()) { - player = new OfflinePlayer(uuid, ess.getServer()); - final User user = new User(player, ess); - ((OfflinePlayer) player).setName(user.getLastAccountName()); - trackUUID(uuid, user.getName(), false); - return user; - } - - throw new Exception("User not found!"); + return userMap.load(UUID.fromString(stringUUID)); } public User load(final org.bukkit.OfflinePlayer player) throws UserDoesNotExistException { - if (player == null) { - throw new IllegalArgumentException("Player cannot be null!"); - } - + final Player userPlayer; if (player instanceof Player) { - if (ess.getSettings().isDebug()) { - ess.getLogger().info("Loading online OfflinePlayer into user map..."); - } - final User user = new User((Player) player, ess); - trackUUID(player.getUniqueId(), player.getName(), true); - return user; - } - - final File userFile = getUserFileFromID(player.getUniqueId()); - if (ess.getSettings().isDebug()) { - ess.getLogger().info("Loading OfflinePlayer into user map. Has data: " + userFile.exists() + " for " + player); - } - - final OfflinePlayer essPlayer = new OfflinePlayer(player.getUniqueId(), ess.getServer()); - final User user = new User(essPlayer, ess); - if (userFile.exists()) { - essPlayer.setName(user.getLastAccountName()); + userPlayer = (Player) player; } else { - if (ess.getSettings().isDebug()) { - ess.getLogger().info("OfflinePlayer usermap load saving user data for " + player); - } - - // this code makes me sad - user.startTransaction(); - try { - user.setMoney(ess.getSettings().getStartingBalance()); - } catch (MaxMoneyException e) { - // Shouldn't happen as it would be an illegal configuration state - throw new RuntimeException(e); - } - user.setLastAccountName(user.getName()); - user.stopTransaction(); + final com.earth2me.essentials.OfflinePlayer essPlayer = new com.earth2me.essentials.OfflinePlayer(player.getUniqueId(), Bukkit.getServer()); + essPlayer.setName(player.getName()); + userPlayer = essPlayer; } - trackUUID(player.getUniqueId(), user.getName(), false); + final User user = userMap.getUser(userPlayer); + if (user == null) { + throw new UserDoesNotExistException("User not found"); + } return user; } - @Override - public void reloadConfig() { - getUUIDMap().forceWriteUUIDMap(); - loadAllUsersAsync(ess); - } - - public void invalidateAll() { - users.invalidateAll(); - } - - public void removeUser(final String name) { - if (names == null) { - ess.getLogger().warning("Name collection is null, cannot remove user."); - return; - } - final UUID uuid = names.get(name); - if (uuid != null) { - keys.remove(uuid); - users.invalidate(uuid.toString()); - } - names.remove(name); - names.remove(StringUtil.safeString(name)); - } - - public void removeUserUUID(final String uuid) { - users.invalidate(uuid); - } - public Set getAllUniqueUsers() { - return Collections.unmodifiableSet(keys); + return userMap.getAllUserUUIDs(); } public int getUniqueUsers() { - return keys.size(); + return userMap.getUserCount(); } protected ConcurrentSkipListMap getNames() { - return names; - } - - protected ConcurrentSkipListMap> getHistory() { - return history; - } - - public List getUserHistory(final UUID uuid) { - return history.get(uuid); + return new ConcurrentSkipListMap<>(userMap.getNameCache()); } public UUIDMap getUUIDMap() { return uuidMap; } - // class UserMapRemovalListener implements RemovalListener - // { - // @Override - // public void onRemoval(final RemovalNotification notification) - // { - // Object value = notification.getValue(); - // if (value != null) - // { - // ((User)value).cleanup(); - // } - // } - // } - - private File getUserFileFromID(final UUID uuid) { - final File userFolder = new File(ess.getDataFolder(), "userdata"); - return new File(userFolder, uuid.toString() + ".yml"); - } - - public File getUserFileFromString(final String name) { - final File userFolder = new File(ess.getDataFolder(), "userdata"); - return new File(userFolder, StringUtil.sanitizeFileName(name) + ".yml"); - } - - @SuppressWarnings("deprecation") - public User getUserFromBukkit(String name) { - name = StringUtil.safeString(name); - if (ess.getSettings().isDebug()) { - ess.getLogger().warning("Using potentially blocking Bukkit UUID lookup for: " + name); - } - // Don't attempt to look up entirely invalid usernames - if (name == null || !validUserPattern.matcher(name).matches()) { - return null; - } - final org.bukkit.OfflinePlayer offlinePlayer = ess.getServer().getOfflinePlayer(name); - if (offlinePlayer == null) { - return null; - } - final UUID uuid; - try { - uuid = offlinePlayer.getUniqueId(); - } catch (final UnsupportedOperationException | NullPointerException e) { - return null; - } - // This is how Bukkit generates fake UUIDs - if (UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)).equals(uuid)) { - return null; - } else { - names.put(name, uuid); - return getUser(uuid); - } - } - - private User legacyCacheGet(final UUID uuid) { - if (getLegacy == null) { - final Class usersClass = users.getClass(); - for (final Method m : usersClass.getDeclaredMethods()) { - if (m.getName().equals("get")) { - getLegacy = m; - getLegacy.setAccessible(true); - break; - } - } - } - try { - return (User) getLegacy.invoke(users, uuid.toString()); - } catch (final IllegalAccessException | InvocationTargetException e) { - return null; - } - } - - private void legacyMaximumSize(final CacheBuilder builder, final int maxCount) { - try { - final Method maxSizeLegacy = builder.getClass().getDeclaredMethod("maximumSize", Integer.TYPE); - maxSizeLegacy.setAccessible(true); - maxSizeLegacy.invoke(builder, maxCount); - } catch (final NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - e.printStackTrace(); - } - } - - @SuppressWarnings("unchecked") - private Cache legacyBuild(final CacheBuilder builder) { - Method build = null; - for (final Method method : builder.getClass().getDeclaredMethods()) { - if (method.getName().equals("build")) { - build = method; - break; - } - } - Cache legacyUsers; - try { - assert build != null; - build.setAccessible(true); - legacyUsers = (Cache) build.invoke(builder, this); - } catch (final IllegalAccessException | InvocationTargetException e) { - legacyUsers = null; - } - return legacyUsers; - } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/api/Economy.java b/Essentials/src/main/java/com/earth2me/essentials/api/Economy.java index a848d7007..8fdcfa5a1 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/api/Economy.java +++ b/Essentials/src/main/java/com/earth2me/essentials/api/Economy.java @@ -3,6 +3,7 @@ package com.earth2me.essentials.api; import com.earth2me.essentials.Trade; import com.earth2me.essentials.User; import com.earth2me.essentials.config.EssentialsUserConfiguration; +import com.earth2me.essentials.userstorage.ModernUserMap; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.StringUtil; import com.google.common.base.Charsets; @@ -41,9 +42,9 @@ public class Economy { ess = aEss; } - private static void createNPCFile(String name) { + private static void createNPCFile(final String unsanitizedName) { final File folder = new File(ess.getDataFolder(), "userdata"); - name = StringUtil.safeString(name); + final String name = ess.getSettings().isSafeUsermap() ? StringUtil.safeString(unsanitizedName) : unsanitizedName; if (!folder.exists()) { if (!folder.mkdirs()) { throw new RuntimeException("Error while creating userdata directory!"); @@ -59,9 +60,12 @@ public class Economy { npcConfig.load(); npcConfig.setProperty("npc", true); npcConfig.setProperty("last-account-name", name); + npcConfig.setProperty("npc-name", unsanitizedName); npcConfig.setProperty("money", ess.getSettings().getStartingBalance()); npcConfig.blockingSave(); - ess.getUserMap().trackUUID(npcUUID, name, false); + // This will load the NPC into the UserMap + UUID cache + ((ModernUserMap) ess.getUsers()).addCachedNpcName(npcUUID, name); + ess.getUsers().getUser(npcUUID); } private static void deleteNPC(final String name) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbalancetop.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbalancetop.java index 767b2b79a..a0846e43d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbalancetop.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandbalancetop.java @@ -64,8 +64,8 @@ public class Commandbalancetop extends EssentialsCommand { } // If there are less than 50 users in our usermap, there is no need to display a warning as these calculations should be done quickly - if (ess.getUserMap().getUniqueUsers() > MINUSERS) { - sender.sendMessage(tl("orderBalances", ess.getUserMap().getUniqueUsers())); + if (ess.getUsers().getUserCount() > MINUSERS) { + sender.sendMessage(tl("orderBalances", ess.getUsers().getUserCount())); } ess.runTaskAsynchronously(new Viewer(sender, page, force)); diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java index ffa29c026..9da15711f 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandessentials.java @@ -3,9 +3,9 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.EssentialsUpgrade; import com.earth2me.essentials.User; -import com.earth2me.essentials.UserMap; import com.earth2me.essentials.economy.EconomyLayer; import com.earth2me.essentials.economy.EconomyLayers; +import com.earth2me.essentials.userstorage.ModernUserMap; import com.earth2me.essentials.utils.CommandMapUtil; import com.earth2me.essentials.utils.DateUtil; import com.earth2me.essentials.utils.EnumUtil; @@ -13,7 +13,6 @@ import com.earth2me.essentials.utils.FloatUtil; import com.earth2me.essentials.utils.NumberUtil; import com.earth2me.essentials.utils.PasteUtil; import com.earth2me.essentials.utils.VersionUtil; -import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.gson.JsonArray; @@ -33,6 +32,7 @@ import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitRunnable; +import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.nio.charset.StandardCharsets; @@ -45,12 +45,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import java.util.logging.Level; import java.util.stream.Collectors; import static com.earth2me.essentials.I18n.tl; @@ -147,11 +150,8 @@ public class Commandessentials extends EssentialsCommand { case "homes": runHomes(server, sender, commandLabel, args); break; - case "uuidconvert": - runUUIDConvert(server, sender, commandLabel, args); - break; - case "uuidtest": - runUUIDTest(server, sender, commandLabel, args); + case "usermap": + runUserMap(sender, args); break; // "#EasterEgg" @@ -473,12 +473,11 @@ public class Commandessentials extends EssentialsCommand { final long daysArg = Long.parseLong(args[1]); final double moneyArg = args.length >= 3 ? FloatUtil.parseDouble(args[2].replaceAll("[^0-9\\.]", "")) : 0; final int homesArg = args.length >= 4 && NumberUtil.isInt(args[3]) ? Integer.parseInt(args[3]) : 0; - final UserMap userMap = ess.getUserMap(); ess.runTaskAsynchronously(() -> { final long currTime = System.currentTimeMillis(); - for (final UUID u : userMap.getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(u); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user == null) { continue; } @@ -523,13 +522,12 @@ public class Commandessentials extends EssentialsCommand { throw new Exception(HOMES_USAGE); } - final UserMap userMap = ess.getUserMap(); switch (args[1]) { case "fix": sender.sendMessage(tl("fixingHomes")); ess.runTaskAsynchronously(() -> { - for (final UUID u : userMap.getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(u); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user == null) { continue; } @@ -553,8 +551,8 @@ public class Commandessentials extends EssentialsCommand { } sender.sendMessage(filterByWorld ? tl("deletingHomesWorld", args[2]) : tl("deletingHomes")); ess.runTaskAsynchronously(() -> { - for (final UUID u : userMap.getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(u); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user == null) { continue; } @@ -577,52 +575,77 @@ public class Commandessentials extends EssentialsCommand { } } - // Forces a rerun of userdata UUID conversion. - private void runUUIDConvert(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception { - sender.sendMessage("Starting Essentials UUID userdata conversion; this may lag the server."); - - final Boolean ignoreUFCache = args.length > 2 && args[1].toLowerCase(Locale.ENGLISH).contains("ignore"); - EssentialsUpgrade.uuidFileConvert(ess, ignoreUFCache); - - sender.sendMessage("UUID conversion complete. Check your server log for more information."); - } - - // Looks up various UUIDs for a user. - private void runUUIDTest(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception { - if (args.length < 2) { - throw new Exception("/ uuidtest "); + // Gets information about cached users + private void runUserMap(final CommandSource sender, final String[] args) { + if (!sender.isAuthorized("essentials.usermap", ess)) { + return; } - final String name = args[1]; - sender.sendMessage("Looking up UUID for " + name); - UUID onlineUUID = null; + final ModernUserMap userMap = (ModernUserMap) ess.getUsers(); + sender.sendMessage(tl("usermapSize", userMap.getCachedCount(), userMap.getUserCount(), ess.getSettings().getMaxUserCacheCount())); + if (args.length > 1) { + if (args[1].equals("full")) { + for (final Map.Entry entry : userMap.getNameCache().entrySet()) { + sender.sendMessage(tl("usermapEntry", entry.getKey(), entry.getValue().toString())); + } + } else if (args[1].equals("purge")) { + final boolean seppuku = args.length > 2 && args[2].equals("iknowwhatimdoing"); - for (final Player player : ess.getOnlinePlayers()) { - if (player.getName().equalsIgnoreCase(name)) { - onlineUUID = player.getUniqueId(); - break; + sender.sendMessage(tl("usermapPurge", String.valueOf(seppuku))); + + final Set uuids = new HashSet<>(ess.getUsers().getAllUserUUIDs()); + ess.runTaskAsynchronously(() -> { + final File userdataFolder = new File(ess.getDataFolder(), "userdata"); + final File backupFolder = new File(ess.getDataFolder(), "userdata-npc-backup-boogaloo-" + System.currentTimeMillis()); + + if (!userdataFolder.isDirectory()) { + ess.getLogger().warning("Missing userdata folder, aborting usermap purge."); + return; + } + + if (seppuku && !backupFolder.mkdir()) { + ess.getLogger().warning("Unable to create backup folder, aborting usermap purge."); + return; + } + + int total = 0; + final File[] files = userdataFolder.listFiles(EssentialsUpgrade.YML_FILTER); + if (files != null) { + for (final File file : files) { + try { + final String fileName = file.getName(); + final UUID uuid = UUID.fromString(fileName.substring(0, fileName.length() - 4)); + if (!uuids.contains(uuid)) { + total++; + ess.getLogger().warning("Found orphaned userdata file: " + file.getName()); + if (seppuku) { + try { + com.google.common.io.Files.move(file, new File(backupFolder, file.getName())); + } catch (IOException e) { + ess.getLogger().log(Level.WARNING, "Unable to move orphaned userdata file: " + file.getName(), e); + } + } + } + } catch (IllegalArgumentException ignored) { + } + } + } + ess.getLogger().info("Found " + total + " orphaned userdata files."); + }); + } else { + try { + final UUID uuid = UUID.fromString(args[1]); + for (final Map.Entry entry : userMap.getNameCache().entrySet()) { + if (entry.getValue().equals(uuid)) { + sender.sendMessage(tl("usermapEntry", entry.getKey(), args[1])); + } + } + } catch (IllegalArgumentException ignored) { + final String sanitizedName = userMap.getSanitizedName(args[1]); + sender.sendMessage(tl("usermapEntry", sanitizedName, userMap.getNameCache().get(sanitizedName).toString())); + } } } - - final UUID essUUID = ess.getUserMap().getUser(name).getConfigUUID(); - - final org.bukkit.OfflinePlayer player = ess.getServer().getOfflinePlayer(name); - final UUID bukkituuid = player.getUniqueId(); - sender.sendMessage("Bukkit Lookup: " + bukkituuid.toString()); - - if (onlineUUID != null && onlineUUID != bukkituuid) { - sender.sendMessage("Online player: " + onlineUUID.toString()); - } - - if (essUUID != null && essUUID != bukkituuid) { - sender.sendMessage("Essentials config: " + essUUID.toString()); - } - - final UUID npcuuid = UUID.nameUUIDFromBytes(("NPC:" + name).getBytes(Charsets.UTF_8)); - sender.sendMessage("NPC UUID: " + npcuuid.toString()); - - final UUID offlineuuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)); - sender.sendMessage("Offline Mode UUID: " + offlineuuid.toString()); } // Displays versions of EssentialsX and related plugins. @@ -750,7 +773,6 @@ public class Commandessentials extends EssentialsCommand { options.add("cleanup"); options.add("homes"); //options.add("uuidconvert"); - //options.add("uuidtest"); //options.add("nya"); //options.add("moo"); return options; @@ -763,7 +785,6 @@ public class Commandessentials extends EssentialsCommand { } break; case "reset": - case "uuidtest": if (args.length == 2) { return getPlayers(server, sender); } @@ -782,11 +803,6 @@ public class Commandessentials extends EssentialsCommand { return server.getWorlds().stream().map(World::getName).collect(Collectors.toList()); } break; - case "uuidconvert": - if (args.length == 2) { - return Lists.newArrayList("ignoreUFCache"); - } - break; case "dump": final List list = Lists.newArrayList("config", "kits", "log", "discord", "worth", "tpr", "spawns", "commands", "all"); for (String arg : args) { diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java index a40f4ade9..747f03629 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandmail.java @@ -261,8 +261,8 @@ public class Commandmail extends EssentialsCommand { @Override public void run() { - for (UUID userid : ess.getUserMap().getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(userid); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user != null) { user.sendMail(messageRecipient, message, dateDiff); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandseen.java b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandseen.java index 378d69333..f6c3e2d00 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/Commandseen.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/Commandseen.java @@ -2,7 +2,6 @@ package com.earth2me.essentials.commands; import com.earth2me.essentials.CommandSource; import com.earth2me.essentials.User; -import com.earth2me.essentials.UserMap; import com.earth2me.essentials.craftbukkit.BanLookup; import com.earth2me.essentials.utils.DateUtil; import com.earth2me.essentials.utils.FormatUtil; @@ -63,7 +62,7 @@ public class Commandseen extends EssentialsCommand { ess.getScheduler().runTaskAsynchronously(ess, new Runnable() { @Override public void run() { - final User userFromBukkit = ess.getUserMap().getUserFromBukkit(args[0]); + final User userFromBukkit = ess.getUsers().getUser(args[0]); try { if (userFromBukkit != null) { showUserSeen(userFromBukkit); @@ -103,8 +102,8 @@ public class Commandseen extends EssentialsCommand { user.setDisplayNick(); sender.sendMessage(tl("seenOnline", user.getDisplayName(), DateUtil.formatDateDiff(user.getLastLogin()))); - final List history = ess.getUserMap().getUserHistory(user.getBase().getUniqueId()); - if (history != null && history.size() > 1) { + final List history = user.getPastUsernames(); + if (history != null && !history.isEmpty()) { sender.sendMessage(tl("seenAccounts", StringUtil.joinListSkip(", ", user.getName(), history))); } @@ -139,7 +138,7 @@ public class Commandseen extends EssentialsCommand { user.setDisplayNick(); if (user.getLastLogout() > 0) { sender.sendMessage(tl("seenOffline", user.getName(), DateUtil.formatDateDiff(user.getLastLogout()))); - final List history = ess.getUserMap().getUserHistory(user.getBase().getUniqueId()); + final List history = user.getPastUsernames(); if (history != null && history.size() > 1) { sender.sendMessage(tl("seenAccounts", StringUtil.joinListSkip(", ", user.getName(), history))); } @@ -192,14 +191,12 @@ public class Commandseen extends EssentialsCommand { } private void seenIP(final CommandSource sender, final String ipAddress, final String display) { - final UserMap userMap = ess.getUserMap(); - sender.sendMessage(tl("runningPlayerMatch", display)); ess.runTaskAsynchronously(() -> { final List matches = new ArrayList<>(); - for (final UUID u : userMap.getAllUniqueUsers()) { - final User user = ess.getUserMap().getUser(u); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); if (user == null) { continue; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java index 29934d7e8..3ac7f51a3 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java +++ b/Essentials/src/main/java/com/earth2me/essentials/commands/EssentialsLoopCommand.java @@ -32,9 +32,9 @@ public abstract class EssentialsLoopCommand extends EssentialsCommand { final User matchedUser = ess.getUser(uuid); userConsumer.accept(matchedUser); } else if (matchWildcards && searchTerm.contentEquals("**")) { - for (final UUID sUser : ess.getUserMap().getAllUniqueUsers()) { - final User matchedUser = ess.getUser(sUser); - userConsumer.accept(matchedUser); + for (final UUID u : ess.getUsers().getAllUserUUIDs()) { + final User user = ess.getUsers().loadUncachedUser(u); + userConsumer.accept(user); } } else if (matchWildcards && searchTerm.contentEquals("*")) { final boolean skipHidden = sender.isPlayer() && !ess.getUser(sender.getPlayer()).canInteractVanished(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java index 9e2eb89ed..e53fa07a8 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java @@ -103,6 +103,15 @@ public class EssentialsConfiguration { return configurationNode; } + public void setRootHolder(final Class holderClass, final Object holder) { + try { + getRootNode().set(holderClass, holder); + } catch (SerializationException e) { + Essentials.getWrappedLogger().log(Level.SEVERE, "Error while saving user config: " + configFile.getName(), e); + throw new RuntimeException(e); + } + } + public File getFile() { return configFile; } @@ -366,10 +375,10 @@ public class EssentialsConfiguration { } catch (final ParsingException e) { final File broken = new File(configFile.getAbsolutePath() + ".broken." + System.currentTimeMillis()); if (configFile.renameTo(broken)) { - Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile.toString() + " is broken, it has been renamed to " + broken.toString(), e.getCause()); + Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile + " is broken, it has been renamed to " + broken, e.getCause()); return; } - Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile.toString() + " is broken. A backup file has failed to be created", e.getCause()); + Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile + " is broken. A backup file has failed to be created", e.getCause()); } catch (final ConfigurateException e) { Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e); } finally { @@ -420,6 +429,10 @@ public class EssentialsConfiguration { } } + public boolean isTransaction() { + return transaction.get(); + } + public void setSaveHook(Runnable saveHook) { this.saveHook = saveHook; } diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsUserConfiguration.java b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsUserConfiguration.java index 35d7c1e90..eac64cc19 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsUserConfiguration.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsUserConfiguration.java @@ -52,7 +52,7 @@ public class EssentialsUserConfiguration extends EssentialsConfiguration { private File getAltFile() { final UUID fn = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username.toLowerCase(Locale.ENGLISH)).getBytes(Charsets.UTF_8)); - return new File(configFile.getParentFile(), fn.toString() + ".yml"); + return new File(configFile.getParentFile(), fn + ".yml"); } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java b/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java index 6a468d7a4..545537667 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java @@ -262,6 +262,16 @@ public class UserConfigHolder { this.lastAccountName = value; } + private @MonotonicNonNull String npcName; + + public String npcName() { + return this.npcName; + } + + public void npcName(final String value) { + this.npcName = value; + } + private boolean powertoolsenabled = true; public boolean powerToolsEnabled() { @@ -332,6 +342,20 @@ public class UserConfigHolder { this.shouting = value; } + @DeleteOnEmpty + private @MonotonicNonNull List pastUsernames; + + public List pastUsernames() { + if (this.pastUsernames == null) { + this.pastUsernames = new ArrayList<>(); + } + return this.pastUsernames; + } + + public void pastUsernames(List value) { + this.pastUsernames = value; + } + private @NonNull Timestamps timestamps = new Timestamps(); public Timestamps timestamps() { diff --git a/Essentials/src/main/java/com/earth2me/essentials/economy/vault/VaultEconomyProvider.java b/Essentials/src/main/java/com/earth2me/essentials/economy/vault/VaultEconomyProvider.java index 672256d69..d231c56ab 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/economy/vault/VaultEconomyProvider.java +++ b/Essentials/src/main/java/com/earth2me/essentials/economy/vault/VaultEconomyProvider.java @@ -11,6 +11,7 @@ import net.ess3.api.MaxMoneyException; import net.milkbowl.vault.economy.Economy; import net.milkbowl.vault.economy.EconomyResponse; import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; import java.io.File; import java.math.BigDecimal; @@ -308,21 +309,27 @@ public class VaultEconomyProvider implements Economy { npcConfig.setProperty("last-account-name", player.getName()); npcConfig.setProperty("money", ess.getSettings().getStartingBalance()); npcConfig.blockingSave(); - ess.getUserMap().trackUUID(player.getUniqueId(), player.getName(), false); + // This will load the NPC into the UserMap + UUID cache + ess.getUsers().addCachedNpcName(player.getUniqueId(), player.getName()); + ess.getUsers().getUser(player.getUniqueId()); return true; } // Loading a v4 UUID that we somehow didn't track, mark it as a normal player and hope for the best, vault sucks :/ - try { - if (ess.getSettings().isDebug()) { - ess.getLogger().info("Vault requested a player account creation for a v4 UUID: " + player); - } - ess.getUserMap().load(player); - return true; - } catch (UserDoesNotExistException e) { - e.printStackTrace(); - return false; + if (ess.getSettings().isDebug()) { + ess.getLogger().info("Vault requested a player account creation for a v4 UUID: " + player); } + + final Player userPlayer; + if (player instanceof Player) { + userPlayer = (Player) player; + } else { + final com.earth2me.essentials.OfflinePlayer essPlayer = new com.earth2me.essentials.OfflinePlayer(player.getUniqueId(), ess.getServer()); + essPlayer.setName(player.getName()); + userPlayer = essPlayer; + } + ess.getUsers().getUser(userPlayer); + return true; } @Override diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java index 77fff460f..e4db80360 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java @@ -70,11 +70,11 @@ public class LuckPermsHandler extends ModernVaultHandler { // If the player doesn't exist in the UserMap, just skip // Ess will cause performance problems for permissions checks if it attempts to // perform i/o to load the user data otherwise. - if (!ess.getUserMap().userExists(target.getUniqueId())) { + if (!ess.getUsers().getAllUserUUIDs().contains(target.getUniqueId())) { return; } - final User user = ess.getUser(target); + final User user = ess.getUsers().loadUncachedUser(target.getUniqueId()); for (Calculator calculator : this.calculators) { calculator.function.apply(user).forEach(value -> consumer.accept(calculator.id, value)); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/textreader/KeywordReplacer.java b/Essentials/src/main/java/com/earth2me/essentials/textreader/KeywordReplacer.java index 3bc099e08..f53d62940 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/textreader/KeywordReplacer.java +++ b/Essentials/src/main/java/com/earth2me/essentials/textreader/KeywordReplacer.java @@ -253,7 +253,7 @@ public class KeywordReplacer implements IText { replacer = Integer.toString(ess.getOnlinePlayers().size() - playerHidden); break; case UNIQUE: - replacer = NumberFormat.getInstance().format(ess.getUserMap().getUniqueUsers()); + replacer = NumberFormat.getInstance().format(ess.getUsers().getUserCount()); break; case WORLDS: final StringBuilder worldsBuilder = new StringBuilder(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/userstorage/IUserMap.java b/Essentials/src/main/java/com/earth2me/essentials/userstorage/IUserMap.java new file mode 100644 index 000000000..6bb13d554 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/userstorage/IUserMap.java @@ -0,0 +1,51 @@ +package com.earth2me.essentials.userstorage; + +import com.earth2me.essentials.User; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public interface IUserMap { + /** + * Gets all the UUIDs of every User which has joined. + * @return the UUIDs of all Users. + */ + Set getAllUserUUIDs(); + + /** + * Gets the current amount of users loaded into memory. + * @return the amount of users loaded into memory. + */ + long getCachedCount(); + + /** + * Gets the amount of users stored by Essentials. + * @return the amount of users stored by Essentials. + */ + int getUserCount(); + + User getUser(final UUID uuid); + + User getUser(final Player base); + + User getUser(final String name); + + User loadUncachedUser(final Player base); + + /** + * Gets a User by the given UUID in the cache, if present, otherwise loads the user without placing them in the cache. + * Ideally to be used when running operations on all stored users. + * + * @param uuid the UUID of the user to get. + * @return the User with the given UUID, or null if not found. + */ + User loadUncachedUser(final UUID uuid); + + /** + * Gets the name to UUID cache. + * @return the name to UUID cache. + */ + Map getNameCache(); +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUUIDCache.java b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUUIDCache.java new file mode 100644 index 000000000..74b88062f --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUUIDCache.java @@ -0,0 +1,250 @@ +package com.earth2me.essentials.userstorage; + +import com.earth2me.essentials.utils.StringUtil; +import com.google.common.io.Files; +import net.ess3.api.IEssentials; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +public class ModernUUIDCache { + private final IEssentials ess; + + /** + * We use a name to uuid map for offline caching due to the following scenario: + * * JRoy and mdcfeYT420 play on a server + * * mdcfeYT420 changes his name to mdcfe + * * mdcfe doesn't log in the server for 31 days + * * JRoy changes his name to mdcfeYT420 + * * mdcfeYT420 (previously JRoy) logs in the server + * In a UUID->Name based map, different uuids now point to the same name + * preventing any command which allows for offline players from resolving a + * given uuid from a given name. + *

+ * This map is backed by a file-based cache. If this cache is missing, a new + * one is populated by iterating over all files in the userdata folder and + * caching the {@code last-account-name} value. + */ + private final ConcurrentHashMap nameToUuidMap = new ConcurrentHashMap<>(); + private final CopyOnWriteArraySet uuidCache = new CopyOnWriteArraySet<>(); + + private final ScheduledExecutorService writeExecutor = Executors.newSingleThreadScheduledExecutor(); + private final AtomicBoolean pendingNameWrite = new AtomicBoolean(false); + private final AtomicBoolean pendingUuidWrite = new AtomicBoolean(false); + private final File nameToUuidFile; + private final File uuidCacheFile; + + public ModernUUIDCache(final IEssentials ess) { + this.ess = ess; + this.nameToUuidFile = new File(ess.getDataFolder(), "usermap.bin"); + this.uuidCacheFile = new File(ess.getDataFolder(), "uuids.bin"); + loadCache(); + writeExecutor.scheduleWithFixedDelay(() -> { + if (pendingNameWrite.compareAndSet(true, false)) { + saveNameToUuidCache(); + } + + if (pendingUuidWrite.compareAndSet(true, false)) { + saveUuidCache(); + } + }, 5, 5, TimeUnit.SECONDS); + } + + protected UUID getCachedUUID(final String name) { + return nameToUuidMap.get(getSanitizedName(name)); + } + + protected Set getCachedUUIDs() { + return Collections.unmodifiableSet(uuidCache); + } + + protected Map getNameCache() { + return Collections.unmodifiableMap(nameToUuidMap); + } + + protected int getCacheSize() { + return uuidCache.size(); + } + + protected String getSanitizedName(final String name) { + return ess.getSettings().isSafeUsermap() ? StringUtil.safeString(name) : name; + } + + protected void updateCache(final UUID uuid, final String name) { + if (uuidCache.add(uuid)) { + pendingUuidWrite.set(true); + } + if (name != null) { + final String sanitizedName = getSanitizedName(name); + final UUID replacedUuid = nameToUuidMap.put(sanitizedName, uuid); + if (!uuid.equals(replacedUuid)) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.WARNING, "Replaced UUID during cache update for " + sanitizedName + ": " + replacedUuid + " -> " + uuid); + } + pendingNameWrite.set(true); + } + } + } + + protected void removeCache(final UUID uuid) { + if (uuid == null) { + return; + } + + if (uuidCache.remove(uuid)) { + pendingUuidWrite.set(true); + } + + final Set toRemove = new HashSet<>(); + for (final Map.Entry entry : nameToUuidMap.entrySet()) { + if (uuid.equals(entry.getValue())) { + toRemove.add(entry.getKey()); + } + } + + for (final String name : toRemove) { + nameToUuidMap.remove(name); + } + + if (!toRemove.isEmpty()) { + pendingNameWrite.set(true); + } + } + + private void loadCache() { + final boolean debug = ess.getSettings().isDebug(); + + try { + if (!nameToUuidFile.exists()) { + if (!nameToUuidFile.createNewFile()) { + throw new RuntimeException("Error while creating usermap.bin"); + } + return; + } + + if (debug) { + ess.getLogger().log(Level.INFO, "Loading Name->UUID cache from disk..."); + } + + nameToUuidMap.clear(); + + try (final DataInputStream dis = new DataInputStream(new FileInputStream(nameToUuidFile))) { + while (dis.available() > 0) { + final String username = dis.readUTF(); + final UUID uuid = new UUID(dis.readLong(), dis.readLong()); + final UUID previous = nameToUuidMap.put(username, uuid); + if (previous != null && debug) { + ess.getLogger().log(Level.WARNING, "Replaced UUID during cache load for " + username + ": " + previous + " -> " + uuid); + } + } + } + } catch (IOException e) { + ess.getLogger().log(Level.SEVERE, "Error while loading Name->UUID cache", e); + } + + try { + if (!uuidCacheFile.exists()) { + if (!uuidCacheFile.createNewFile()) { + throw new RuntimeException("Error while creating uuids.bin"); + } + return; + } + + if (debug) { + ess.getLogger().log(Level.INFO, "Loading UUID cache from disk..."); + } + + uuidCache.clear(); + + try (final DataInputStream dis = new DataInputStream(new FileInputStream(uuidCacheFile))) { + while (dis.available() > 0) { + final UUID uuid = new UUID(dis.readLong(), dis.readLong()); + if (uuidCache.contains(uuid) && debug) { + ess.getLogger().log(Level.WARNING, "UUID " + uuid + " duplicated in cache"); + } + uuidCache.add(uuid); + } + } + } catch (IOException e) { + ess.getLogger().log(Level.SEVERE, "Error while loading UUID cache", e); + } + } + + private void saveUuidCache() { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "Saving UUID cache to disk..."); + } + + try { + final File tmpMap = File.createTempFile("uuids", ".tmp.bin", ess.getDataFolder()); + + writeUuidCache(tmpMap, uuidCache); + //noinspection UnstableApiUsage + Files.move(tmpMap, uuidCacheFile); + } catch (IOException e) { + ess.getLogger().log(Level.SEVERE, "Error while saving UUID cache", e); + } + } + + private void saveNameToUuidCache() { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "Saving Name->UUID cache to disk..."); + } + + try { + final File tmpMap = File.createTempFile("usermap", ".tmp.bin", ess.getDataFolder()); + + writeNameUuidMap(tmpMap, nameToUuidMap); + //noinspection UnstableApiUsage + Files.move(tmpMap, nameToUuidFile); + } catch (IOException e) { + ess.getLogger().log(Level.SEVERE, "Error while saving Name->UUID cache", e); + } + } + + protected void blockingSave() { + saveUuidCache(); + saveNameToUuidCache(); + } + + public static void writeUuidCache(final File file, Set uuids) throws IOException { + try (final DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) { + for (final UUID uuid: uuids) { + dos.writeLong(uuid.getMostSignificantBits()); + dos.writeLong(uuid.getLeastSignificantBits()); + } + } + } + + public static void writeNameUuidMap(final File file, final Map nameToUuidMap) throws IOException { + try (final DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) { + for (final Map.Entry entry : nameToUuidMap.entrySet()) { + dos.writeUTF(entry.getKey()); + final UUID uuid = entry.getValue(); + dos.writeLong(uuid.getMostSignificantBits()); + dos.writeLong(uuid.getLeastSignificantBits()); + } + } + } + + public void shutdown() { + writeExecutor.shutdownNow(); + blockingSave(); + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java new file mode 100644 index 000000000..2ea21e487 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/userstorage/ModernUserMap.java @@ -0,0 +1,177 @@ +package com.earth2me.essentials.userstorage; + +import com.earth2me.essentials.OfflinePlayer; +import com.earth2me.essentials.User; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import net.ess3.api.IEssentials; +import org.bukkit.entity.Player; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; + +public class ModernUserMap extends CacheLoader implements IUserMap { + private final transient IEssentials ess; + private final transient ModernUUIDCache uuidCache; + private final transient LoadingCache userCache; + + public ModernUserMap(final IEssentials ess) { + this.ess = ess; + this.uuidCache = new ModernUUIDCache(ess); + this.userCache = CacheBuilder.newBuilder() + .maximumSize(ess.getSettings().getMaxUserCacheCount()) + .softValues() + .build(this); + } + + @Override + public Set getAllUserUUIDs() { + return uuidCache.getCachedUUIDs(); + } + + @Override + public long getCachedCount() { + return userCache.size(); + } + + @Override + public int getUserCount() { + return uuidCache.getCacheSize(); + } + + @Override + public User getUser(final UUID uuid) { + if (uuid == null) { + return null; + } + + try { + return userCache.get(uuid); + } catch (ExecutionException e) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.WARNING, "Exception while getting user for " + uuid, e); + } + return null; + } + } + + @Override + public User getUser(final Player base) { + final User user = loadUncachedUser(base); + userCache.put(user.getUUID(), user); + return user; + } + + @Override + public User getUser(final String name) { + if (name == null) { + return null; + } + + final User user = getUser(uuidCache.getCachedUUID(name)); + if (user != null && user.getBase() instanceof OfflinePlayer) { + if (user.getLastAccountName() != null) { + ((OfflinePlayer) user.getBase()).setName(user.getLastAccountName()); + } else { + ((OfflinePlayer) user.getBase()).setName(name); + } + } + return user; + } + + public void addCachedNpcName(final UUID uuid, final String name) { + if (uuid == null || name == null) { + return; + } + + uuidCache.updateCache(uuid, name); + } + + @SuppressWarnings("NullableProblems") + @Override + public User load(final UUID uuid) throws Exception { + final User user = loadUncachedUser(uuid); + if (user != null) { + return user; + } + + throw new Exception("User not found!"); + } + + @Override + public User loadUncachedUser(final Player base) { + if (base == null) { + return null; + } + + User user = getUser(base.getUniqueId()); + if (user == null) { + ess.getLogger().log(Level.INFO, "Essentials created a User for " + base.getName() + " (" + base.getUniqueId() + ") for non Bukkit type: " + base.getClass().getName()); + user = new User(base, ess); + } else if (!base.equals(user.getBase())) { + ess.getLogger().log(Level.INFO, "Essentials updated the underlying Player object for " + user.getUUID()); + user.update(base); + } + uuidCache.updateCache(user.getUUID(), user.getName()); + + return user; + } + + @Override + public User loadUncachedUser(final UUID uuid) { + User user = userCache.getIfPresent(uuid); + if (user != null) { + return user; + } + + Player player = ess.getServer().getPlayer(uuid); + if (player != null) { + // This is a real player, cache their UUID. + user = new User(player, ess); + uuidCache.updateCache(uuid, player.getName()); + return user; + } + + final File userFile = getUserFile(uuid); + if (userFile.exists()) { + player = new OfflinePlayer(uuid, ess.getServer()); + user = new User(player, ess); + ((OfflinePlayer) player).setName(user.getLastAccountName()); + uuidCache.updateCache(uuid, null); + return user; + } + + return null; + } + + @Override + public Map getNameCache() { + return uuidCache.getNameCache(); + } + + public String getSanitizedName(final String name) { + return uuidCache.getSanitizedName(name); + } + + public void blockingSave() { + uuidCache.blockingSave(); + } + + public void invalidate(final UUID uuid) { + userCache.invalidate(uuid); + uuidCache.removeCache(uuid); + } + + private File getUserFile(final UUID uuid) { + return new File(new File(ess.getDataFolder(), "userdata"), uuid.toString() + ".yml"); + } + + public void shutdown() { + uuidCache.shutdown(); + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/utils/StringUtil.java b/Essentials/src/main/java/com/earth2me/essentials/utils/StringUtil.java index b9ba37df0..d9d6602d7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/utils/StringUtil.java +++ b/Essentials/src/main/java/com/earth2me/essentials/utils/StringUtil.java @@ -25,6 +25,9 @@ public final class StringUtil { //Used to clean strings/names before saving as filenames/permissions public static String safeString(final String string) { + if (string == null) { + return null; + } return STRICTINVALIDCHARS.matcher(string.toLowerCase(Locale.ENGLISH)).replaceAll("_"); } diff --git a/Essentials/src/main/resources/messages.properties b/Essentials/src/main/resources/messages.properties index daa9ef0ae..226c8484b 100644 --- a/Essentials/src/main/resources/messages.properties +++ b/Essentials/src/main/resources/messages.properties @@ -1415,6 +1415,9 @@ userIsAwaySelf=\u00a77You are now AFK. userIsAwaySelfWithMessage=\u00a77You are now AFK. userIsNotAwaySelf=\u00a77You are no longer AFK. userJailed=\u00a76You have been jailed\! +usermapEntry=\u00a7c{0} \u00a76is mapped to \u00a7c{1}\u00a76. +usermapPurge=\u00a76Checking for files in userdata that are not mapped, results will be logged to console. Destructive Mode: {0} +usermapSize=\u00a76Current cached users in user map is \u00a7c{0}\u00a76/\u00a7c{1}\u00a76/\u00a7c{2}\u00a76. userUnknown=\u00a74Warning\: The user ''\u00a7c{0}\u00a74'' has never joined this server. usingTempFolderForTesting=Using temp folder for testing\: vanish=\u00a76Vanish for {0}\u00a76\: {1} diff --git a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java index 07d60ba20..3a5cf72f7 100644 --- a/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java +++ b/EssentialsDiscord/src/main/java/net/essentialsx/discord/listeners/BukkitListener.java @@ -137,7 +137,7 @@ public class BukkitListener implements Listener { MessageUtil.sanitizeDiscordMarkdown(player.getDisplayName()), MessageUtil.sanitizeDiscordMarkdown(message), jda.getPlugin().getEss().getOnlinePlayers().size() - (join ? 0 : 1), - jda.getPlugin().getEss().getUserMap().getUniqueUsers()), + jda.getPlugin().getEss().getUsers().getUserCount()), player); } diff --git a/providers/1_12Provider/src/main/java/com/earth2me/essentials/OfflinePlayer.java b/providers/1_12Provider/src/main/java/com/earth2me/essentials/OfflinePlayer.java index 882605d90..99772592a 100644 --- a/providers/1_12Provider/src/main/java/com/earth2me/essentials/OfflinePlayer.java +++ b/providers/1_12Provider/src/main/java/com/earth2me/essentials/OfflinePlayer.java @@ -1341,7 +1341,7 @@ public class OfflinePlayer implements Player { return name; } - protected void setName(final String name) { + public void setName(final String name) { this.name = base.getName(); if (this.name == null) { this.name = name;