Rewrite User storage and UUID cache (#4581)

Co-authored-by: triagonal <10545540+triagonal@users.noreply.github.com>
Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>
This commit is contained in:
Josh Roy 2022-09-04 10:42:43 -04:00
parent d75d3cd001
commit 0ca58ce4ba
No known key found for this signature in database
GPG Key ID: 86A69D08540BC29A
31 changed files with 814 additions and 682 deletions

2
.gitignore vendored
View File

@ -15,6 +15,8 @@
/Essentials/kits.yml
/Essentials/userdata/testplayer1.yml
/Essentials/usermap.csv
/Essentials/usermap.bin
/Essentials/uuids.bin
# Build files
.gradle/

View File

@ -24,4 +24,4 @@
</list>
</option>
</component>
</project>
</project>

View File

@ -28,8 +28,8 @@ public class BalanceTopImpl implements BalanceTop {
private void calculateBalanceTopMap() {
final List<Entry> 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

View File

@ -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;
}

View File

@ -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;

View File

@ -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<String, UUID> 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<UUID, Long> uuids = new HashMap<>();
final Map<String, UUID> 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();

View File

@ -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();

View File

@ -330,4 +330,8 @@ public interface IUser {
this.time = time;
}
}
List<String> getPastUsernames();
void addPastUsername(String username);
}

View File

@ -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<String, UUID> names, final ConcurrentSkipListMap<UUID, ArrayList<String>> 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<String> list = new ArrayList<>();
list.add(name);
history.put(uuid, list);
} else {
final ArrayList<String> 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<String, UUID> names;
private WriteRunner(final File location, final File endFile, final Map<String, UUID> 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<String, UUID> 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
}
}

View File

@ -106,7 +106,7 @@ public class User extends UserData implements Comparable<User>, IMessageRecipien
this.messageRecipient = new SimpleMessageRecipient(ess, this);
}
void update(final Player base) {
public void update(final Player base) {
setBase(base);
}

View File

@ -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<String> 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<String> getPastUsernames() {
return holder.pastUsernames();
}
public void addPastUsername(String username) {
final List<String> usernames = holder.pastUsernames();
usernames.add(0, username);
holder.pastUsernames(usernames);
config.save();
}
public boolean isShouting() {
if (holder.shouting() == null) {
holder.shouting(ess.getSettings().isShoutDefault());

View File

@ -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<String, User> implements IConf {
private static boolean legacy = false;
private static Method getLegacy;
private final transient IEssentials ess;
private final transient ConcurrentSkipListSet<UUID> keys = new ConcurrentSkipListSet<>();
private final transient ConcurrentSkipListMap<String, UUID> names = new ConcurrentSkipListMap<>();
private final transient ConcurrentSkipListMap<UUID, ArrayList<String>> history = new ConcurrentSkipListMap<>();
private final UUIDMap uuidMap;
private final transient Cache<String, User> 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<UUID, User> remListener = new UserMapRemovalListener();
//users = CacheBuilder.newBuilder().maximumSize(ess.getSettings().getMaxUserCacheCount()).softValues().removalListener(remListener).build(this);
final CacheBuilder<Object, Object> 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<String, User>) 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<UUID> getAllUniqueUsers() {
return Collections.unmodifiableSet(keys);
return userMap.getAllUserUUIDs();
}
public int getUniqueUsers() {
return keys.size();
return userMap.getUserCount();
}
protected ConcurrentSkipListMap<String, UUID> getNames() {
return names;
}
protected ConcurrentSkipListMap<UUID, ArrayList<String>> getHistory() {
return history;
}
public List<String> 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<String, User> legacyBuild(final CacheBuilder builder) {
Method build = null;
for (final Method method : builder.getClass().getDeclaredMethods()) {
if (method.getName().equals("build")) {
build = method;
break;
}
}
Cache<String, User> legacyUsers;
try {
assert build != null;
build.setAccessible(true);
legacyUsers = (Cache<String, User>) build.invoke(builder, this);
} catch (final IllegalAccessException | InvocationTargetException e) {
legacyUsers = null;
}
return legacyUsers;
}
}

View File

@ -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) {

View File

@ -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));

View File

@ -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("/<command> uuidtest <name>");
// 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<String, UUID> 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<UUID> 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<String, UUID> 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<String> list = Lists.newArrayList("config", "kits", "log", "discord", "worth", "tpr", "spawns", "commands", "all");
for (String arg : args) {

View File

@ -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);
}

View File

@ -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<String> history = ess.getUserMap().getUserHistory(user.getBase().getUniqueId());
if (history != null && history.size() > 1) {
final List<String> 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<String> history = ess.getUserMap().getUserHistory(user.getBase().getUniqueId());
final List<String> 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<String> 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;
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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

View File

@ -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<String> pastUsernames;
public List<String> pastUsernames() {
if (this.pastUsernames == null) {
this.pastUsernames = new ArrayList<>();
}
return this.pastUsernames;
}
public void pastUsernames(List<String> value) {
this.pastUsernames = value;
}
private @NonNull Timestamps timestamps = new Timestamps();
public Timestamps timestamps() {

View File

@ -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

View File

@ -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));
}

View File

@ -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();

View File

@ -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<UUID> 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<String, UUID> getNameCache();
}

View File

@ -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.
* <p>
* 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<String, UUID> nameToUuidMap = new ConcurrentHashMap<>();
private final CopyOnWriteArraySet<UUID> 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<UUID> getCachedUUIDs() {
return Collections.unmodifiableSet(uuidCache);
}
protected Map<String, UUID> 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<String> toRemove = new HashSet<>();
for (final Map.Entry<String, UUID> 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<UUID> 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<String, UUID> nameToUuidMap) throws IOException {
try (final DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
for (final Map.Entry<String, UUID> 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();
}
}

View File

@ -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<UUID, User> implements IUserMap {
private final transient IEssentials ess;
private final transient ModernUUIDCache uuidCache;
private final transient LoadingCache<UUID, User> 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<UUID> 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<String, UUID> 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();
}
}

View File

@ -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("_");
}

View File

@ -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}

View File

@ -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);
}

View File

@ -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;