arguments) {
+ if (arguments.size() < 2) {
+ sender.sendMessage("Check if a player has permission:");
+ sender.sendMessage("Example: /authme debug perm bobby my.perm.node");
+ return;
+ }
+
+ final String playerName = arguments.get(0);
+ final String permissionNode = arguments.get(1);
+
+ Player player = bukkitService.getPlayerExact(playerName);
+ if (player == null) {
+ OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerName);
+ if (offlinePlayer == null) {
+ sender.sendMessage(ChatColor.DARK_RED + "Player '" + playerName + "' does not exist");
+ } else {
+ sender.sendMessage("Player '" + playerName + "' not online; checking with offline player");
+ performPermissionCheck(offlinePlayer, permissionNode, permissionsManager::hasPermissionOffline, sender);
+ }
+ } else {
+ performPermissionCheck(player, permissionNode, permissionsManager::hasPermission, sender);
+ }
+ }
+
+ /**
+ * Performs a permission check and informs the given sender of the result. {@code permissionChecker} is the
+ * permission check to perform with the given {@code node} and the {@code player}.
+ *
+ * @param player the player to check a permission for
+ * @param node the node of the permission to check
+ * @param permissionChecker permission checking function
+ * @param sender the sender to inform of the result
+ * @param the player type
+ */
+ private static
void performPermissionCheck(
+ P player, String node, BiFunction
permissionChecker, CommandSender sender) {
+
+ PermissionNode permNode = getPermissionNode(sender, node);
+ if (permissionChecker.apply(player, permNode)) {
+ sender.sendMessage(ChatColor.DARK_GREEN + "Success: player '" + player.getName()
+ + "' has permission '" + node + "'");
+ } else {
+ sender.sendMessage(ChatColor.DARK_RED + "Check failed: player '" + player.getName()
+ + "' does NOT have permission '" + node + "'");
+ }
+
+ }
+
+ /**
+ * Based on the given permission node (String), tries to find the according AuthMe {@link PermissionNode}
+ * instance, or creates a new one if not available.
+ *
+ * @param sender the sender (used to inform him if no AuthMe PermissionNode can be matched)
+ * @param node the node to search for
+ * @return the node as {@link PermissionNode} object
+ */
+ private static PermissionNode getPermissionNode(CommandSender sender, String node) {
+ Optional extends PermissionNode> permNode = PERMISSION_NODE_CLASSES.stream()
+ .map(Class::getEnumConstants)
+ .flatMap(Arrays::stream)
+ .filter(perm -> perm.getNode().equals(node))
+ .findFirst();
+ if (permNode.isPresent()) {
+ return permNode.get();
+ } else {
+ sender.sendMessage("Did not detect AuthMe permission; using default permission = DENIED");
+ return createPermNode(node);
+ }
+ }
+
+ private static PermissionNode createPermNode(String node) {
+ return new PermissionNode() {
+ @Override
+ public String getNode() {
+ return node;
+ }
+
+ @Override
+ public DefaultPermission getDefaultPermission() {
+ return DefaultPermission.NOT_ALLOWED;
+ }
+ };
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java
new file mode 100644
index 00000000..bf7f9b3c
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java
@@ -0,0 +1,129 @@
+package fr.xephi.authme.command.executable.authme.debug;
+
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import fr.xephi.authme.data.limbo.LimboService;
+import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
+import fr.xephi.authme.service.BukkitService;
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+import javax.inject.Inject;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
+import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.applyToLimboPlayersMap;
+
+/**
+ * Shows the data stored in LimboPlayers and the equivalent properties on online players.
+ */
+class LimboPlayerViewer implements DebugSection {
+
+ @Inject
+ private LimboService limboService;
+
+ @Inject
+ private LimboPersistence limboPersistence;
+
+ @Inject
+ private BukkitService bukkitService;
+
+ @Override
+ public String getName() {
+ return "limbo";
+ }
+
+ @Override
+ public String getDescription() {
+ return "View LimboPlayers and player's \"limbo stats\"";
+ }
+
+ @Override
+ public void execute(CommandSender sender, List arguments) {
+ if (arguments.isEmpty()) {
+ sender.sendMessage("/authme debug limbo : show a player's limbo info");
+ sender.sendMessage("Available limbo records: " + applyToLimboPlayersMap(limboService, Map::keySet));
+ return;
+ }
+
+ LimboPlayer memoryLimbo = limboService.getLimboPlayer(arguments.get(0));
+ Player player = bukkitService.getPlayerExact(arguments.get(0));
+ LimboPlayer diskLimbo = player != null ? limboPersistence.getLimboPlayer(player) : null;
+ if (memoryLimbo == null && player == null) {
+ sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'");
+ return;
+ }
+
+ sender.sendMessage(ChatColor.GOLD + "Showing disk limbo / limbo / player info for '" + arguments.get(0) + "'");
+ new InfoDisplayer(sender, diskLimbo, memoryLimbo, player)
+ .sendEntry("Is op", LimboPlayer::isOperator, Player::isOp)
+ .sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed)
+ .sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight)
+ .sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed)
+ .sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation()))
+ .sendEntry("Group", LimboPlayer::getGroup, p -> "");
+ sender.sendMessage("Note: group is not shown for Player. Use /authme debug groups");
+ }
+
+ /**
+ * Displays the info for the given LimboPlayer and Player to the provided CommandSender.
+ */
+ private static final class InfoDisplayer {
+ private final CommandSender sender;
+ private final Optional diskLimbo;
+ private final Optional memoryLimbo;
+ private final Optional player;
+
+ /**
+ * Constructor.
+ *
+ * @param sender command sender to send the information to
+ * @param memoryLimbo the limbo player to get data from
+ * @param player the player to get data from
+ */
+ InfoDisplayer(CommandSender sender, LimboPlayer diskLimbo, LimboPlayer memoryLimbo, Player player) {
+ this.sender = sender;
+ this.diskLimbo = Optional.ofNullable(diskLimbo);
+ this.memoryLimbo = Optional.ofNullable(memoryLimbo);
+ this.player = Optional.ofNullable(player);
+
+ if (memoryLimbo == null) {
+ sender.sendMessage("Note: no Limbo information available");
+ }
+ if (player == null) {
+ sender.sendMessage("Note: player is not online");
+ } else if (diskLimbo == null) {
+ sender.sendMessage("Note: no Limbo on disk available");
+ }
+ }
+
+ /**
+ * Displays a piece of information to the command sender.
+ *
+ * @param title the designation of the piece of information
+ * @param limboGetter getter for data retrieval on the LimboPlayer
+ * @param playerGetter getter for data retrieval on Player
+ * @param the data type
+ * @return this instance (for chaining)
+ */
+ InfoDisplayer sendEntry(String title,
+ Function limboGetter,
+ Function playerGetter) {
+ sender.sendMessage(
+ title + ": "
+ + getData(diskLimbo, limboGetter)
+ + " / "
+ + getData(memoryLimbo, limboGetter)
+ + " / "
+ + getData(player, playerGetter));
+ return this;
+ }
+
+ static String getData(Optional entity, Function getter) {
+ return entity.map(getter).map(String::valueOf).orElse(" -- ");
+ }
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java
new file mode 100644
index 00000000..a985c827
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java
@@ -0,0 +1,104 @@
+package fr.xephi.authme.command.executable.authme.debug;
+
+import fr.xephi.authme.data.auth.PlayerAuth;
+import fr.xephi.authme.datasource.DataSource;
+import fr.xephi.authme.security.crypts.HashedPassword;
+import fr.xephi.authme.util.StringUtils;
+import org.bukkit.ChatColor;
+import org.bukkit.command.CommandSender;
+
+import javax.inject.Inject;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
+
+/**
+ * Allows to view the data of a PlayerAuth in the database.
+ */
+class PlayerAuthViewer implements DebugSection {
+
+ @Inject
+ private DataSource dataSource;
+
+ @Override
+ public String getName() {
+ return "db";
+ }
+
+ @Override
+ public String getDescription() {
+ return "View player's data in the database";
+ }
+
+ @Override
+ public void execute(CommandSender sender, List arguments) {
+ if (arguments.isEmpty()) {
+ sender.sendMessage("Enter player name to view his data in the database.");
+ sender.sendMessage("Example: /authme debug db Bobby");
+ return;
+ }
+
+ PlayerAuth auth = dataSource.getAuth(arguments.get(0));
+ if (auth == null) {
+ sender.sendMessage("No record exists for '" + arguments.get(0) + "'");
+ } else {
+ displayAuthToSender(auth, sender);
+ }
+ }
+
+ /**
+ * Outputs the PlayerAuth information to the given sender.
+ *
+ * @param auth the PlayerAuth to display
+ * @param sender the sender to send the messages to
+ */
+ private void displayAuthToSender(PlayerAuth auth, CommandSender sender) {
+ sender.sendMessage(ChatColor.GOLD + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName());
+ sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getIp() + ". Group: " + auth.getGroupId());
+ sender.sendMessage("Quit location: "
+ + formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld()));
+ sender.sendMessage("Last login: " + formatLastLogin(auth));
+
+ HashedPassword hashedPass = auth.getPassword();
+ sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ + "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
+ }
+
+ /**
+ * Fail-safe substring method. Guarantees not to show the entire String.
+ *
+ * @param str the string to transform
+ * @param length number of characters to show from the start of the String
+ * @return the first length characters of the string, or half of the string if it is shorter,
+ * or empty string if the string is null or empty
+ */
+ private static String safeSubstring(String str, int length) {
+ if (StringUtils.isEmpty(str)) {
+ return "";
+ } else if (str.length() < length) {
+ return str.substring(0, str.length() / 2) + "...";
+ } else {
+ return str.substring(0, length) + "...";
+ }
+ }
+
+ /**
+ * Formats the last login date from the given PlayerAuth.
+ *
+ * @param auth the auth object
+ * @return the last login as human readable date
+ */
+ private static String formatLastLogin(PlayerAuth auth) {
+ long lastLogin = auth.getLastLogin();
+ if (lastLogin == 0) {
+ return "Never (0)";
+ } else {
+ LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastLogin), ZoneId.systemDefault());
+ return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date);
+ }
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java
index b0abcd55..1d505254 100644
--- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java
+++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java
@@ -41,8 +41,8 @@ class TestEmailSender implements DebugSection {
@Override
public void execute(CommandSender sender, List arguments) {
if (!sendMailSSL.hasAllInformation()) {
- sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " +
- "for sending emails. Please check your config.yml");
+ sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml "
+ + "for sending emails. Please check your config.yml");
return;
}
@@ -69,7 +69,8 @@ class TestEmailSender implements DebugSection {
}
String email = auth.getEmail();
if (email == null || "your@email.com".equals(email)) {
- sender.sendMessage(ChatColor.RED + "No email set for your account! Please use /authme debug mail ");
+ sender.sendMessage(ChatColor.RED + "No email set for your account!"
+ + " Please use /authme debug mail ");
return null;
}
return email;
diff --git a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java
index c5756cda..259e20f9 100644
--- a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java
+++ b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java
@@ -3,8 +3,7 @@ package fr.xephi.authme.command.executable.captcha;
import fr.xephi.authme.command.PlayerCommand;
import fr.xephi.authme.data.CaptchaManager;
import fr.xephi.authme.data.auth.PlayerCache;
-import fr.xephi.authme.command.PlayerCommand;
-import fr.xephi.authme.data.limbo.LimboCache;
+import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.service.CommonService;
import org.bukkit.entity.Player;
@@ -24,7 +23,7 @@ public class CaptchaCommand extends PlayerCommand {
private CommonService commonService;
@Inject
- private LimboCache limboCache;
+ private LimboService limboService;
@Override
public void runCommand(Player player, List arguments) {
@@ -44,7 +43,7 @@ public class CaptchaCommand extends PlayerCommand {
if (isCorrectCode) {
commonService.send(player, MessageKey.CAPTCHA_SUCCESS);
commonService.send(player, MessageKey.LOGIN_MESSAGE);
- limboCache.getPlayerData(player.getName()).getMessageTask().setMuted(false);
+ limboService.unmuteMessageTask(player);
} else {
String newCode = captchaManager.generateCode(player.getName());
commonService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, newCode);
diff --git a/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java b/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java
index 151236e1..24a300ef 100644
--- a/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java
+++ b/src/main/java/fr/xephi/authme/command/executable/email/ShowEmailCommand.java
@@ -24,7 +24,7 @@ public class ShowEmailCommand extends PlayerCommand {
@Override
public void runCommand(Player player, List arguments) {
PlayerAuth auth = playerCache.getAuth(player.getName());
- if (auth.getEmail() != null && !"your@email.com".equalsIgnoreCase(auth.getEmail())) {
+ if (auth != null && auth.getEmail() != null && !"your@email.com".equalsIgnoreCase(auth.getEmail())) {
commonService.send(player, MessageKey.EMAIL_SHOW, auth.getEmail());
} else {
commonService.send(player, MessageKey.SHOW_NO_EMAIL);
diff --git a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java
index 6a0a590c..5b9d75ab 100644
--- a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java
+++ b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java
@@ -7,7 +7,10 @@ import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.process.Management;
import fr.xephi.authme.process.register.RegisterSecondaryArgument;
import fr.xephi.authme.process.register.RegistrationType;
-import fr.xephi.authme.process.register.executors.RegistrationExecutorProvider;
+import fr.xephi.authme.process.register.executors.EmailRegisterParams;
+import fr.xephi.authme.process.register.executors.PasswordRegisterParams;
+import fr.xephi.authme.process.register.executors.RegistrationMethod;
+import fr.xephi.authme.process.register.executors.TwoFactorRegisterParams;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.service.CommonService;
import fr.xephi.authme.service.ValidationService;
@@ -42,15 +45,12 @@ public class RegisterCommand extends PlayerCommand {
@Inject
private ValidationService validationService;
- @Inject
- private RegistrationExecutorProvider registrationExecutorProvider;
-
@Override
public void runCommand(Player player, List arguments) {
if (commonService.getProperty(SecuritySettings.PASSWORD_HASH) == HashAlgorithm.TWO_FACTOR) {
//for two factor auth we don't need to check the usage
- management.performRegister(player,
- registrationExecutorProvider.getTwoFactorRegisterExecutor(player));
+ management.performRegister(RegistrationMethod.TWO_FACTOR_REGISTRATION,
+ TwoFactorRegisterParams.of(player));
return;
} else if (arguments.size() < 1) {
commonService.send(player, MessageKey.USAGE_REGISTER);
@@ -82,8 +82,8 @@ public class RegisterCommand extends PlayerCommand {
final String password = arguments.get(0);
final String email = getEmailIfAvailable(arguments);
- management.performRegister(
- player, registrationExecutorProvider.getPasswordRegisterExecutor(player, password, email));
+ management.performRegister(RegistrationMethod.PASSWORD_REGISTRATION,
+ PasswordRegisterParams.of(player, password, email));
}
}
@@ -138,7 +138,8 @@ public class RegisterCommand extends PlayerCommand {
if (!validationService.validateEmail(email)) {
commonService.send(player, MessageKey.INVALID_EMAIL);
} else if (isSecondArgValidForEmailRegistration(player, arguments)) {
- management.performRegister(player, registrationExecutorProvider.getEmailRegisterExecutor(player, email));
+ management.performRegister(RegistrationMethod.EMAIL_REGISTRATION,
+ EmailRegisterParams.of(player, email));
}
}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java
new file mode 100644
index 00000000..f085afbb
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/AllowFlightRestoreType.java
@@ -0,0 +1,43 @@
+package fr.xephi.authme.data.limbo;
+
+import org.bukkit.entity.Player;
+
+import java.util.function.Function;
+
+/**
+ * Possible types to restore the "allow flight" property
+ * from LimboPlayer to Bukkit Player.
+ */
+public enum AllowFlightRestoreType {
+
+ /** Set value from LimboPlayer to Player. */
+ RESTORE(LimboPlayer::isCanFly),
+
+ /** Always set flight enabled to true. */
+ ENABLE(l -> true),
+
+ /** Always set flight enabled to false. */
+ DISABLE(l -> false);
+
+ private final Function valueGetter;
+
+ /**
+ * Constructor.
+ *
+ * @param valueGetter function with which the value to set on the player can be retrieved
+ */
+ AllowFlightRestoreType(Function valueGetter) {
+ this.valueGetter = valueGetter;
+ }
+
+ /**
+ * Restores the "allow flight" property from the LimboPlayer to the Player.
+ * This method behaves differently for each restoration type.
+ *
+ * @param player the player to modify
+ * @param limbo the limbo player to read from
+ */
+ public void restoreAllowFlight(Player player, LimboPlayer limbo) {
+ player.setAllowFlight(valueGetter.apply(limbo));
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java b/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java
deleted file mode 100644
index 26883ca9..00000000
--- a/src/main/java/fr/xephi/authme/data/limbo/LimboCache.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package fr.xephi.authme.data.limbo;
-
-import fr.xephi.authme.ConsoleLogger;
-import fr.xephi.authme.permission.PermissionsManager;
-import fr.xephi.authme.settings.SpawnLoader;
-import org.bukkit.Location;
-import org.bukkit.entity.Player;
-
-import javax.inject.Inject;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-/**
- * Manages all {@link LimboPlayer} instances.
- */
-public class LimboCache {
-
- private final Map cache = new ConcurrentHashMap<>();
-
- private LimboPlayerStorage limboPlayerStorage;
- private PermissionsManager permissionsManager;
- private SpawnLoader spawnLoader;
-
- @Inject
- LimboCache(PermissionsManager permissionsManager, SpawnLoader spawnLoader, LimboPlayerStorage limboPlayerStorage) {
- this.permissionsManager = permissionsManager;
- this.spawnLoader = spawnLoader;
- this.limboPlayerStorage = limboPlayerStorage;
- }
-
- /**
- * Load player data if exist, otherwise current player's data will be stored.
- *
- * @param player Player instance to add.
- */
- public void addPlayerData(Player player) {
- String name = player.getName().toLowerCase();
- Location location = spawnLoader.getPlayerLocationOrSpawn(player);
- boolean operator = player.isOp();
- boolean flyEnabled = player.getAllowFlight();
- float walkSpeed = player.getWalkSpeed();
- float flySpeed = player.getFlySpeed();
- String playerGroup = "";
- if (permissionsManager.hasGroupSupport()) {
- playerGroup = permissionsManager.getPrimaryGroup(player);
- }
- ConsoleLogger.debug("Player `{0}` has primary group `{1}`", player.getName(), playerGroup);
-
- if (limboPlayerStorage.hasData(player)) {
- LimboPlayer cache = limboPlayerStorage.readData(player);
- if (cache != null) {
- location = cache.getLocation();
- playerGroup = cache.getGroup();
- operator = cache.isOperator();
- flyEnabled = cache.isCanFly();
- walkSpeed = cache.getWalkSpeed();
- flySpeed = cache.getFlySpeed();
- }
- } else {
- limboPlayerStorage.saveData(player);
- }
-
- cache.put(name, new LimboPlayer(location, operator, playerGroup, flyEnabled, walkSpeed, flySpeed));
- }
-
- /**
- * Restore player's data to player if exist.
- *
- * @param player Player instance to restore
- */
- public void restoreData(Player player) {
- String lowerName = player.getName().toLowerCase();
- if (cache.containsKey(lowerName)) {
- LimboPlayer data = cache.get(lowerName);
- player.setOp(data.isOperator());
- player.setAllowFlight(data.isCanFly());
- float walkSpeed = data.getWalkSpeed();
- float flySpeed = data.getFlySpeed();
- // Reset the speed value if it was 0
- if (walkSpeed < 0.01f) {
- walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED;
- }
- if (flySpeed < 0.01f) {
- flySpeed = LimboPlayer.DEFAULT_FLY_SPEED;
- }
- player.setWalkSpeed(walkSpeed);
- player.setFlySpeed(flySpeed);
- data.clearTasks();
- }
- }
-
- /**
- * Remove PlayerData from cache and disk.
- *
- * @param player Player player to remove.
- */
- public void deletePlayerData(Player player) {
- removeFromCache(player);
- limboPlayerStorage.removeData(player);
- }
-
- /**
- * Remove PlayerData from cache.
- *
- * @param player player to remove.
- */
- public void removeFromCache(Player player) {
- String name = player.getName().toLowerCase();
- LimboPlayer cachedPlayer = cache.remove(name);
- if (cachedPlayer != null) {
- cachedPlayer.clearTasks();
- }
- }
-
- /**
- * Method getPlayerData.
- *
- * @param name String
- *
- * @return PlayerData
- */
- public LimboPlayer getPlayerData(String name) {
- checkNotNull(name);
- return cache.get(name.toLowerCase());
- }
-
- /**
- * Method hasPlayerData.
- *
- * @param name String
- *
- * @return boolean
- */
- public boolean hasPlayerData(String name) {
- checkNotNull(name);
- return cache.containsKey(name.toLowerCase());
- }
-
- /**
- * Method updatePlayerData.
- *
- * @param player Player
- */
- public void updatePlayerData(Player player) {
- checkNotNull(player);
- removeFromCache(player);
- addPlayerData(player);
- }
-}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java
index 45fcd7ad..6ba4ae2c 100644
--- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java
+++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayer.java
@@ -118,13 +118,7 @@ public class LimboPlayer {
* Clears all tasks associated to the player.
*/
public void clearTasks() {
- if (messageTask != null) {
- messageTask.cancel();
- }
- messageTask = null;
- if (timeoutTask != null) {
- timeoutTask.cancel();
- }
- timeoutTask = null;
+ setMessageTask(null);
+ setTimeoutTask(null);
}
}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerStorage.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerStorage.java
deleted file mode 100644
index 1f077d2a..00000000
--- a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerStorage.java
+++ /dev/null
@@ -1,213 +0,0 @@
-package fr.xephi.authme.data.limbo;
-
-import com.google.common.io.Files;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonDeserializationContext;
-import com.google.gson.JsonDeserializer;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-import fr.xephi.authme.ConsoleLogger;
-import fr.xephi.authme.initialization.DataFolder;
-import fr.xephi.authme.permission.PermissionsManager;
-import fr.xephi.authme.service.BukkitService;
-import fr.xephi.authme.settings.SpawnLoader;
-import fr.xephi.authme.util.FileUtils;
-import fr.xephi.authme.util.PlayerUtils;
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.bukkit.entity.Player;
-
-import javax.inject.Inject;
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.nio.charset.StandardCharsets;
-
-/**
- * Class used to store player's data (OP, flying, speed, position) to disk.
- */
-public class LimboPlayerStorage {
-
- private final Gson gson;
- private final File cacheDir;
- private PermissionsManager permissionsManager;
- private SpawnLoader spawnLoader;
- private BukkitService bukkitService;
-
- @Inject
- LimboPlayerStorage(@DataFolder File dataFolder, PermissionsManager permsMan,
- SpawnLoader spawnLoader, BukkitService bukkitService) {
- this.permissionsManager = permsMan;
- this.spawnLoader = spawnLoader;
- this.bukkitService = bukkitService;
-
- cacheDir = new File(dataFolder, "playerdata");
- if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
- ConsoleLogger.warning("Failed to create userdata directory.");
- }
- gson = new GsonBuilder()
- .registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
- .registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer())
- .setPrettyPrinting()
- .create();
- }
-
- /**
- * Read and construct new PlayerData from existing player data.
- *
- * @param player player to read
- *
- * @return PlayerData object if the data is exist, null otherwise.
- */
- public LimboPlayer readData(Player player) {
- String id = PlayerUtils.getUUIDorName(player);
- File file = new File(cacheDir, id + File.separator + "data.json");
- if (!file.exists()) {
- return null;
- }
-
- try {
- String str = Files.toString(file, StandardCharsets.UTF_8);
- return gson.fromJson(str, LimboPlayer.class);
- } catch (IOException e) {
- ConsoleLogger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
- return null;
- }
- }
-
- /**
- * Save player data (OP, flying, location, etc) to disk.
- *
- * @param player player to save
- */
- public void saveData(Player player) {
- String id = PlayerUtils.getUUIDorName(player);
- Location location = spawnLoader.getPlayerLocationOrSpawn(player);
- String group = "";
- if (permissionsManager.hasGroupSupport()) {
- group = permissionsManager.getPrimaryGroup(player);
- }
- boolean operator = player.isOp();
- boolean canFly = player.getAllowFlight();
- float walkSpeed = player.getWalkSpeed();
- float flySpeed = player.getFlySpeed();
- LimboPlayer limboPlayer = new LimboPlayer(location, operator, group, canFly, walkSpeed, flySpeed);
- try {
- File file = new File(cacheDir, id + File.separator + "data.json");
- Files.createParentDirs(file);
- Files.touch(file);
- Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
- } catch (IOException e) {
- ConsoleLogger.logException("Failed to write " + player.getName() + " data.", e);
- }
- }
-
- /**
- * Remove player data, this will delete
- * "playerdata/<uuid or name>/" folder from disk.
- *
- * @param player player to remove
- */
- public void removeData(Player player) {
- String id = PlayerUtils.getUUIDorName(player);
- File file = new File(cacheDir, id);
- if (file.exists()) {
- FileUtils.purgeDirectory(file);
- if (!file.delete()) {
- ConsoleLogger.warning("Failed to remove " + player.getName() + " cache.");
- }
- }
- }
-
- /**
- * Use to check is player data is exist.
- *
- * @param player player to check
- *
- * @return true if data exist, false otherwise.
- */
- public boolean hasData(Player player) {
- String id = PlayerUtils.getUUIDorName(player);
- File file = new File(cacheDir, id + File.separator + "data.json");
- return file.exists();
- }
-
- private class LimboPlayerDeserializer implements JsonDeserializer {
- @Override
- public LimboPlayer deserialize(JsonElement jsonElement, Type type,
- JsonDeserializationContext context) {
- JsonObject jsonObject = jsonElement.getAsJsonObject();
- if (jsonObject == null) {
- return null;
- }
-
- Location loc = null;
- String group = "";
- boolean operator = false;
- boolean canFly = false;
- float walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED;
- float flySpeed = LimboPlayer.DEFAULT_FLY_SPEED;
-
- JsonElement e;
- if ((e = jsonObject.getAsJsonObject("location")) != null) {
- JsonObject obj = e.getAsJsonObject();
- World world = bukkitService.getWorld(obj.get("world").getAsString());
- if (world != null) {
- double x = obj.get("x").getAsDouble();
- double y = obj.get("y").getAsDouble();
- double z = obj.get("z").getAsDouble();
- float yaw = obj.get("yaw").getAsFloat();
- float pitch = obj.get("pitch").getAsFloat();
- loc = new Location(world, x, y, z, yaw, pitch);
- }
- }
- if ((e = jsonObject.get("group")) != null) {
- group = e.getAsString();
- }
- if ((e = jsonObject.get("operator")) != null) {
- operator = e.getAsBoolean();
- }
- if ((e = jsonObject.get("can-fly")) != null) {
- canFly = e.getAsBoolean();
- }
- if ((e = jsonObject.get("walk-speed")) != null) {
- walkSpeed = e.getAsFloat();
- }
- if ((e = jsonObject.get("fly-speed")) != null) {
- flySpeed = e.getAsFloat();
- }
-
- return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
- }
- }
-
- private class LimboPlayerSerializer implements JsonSerializer {
- @Override
- public JsonElement serialize(LimboPlayer limboPlayer, Type type,
- JsonSerializationContext context) {
- JsonObject obj = new JsonObject();
- obj.addProperty("group", limboPlayer.getGroup());
-
- Location loc = limboPlayer.getLocation();
- JsonObject obj2 = new JsonObject();
- obj2.addProperty("world", loc.getWorld().getName());
- obj2.addProperty("x", loc.getX());
- obj2.addProperty("y", loc.getY());
- obj2.addProperty("z", loc.getZ());
- obj2.addProperty("yaw", loc.getYaw());
- obj2.addProperty("pitch", loc.getPitch());
- obj.add("location", obj2);
-
- obj.addProperty("operator", limboPlayer.isOperator());
- obj.addProperty("can-fly", limboPlayer.isCanFly());
- obj.addProperty("walk-speed", limboPlayer.getWalkSpeed());
- obj.addProperty("fly-speed", limboPlayer.getFlySpeed());
- return obj;
- }
- }
-
-
-}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java
new file mode 100644
index 00000000..4e0c0a33
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/LimboPlayerTaskManager.java
@@ -0,0 +1,97 @@
+package fr.xephi.authme.data.limbo;
+
+import fr.xephi.authme.data.auth.PlayerCache;
+import fr.xephi.authme.message.MessageKey;
+import fr.xephi.authme.message.Messages;
+import fr.xephi.authme.service.BukkitService;
+import fr.xephi.authme.settings.Settings;
+import fr.xephi.authme.settings.properties.RegistrationSettings;
+import fr.xephi.authme.settings.properties.RestrictionSettings;
+import fr.xephi.authme.task.MessageTask;
+import fr.xephi.authme.task.TimeoutTask;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+
+import javax.inject.Inject;
+
+import static fr.xephi.authme.service.BukkitService.TICKS_PER_SECOND;
+
+/**
+ * Registers tasks associated with a LimboPlayer.
+ */
+class LimboPlayerTaskManager {
+
+ @Inject
+ private Messages messages;
+
+ @Inject
+ private Settings settings;
+
+ @Inject
+ private BukkitService bukkitService;
+
+ @Inject
+ private PlayerCache playerCache;
+
+ LimboPlayerTaskManager() {
+ }
+
+ /**
+ * Registers a {@link MessageTask} for the given player name.
+ *
+ * @param player the player
+ * @param limbo the associated limbo player of the player
+ * @param isRegistered whether the player is registered or not
+ * (false shows "please register", true shows "please log in")
+ */
+ void registerMessageTask(Player player, LimboPlayer limbo, boolean isRegistered) {
+ int interval = settings.getProperty(RegistrationSettings.MESSAGE_INTERVAL);
+ MessageKey key = getMessageKey(isRegistered);
+ if (interval > 0) {
+ MessageTask messageTask = new MessageTask(player, messages.retrieve(key));
+ bukkitService.runTaskTimer(messageTask, 2 * TICKS_PER_SECOND, interval * TICKS_PER_SECOND);
+ limbo.setMessageTask(messageTask);
+ }
+ }
+
+ /**
+ * Registers a {@link TimeoutTask} for the given player according to the configuration.
+ *
+ * @param player the player to register a timeout task for
+ * @param limbo the associated limbo player
+ */
+ void registerTimeoutTask(Player player, LimboPlayer limbo) {
+ final int timeout = settings.getProperty(RestrictionSettings.TIMEOUT) * TICKS_PER_SECOND;
+ if (timeout > 0) {
+ String message = messages.retrieveSingle(MessageKey.LOGIN_TIMEOUT_ERROR);
+ BukkitTask task = bukkitService.runTaskLater(new TimeoutTask(player, message, playerCache), timeout);
+ limbo.setTimeoutTask(task);
+ }
+ }
+
+ /**
+ * Null-safe method to set the muted flag on a message task.
+ *
+ * @param task the task to modify (or null)
+ * @param isMuted the value to set if task is not null
+ */
+ static void setMuted(MessageTask task, boolean isMuted) {
+ if (task != null) {
+ task.setMuted(isMuted);
+ }
+ }
+
+ /**
+ * Returns the appropriate message key according to the registration status and settings.
+ *
+ * @param isRegistered whether or not the username is registered
+ * @return the message key to display to the user
+ */
+ private static MessageKey getMessageKey(boolean isRegistered) {
+ if (isRegistered) {
+ return MessageKey.LOGIN_MESSAGE;
+ } else {
+ return MessageKey.REGISTER_MESSAGE;
+ }
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboService.java b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java
new file mode 100644
index 00000000..e78ca313
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/LimboService.java
@@ -0,0 +1,170 @@
+package fr.xephi.authme.data.limbo;
+
+import fr.xephi.authme.ConsoleLogger;
+import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
+import fr.xephi.authme.settings.Settings;
+import org.bukkit.entity.Player;
+
+import javax.inject.Inject;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_ALLOW_FLIGHT;
+import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_FLY_SPEED;
+import static fr.xephi.authme.settings.properties.LimboSettings.RESTORE_WALK_SPEED;
+
+/**
+ * Service for managing players that are in "limbo," a temporary state players are
+ * put in which have joined but not yet logged in yet.
+ */
+public class LimboService {
+
+ private final Map entries = new ConcurrentHashMap<>();
+
+ @Inject
+ private Settings settings;
+
+ @Inject
+ private LimboPlayerTaskManager taskManager;
+
+ @Inject
+ private LimboServiceHelper helper;
+
+ @Inject
+ private LimboPersistence persistence;
+
+ LimboService() {
+ }
+
+ /**
+ * Creates a LimboPlayer for the given player and revokes all "limbo data" from the player.
+ *
+ * @param player the player to process
+ * @param isRegistered whether or not the player is registered
+ */
+ public void createLimboPlayer(Player player, boolean isRegistered) {
+ final String name = player.getName().toLowerCase();
+
+ LimboPlayer limboFromDisk = persistence.getLimboPlayer(player);
+ if (limboFromDisk != null) {
+ ConsoleLogger.debug("LimboPlayer for `{0}` already exists on disk", name);
+ }
+
+ LimboPlayer existingLimbo = entries.remove(name);
+ if (existingLimbo != null) {
+ existingLimbo.clearTasks();
+ ConsoleLogger.debug("LimboPlayer for `{0}` already present in memory", name);
+ }
+
+ LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
+ limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered), limboPlayer);
+
+ taskManager.registerMessageTask(player, limboPlayer, isRegistered);
+ taskManager.registerTimeoutTask(player, limboPlayer);
+ helper.revokeLimboStates(player);
+ entries.put(name, limboPlayer);
+ persistence.saveLimboPlayer(player, limboPlayer);
+ }
+
+ /**
+ * Returns the limbo player for the given name, or null otherwise.
+ *
+ * @param name the name to retrieve the data for
+ * @return the associated limbo player, or null if none available
+ */
+ public LimboPlayer getLimboPlayer(String name) {
+ return entries.get(name.toLowerCase());
+ }
+
+ /**
+ * Returns whether there is a limbo player for the given name.
+ *
+ * @param name the name to check
+ * @return true if present, false otherwise
+ */
+ public boolean hasLimboPlayer(String name) {
+ return entries.containsKey(name.toLowerCase());
+ }
+
+ /**
+ * Restores the limbo data and subsequently deletes the entry.
+ *
+ * Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
+ * changing the permission group is handled by {@link fr.xephi.authme.permission.AuthGroupHandler}.
+ *
+ * @param player the player whose data should be restored
+ */
+ public void restoreData(Player player) {
+ String lowerName = player.getName().toLowerCase();
+ LimboPlayer limbo = entries.remove(lowerName);
+
+ if (limbo == null) {
+ ConsoleLogger.debug("No LimboPlayer found for `{0}` - cannot restore", lowerName);
+ } else {
+ player.setOp(limbo.isOperator());
+ settings.getProperty(RESTORE_ALLOW_FLIGHT).restoreAllowFlight(player, limbo);
+ settings.getProperty(RESTORE_FLY_SPEED).restoreFlySpeed(player, limbo);
+ settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
+ limbo.clearTasks();
+ ConsoleLogger.debug("Restored LimboPlayer stats for `{0}`", lowerName);
+ persistence.removeLimboPlayer(player);
+ }
+ }
+
+ /**
+ * Creates new tasks for the given player and cancels the old ones for a newly registered player.
+ * This resets his time to log in (TimeoutTask) and updates the message he is shown (MessageTask).
+ *
+ * @param player the player to reset the tasks for
+ */
+ public void replaceTasksAfterRegistration(Player player) {
+ getLimboOrLogError(player, "reset tasks")
+ .ifPresent(limbo -> {
+ taskManager.registerTimeoutTask(player, limbo);
+ taskManager.registerMessageTask(player, limbo, true);
+ });
+ }
+
+ /**
+ * Resets the message task associated with the player's LimboPlayer.
+ *
+ * @param player the player to set a new message task for
+ * @param isRegistered whether or not the player is registered
+ */
+ public void resetMessageTask(Player player, boolean isRegistered) {
+ getLimboOrLogError(player, "reset message task")
+ .ifPresent(limbo -> taskManager.registerMessageTask(player, limbo, isRegistered));
+ }
+
+ /**
+ * @param player the player whose message task should be muted
+ */
+ public void muteMessageTask(Player player) {
+ getLimboOrLogError(player, "mute message task")
+ .ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), true));
+ }
+
+ /**
+ * @param player the player whose message task should be unmuted
+ */
+ public void unmuteMessageTask(Player player) {
+ getLimboOrLogError(player, "unmute message task")
+ .ifPresent(limbo -> LimboPlayerTaskManager.setMuted(limbo.getMessageTask(), false));
+ }
+
+ /**
+ * Returns the limbo player for the given player or logs an error.
+ *
+ * @param player the player to retrieve the limbo player for
+ * @param context the action for which the limbo player is being retrieved (for logging)
+ * @return Optional with the limbo player
+ */
+ private Optional getLimboOrLogError(Player player, String context) {
+ LimboPlayer limbo = entries.get(player.getName().toLowerCase());
+ if (limbo == null) {
+ ConsoleLogger.debug("No LimboPlayer found for `{0}`. Action: {1}", player.getName(), context);
+ }
+ return Optional.ofNullable(limbo);
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java
new file mode 100644
index 00000000..7373212f
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/LimboServiceHelper.java
@@ -0,0 +1,107 @@
+package fr.xephi.authme.data.limbo;
+
+import fr.xephi.authme.ConsoleLogger;
+import fr.xephi.authme.permission.PermissionsManager;
+import fr.xephi.authme.settings.Settings;
+import fr.xephi.authme.settings.SpawnLoader;
+import fr.xephi.authme.settings.properties.RestrictionSettings;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+import javax.inject.Inject;
+
+/**
+ * Helper class for the LimboService.
+ */
+class LimboServiceHelper {
+
+ @Inject
+ private SpawnLoader spawnLoader;
+
+ @Inject
+ private PermissionsManager permissionsManager;
+
+ @Inject
+ private Settings settings;
+
+ /**
+ * Creates a LimboPlayer with the given player's details.
+ *
+ * @param player the player to process
+ * @param isRegistered whether the player is registered
+ * @return limbo player with the player's data
+ */
+ LimboPlayer createLimboPlayer(Player player, boolean isRegistered) {
+ Location location = spawnLoader.getPlayerLocationOrSpawn(player);
+ // For safety reasons an unregistered player should not have OP status after registration
+ boolean isOperator = isRegistered && player.isOp();
+ boolean flyEnabled = player.getAllowFlight();
+ float walkSpeed = player.getWalkSpeed();
+ float flySpeed = player.getFlySpeed();
+ String playerGroup = permissionsManager.hasGroupSupport()
+ ? permissionsManager.getPrimaryGroup(player) : "";
+ ConsoleLogger.debug("Player `{0}` has primary group `{1}`", player.getName(), playerGroup);
+
+ return new LimboPlayer(location, isOperator, playerGroup, flyEnabled, walkSpeed, flySpeed);
+ }
+
+ /**
+ * Removes the data that is saved in a LimboPlayer from the player.
+ *
+ * Note that teleportation on the player is performed by {@link fr.xephi.authme.service.TeleportationService} and
+ * changing the permission group is handled by {@link fr.xephi.authme.permission.AuthGroupHandler}.
+ *
+ * @param player the player to set defaults to
+ */
+ void revokeLimboStates(Player player) {
+ player.setOp(false);
+ player.setAllowFlight(false);
+
+ if (!settings.getProperty(RestrictionSettings.ALLOW_UNAUTHED_MOVEMENT)
+ && settings.getProperty(RestrictionSettings.REMOVE_SPEED)) {
+ player.setFlySpeed(0.0f);
+ player.setWalkSpeed(0.0f);
+ }
+ }
+
+ /**
+ * Merges two existing LimboPlayer instances of a player. Merging is done the following way:
+ *
+ * isOperator, allowFlight: true if either limbo has true
+ * flySpeed, walkSpeed: maximum value of either limbo player
+ * group, location: from old limbo if not empty/null, otherwise from new limbo
+ *
+ *
+ * @param newLimbo the new limbo player
+ * @param oldLimbo the old limbo player
+ * @return merged limbo player if both arguments are not null, otherwise the first non-null argument
+ */
+ LimboPlayer merge(LimboPlayer newLimbo, LimboPlayer oldLimbo) {
+ if (newLimbo == null) {
+ return oldLimbo;
+ } else if (oldLimbo == null) {
+ return newLimbo;
+ }
+
+ boolean isOperator = newLimbo.isOperator() || oldLimbo.isOperator();
+ boolean canFly = newLimbo.isCanFly() || oldLimbo.isCanFly();
+ float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
+ float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
+ String group = firstNotEmpty(newLimbo.getGroup(), oldLimbo.getGroup());
+ Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
+
+ return new LimboPlayer(location, isOperator, group, canFly, walkSpeed, flySpeed);
+ }
+
+ private static String firstNotEmpty(String newGroup, String oldGroup) {
+ ConsoleLogger.debug("Limbo merge: new and old perm groups are `{0}` and `{1}`", newGroup, oldGroup);
+ if ("".equals(oldGroup)) {
+ return newGroup;
+ }
+ return oldGroup;
+ }
+
+ private static Location firstNotNull(Location first, Location second) {
+ return first == null ? second : first;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java b/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java
new file mode 100644
index 00000000..960cdd43
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/WalkFlySpeedRestoreType.java
@@ -0,0 +1,81 @@
+package fr.xephi.authme.data.limbo;
+
+import org.bukkit.entity.Player;
+
+/**
+ * Possible types to restore the walk and fly speed from LimboPlayer
+ * back to Bukkit Player.
+ */
+public enum WalkFlySpeedRestoreType {
+
+ /** Restores from LimboPlayer to Player. */
+ RESTORE {
+ @Override
+ public void restoreFlySpeed(Player player, LimboPlayer limbo) {
+ player.setFlySpeed(limbo.getFlySpeed());
+ }
+
+ @Override
+ public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
+ player.setWalkSpeed(limbo.getWalkSpeed());
+ }
+ },
+
+ /** Restores from LimboPlayer, using the default speed if the speed on LimboPlayer is 0. */
+ RESTORE_NO_ZERO {
+ @Override
+ public void restoreFlySpeed(Player player, LimboPlayer limbo) {
+ float limboFlySpeed = limbo.getFlySpeed();
+ player.setFlySpeed(limboFlySpeed > 0.01f ? limboFlySpeed : LimboPlayer.DEFAULT_FLY_SPEED);
+ }
+
+ @Override
+ public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
+ float limboWalkSpeed = limbo.getWalkSpeed();
+ player.setWalkSpeed(limboWalkSpeed > 0.01f ? limboWalkSpeed : LimboPlayer.DEFAULT_WALK_SPEED);
+ }
+ },
+
+ /** Uses the max speed of Player (current speed) and the LimboPlayer. */
+ MAX_RESTORE {
+ @Override
+ public void restoreFlySpeed(Player player, LimboPlayer limbo) {
+ player.setFlySpeed(Math.max(player.getFlySpeed(), limbo.getFlySpeed()));
+ }
+
+ @Override
+ public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
+ player.setWalkSpeed(Math.max(player.getWalkSpeed(), limbo.getWalkSpeed()));
+ }
+ },
+
+ /** Always sets the default speed to the player. */
+ DEFAULT {
+ @Override
+ public void restoreFlySpeed(Player player, LimboPlayer limbo) {
+ player.setFlySpeed(LimboPlayer.DEFAULT_FLY_SPEED);
+ }
+
+ @Override
+ public void restoreWalkSpeed(Player player, LimboPlayer limbo) {
+ player.setWalkSpeed(LimboPlayer.DEFAULT_WALK_SPEED);
+ }
+ };
+
+ /**
+ * Restores the fly speed from Limbo to Player according to the restoration type.
+ *
+ * @param player the player to modify
+ * @param limbo the limbo player to read from
+ */
+ public abstract void restoreFlySpeed(Player player, LimboPlayer limbo);
+
+ /**
+ * Restores the walk speed from Limbo to Player according to the restoration type.
+ *
+ * @param player the player to modify
+ * @param limbo the limbo player to read from
+ */
+ public abstract void restoreWalkSpeed(Player player, LimboPlayer limbo);
+
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java
new file mode 100644
index 00000000..f07e82cd
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistence.java
@@ -0,0 +1,79 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import fr.xephi.authme.ConsoleLogger;
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import fr.xephi.authme.initialization.SettingsDependent;
+import fr.xephi.authme.initialization.factory.Factory;
+import fr.xephi.authme.settings.Settings;
+import fr.xephi.authme.settings.properties.LimboSettings;
+import org.bukkit.entity.Player;
+
+import javax.inject.Inject;
+
+/**
+ * Handles the persistence of LimboPlayers.
+ */
+public class LimboPersistence implements SettingsDependent {
+
+ private final Factory handlerFactory;
+
+ private LimboPersistenceHandler handler;
+
+ @Inject
+ LimboPersistence(Settings settings, Factory handlerFactory) {
+ this.handlerFactory = handlerFactory;
+ reload(settings);
+ }
+
+ /**
+ * Retrieves the LimboPlayer for the given player if available.
+ *
+ * @param player the player to retrieve the LimboPlayer for
+ * @return the player's limbo player, or null if not available
+ */
+ public LimboPlayer getLimboPlayer(Player player) {
+ try {
+ return handler.getLimboPlayer(player);
+ } catch (Exception e) {
+ ConsoleLogger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e);
+ }
+ return null;
+ }
+
+ /**
+ * Saves the given LimboPlayer for the provided player.
+ *
+ * @param player the player to save the LimboPlayer for
+ * @param limbo the limbo player to save
+ */
+ public void saveLimboPlayer(Player player, LimboPlayer limbo) {
+ try {
+ handler.saveLimboPlayer(player, limbo);
+ } catch (Exception e) {
+ ConsoleLogger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e);
+ }
+ }
+
+ /**
+ * Removes the LimboPlayer for the given player.
+ *
+ * @param player the player whose LimboPlayer should be removed
+ */
+ public void removeLimboPlayer(Player player) {
+ try {
+ handler.removeLimboPlayer(player);
+ } catch (Exception e) {
+ ConsoleLogger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e);
+ }
+ }
+
+ @Override
+ public void reload(Settings settings) {
+ LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE);
+ // If we're changing from an existing handler, output a quick hint that nothing is converted.
+ if (handler != null && handler.getType() != persistenceType) {
+ ConsoleLogger.info("Limbo persistence type has changed! Note that the data is not converted.");
+ }
+ handler = handlerFactory.newInstance(persistenceType.getImplementationClass());
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java
new file mode 100644
index 00000000..95e88aad
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceHandler.java
@@ -0,0 +1,39 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import org.bukkit.entity.Player;
+
+/**
+ * Handles I/O for storing LimboPlayer objects.
+ */
+interface LimboPersistenceHandler {
+
+ /**
+ * Returns the limbo player for the given player if it exists.
+ *
+ * @param player the player
+ * @return the stored limbo player, or null if not available
+ */
+ LimboPlayer getLimboPlayer(Player player);
+
+ /**
+ * Saves the given limbo player for the given player to the disk.
+ *
+ * @param player the player to save the limbo player for
+ * @param limbo the limbo player to save
+ */
+ void saveLimboPlayer(Player player, LimboPlayer limbo);
+
+ /**
+ * Removes the limbo player from the disk.
+ *
+ * @param player the player whose limbo player should be removed
+ */
+ void removeLimboPlayer(Player player);
+
+ /**
+ * @return the type of the limbo persistence implementation
+ */
+ LimboPersistenceType getType();
+
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java
new file mode 100644
index 00000000..68b4611b
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPersistenceType.java
@@ -0,0 +1,37 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+/**
+ * Types of persistence for LimboPlayer objects.
+ */
+public enum LimboPersistenceType {
+
+ /** Store each LimboPlayer in a separate file. */
+ INDIVIDUAL_FILES(SeparateFilePersistenceHandler.class),
+
+ /** Store all LimboPlayers in the same file. */
+ SINGLE_FILE(SingleFilePersistenceHandler.class),
+
+ /** Distribute LimboPlayers by segments into a set number of files. */
+ SEGMENT_FILES(SegmentFilesPersistenceHolder.class),
+
+ /** No persistence to disk. */
+ DISABLED(NoOpPersistenceHandler.class);
+
+ private final Class extends LimboPersistenceHandler> implementationClass;
+
+ /**
+ * Constructor.
+ *
+ * @param implementationClass the implementation class
+ */
+ LimboPersistenceType(Class extends LimboPersistenceHandler> implementationClass) {
+ this.implementationClass= implementationClass;
+ }
+
+ /**
+ * @return class implementing the persistence type
+ */
+ public Class extends LimboPersistenceHandler> getImplementationClass() {
+ return implementationClass;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java
new file mode 100644
index 00000000..94e1950b
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerDeserializer.java
@@ -0,0 +1,115 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import fr.xephi.authme.service.BukkitService;
+import org.bukkit.Location;
+import org.bukkit.World;
+
+import java.lang.reflect.Type;
+import java.util.function.Function;
+
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.CAN_FLY;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.FLY_SPEED;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.GROUP;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.IS_OP;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOCATION;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_PITCH;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_WORLD;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_X;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Y;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_YAW;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.LOC_Z;
+import static fr.xephi.authme.data.limbo.persistence.LimboPlayerSerializer.WALK_SPEED;
+
+/**
+ * Converts a JsonElement to a LimboPlayer.
+ */
+class LimboPlayerDeserializer implements JsonDeserializer {
+
+ private BukkitService bukkitService;
+
+ LimboPlayerDeserializer(BukkitService bukkitService) {
+ this.bukkitService = bukkitService;
+ }
+
+ @Override
+ public LimboPlayer deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext context) {
+ JsonObject jsonObject = jsonElement.getAsJsonObject();
+ if (jsonObject == null) {
+ return null;
+ }
+
+ Location loc = deserializeLocation(jsonObject);
+ boolean operator = getBoolean(jsonObject, IS_OP);
+ String group = getString(jsonObject, GROUP);
+ boolean canFly = getBoolean(jsonObject, CAN_FLY);
+ float walkSpeed = getFloat(jsonObject, WALK_SPEED, LimboPlayer.DEFAULT_WALK_SPEED);
+ float flySpeed = getFloat(jsonObject, FLY_SPEED, LimboPlayer.DEFAULT_FLY_SPEED);
+
+ return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
+ }
+
+ private Location deserializeLocation(JsonObject jsonObject) {
+ JsonElement e;
+ if ((e = jsonObject.getAsJsonObject(LOCATION)) != null) {
+ JsonObject locationObject = e.getAsJsonObject();
+ World world = bukkitService.getWorld(getString(locationObject, LOC_WORLD));
+ if (world != null) {
+ double x = getDouble(locationObject, LOC_X);
+ double y = getDouble(locationObject, LOC_Y);
+ double z = getDouble(locationObject, LOC_Z);
+ float yaw = getFloat(locationObject, LOC_YAW);
+ float pitch = getFloat(locationObject, LOC_PITCH);
+ return new Location(world, x, y, z, yaw, pitch);
+ }
+ }
+ return null;
+ }
+
+ private static String getString(JsonObject jsonObject, String memberName) {
+ JsonElement element = jsonObject.get(memberName);
+ return element != null ? element.getAsString() : "";
+ }
+
+ private static boolean getBoolean(JsonObject jsonObject, String memberName) {
+ JsonElement element = jsonObject.get(memberName);
+ return element != null && element.getAsBoolean();
+ }
+
+ private static float getFloat(JsonObject jsonObject, String memberName) {
+ return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, 0.0f);
+ }
+
+ private static float getFloat(JsonObject jsonObject, String memberName, float defaultValue) {
+ return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsFloat, defaultValue);
+ }
+
+ private static double getDouble(JsonObject jsonObject, String memberName) {
+ return getNumberFromElement(jsonObject.get(memberName), JsonElement::getAsDouble, 0.0);
+ }
+
+ /**
+ * Gets a number from the given JsonElement safely.
+ *
+ * @param jsonElement the element to retrieve the number from
+ * @param numberFunction the function to get the number from the element
+ * @param defaultValue the value to return if the element is null or the number cannot be retrieved
+ * @param the number type
+ * @return the number from the given JSON element, or the default value
+ */
+ private static N getNumberFromElement(JsonElement jsonElement,
+ Function numberFunction,
+ N defaultValue) {
+ if (jsonElement != null) {
+ try {
+ return numberFunction.apply(jsonElement);
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ return defaultValue;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java
new file mode 100644
index 00000000..aeae3b65
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/LimboPlayerSerializer.java
@@ -0,0 +1,52 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import org.bukkit.Location;
+
+import java.lang.reflect.Type;
+
+/**
+ * Converts a LimboPlayer to a JsonElement.
+ */
+class LimboPlayerSerializer implements JsonSerializer {
+
+ static final String LOCATION = "location";
+ static final String LOC_WORLD = "world";
+ static final String LOC_X = "x";
+ static final String LOC_Y = "y";
+ static final String LOC_Z = "z";
+ static final String LOC_YAW = "yaw";
+ static final String LOC_PITCH = "pitch";
+
+ static final String GROUP = "group";
+ static final String IS_OP = "operator";
+ static final String CAN_FLY = "can-fly";
+ static final String WALK_SPEED = "walk-speed";
+ static final String FLY_SPEED = "fly-speed";
+
+
+ @Override
+ public JsonElement serialize(LimboPlayer limboPlayer, Type type, JsonSerializationContext context) {
+ Location loc = limboPlayer.getLocation();
+ JsonObject locationObject = new JsonObject();
+ locationObject.addProperty(LOC_WORLD, loc.getWorld().getName());
+ locationObject.addProperty(LOC_X, loc.getX());
+ locationObject.addProperty(LOC_Y, loc.getY());
+ locationObject.addProperty(LOC_Z, loc.getZ());
+ locationObject.addProperty(LOC_YAW, loc.getYaw());
+ locationObject.addProperty(LOC_PITCH, loc.getPitch());
+
+ JsonObject obj = new JsonObject();
+ obj.add(LOCATION, locationObject);
+ obj.addProperty(GROUP, limboPlayer.getGroup());
+ obj.addProperty(IS_OP, limboPlayer.isOperator());
+ obj.addProperty(CAN_FLY, limboPlayer.isCanFly());
+ obj.addProperty(WALK_SPEED, limboPlayer.getWalkSpeed());
+ obj.addProperty(FLY_SPEED, limboPlayer.getFlySpeed());
+ return obj;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java
new file mode 100644
index 00000000..ac6ff9b3
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/NoOpPersistenceHandler.java
@@ -0,0 +1,30 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import org.bukkit.entity.Player;
+
+/**
+ * Limbo player persistence implementation that does nothing.
+ */
+class NoOpPersistenceHandler implements LimboPersistenceHandler {
+
+ @Override
+ public LimboPlayer getLimboPlayer(Player player) {
+ return null;
+ }
+
+ @Override
+ public void saveLimboPlayer(Player player, LimboPlayer limbo) {
+ // noop
+ }
+
+ @Override
+ public void removeLimboPlayer(Player player) {
+ // noop
+ }
+
+ @Override
+ public LimboPersistenceType getType() {
+ return LimboPersistenceType.DISABLED;
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java
new file mode 100644
index 00000000..5053ba52
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentConfiguration.java
@@ -0,0 +1,94 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+/**
+ * Configuration for the total number of segments to use.
+ *
+ * The {@link SegmentFilesPersistenceHolder} reduces the number of files by assigning each UUID
+ * to a segment. This enum allows to define how many segments the UUIDs should be distributed in.
+ *
+ * Segments are defined by a distribution and a length. The distribution defines
+ * to how many outputs a single hexadecimal characters should be mapped. So e.g. a distribution
+ * of 3 means that all hexadecimal characters 0-f should be distributed over three different
+ * outputs evenly. The {@link SegmentNameBuilder} simply uses hexadecimal characters as outputs,
+ * so e.g. with a distribution of 3 all hex characters 0-f are mapped to 0, 1, or 2.
+ *
+ * To ensure an even distribution the segments must be powers of 2. Trivially, to implement a
+ * distribution of 16, the same character may be returned as was input (since 0-f make up 16
+ * characters). A distribution of 1, on the other hand, means that the same output is returned
+ * regardless of the input character.
+ *
+ * The length parameter defines how many characters of a player's UUID should be used to
+ * create the segment ID. In other words, with a distribution of 2 and a length of 3, the first
+ * three characters of the UUID are taken into consideration, each mapped to one of two possible
+ * characters. For instance, a UUID starting with "0f5c9321" may yield the segment ID "010."
+ * Such a segment ID defines in which file the given UUID can be found and stored.
+ *
+ * The number of segments such a configuration yields is computed as {@code distribution ^ length},
+ * since distribution defines how many outputs there are per digit, and length defines the number
+ * of digits. For instance, a distribution of 2 and a length of 3 will yield segment IDs 000, 001,
+ * 010, 011, 100, 101, 110 and 111 (i.e. all binary numbers from 0 to 7).
+ *
+ * There are multiple possibilities to achieve certain segment totals, e.g. 8 different segments
+ * may be created by setting distribution to 8 and length to 1, or distr. to 2 and length to 3.
+ * Where possible, prefer a length of 1 (no string concatenation required) or a distribution of
+ * 16 (no remapping of the characters required).
+ */
+public enum SegmentConfiguration {
+
+ /** 1. */
+ ONE(1, 1),
+
+ ///** 2. */
+ //TWO(2, 1),
+
+ /** 4. */
+ FOUR(4, 1),
+
+ /** 8. */
+ EIGHT(8, 1),
+
+ /** 16. */
+ SIXTEEN(16, 1),
+
+ /** 32. */
+ THIRTY_TWO(2, 5),
+
+ /** 64. */
+ SIXTY_FOUR(4, 3),
+
+ /** 128. */
+ ONE_TWENTY(2, 7),
+
+ /** 256. */
+ TWO_FIFTY(16, 2);
+
+ private final int distribution;
+ private final int length;
+
+ SegmentConfiguration(int distribution, int length) {
+ this.distribution = distribution;
+ this.length = length;
+ }
+
+ /**
+ * @return the distribution size per character, i.e. how many possible outputs there are
+ * for any hexadecimal character
+ */
+ public int getDistribution() {
+ return distribution;
+ }
+
+ /**
+ * @return number of characters from a UUID that should be used to create a segment ID
+ */
+ public int getLength() {
+ return length;
+ }
+
+ /**
+ * @return number of segments to which this configuration will distribute UUIDs
+ */
+ public int getTotalSegments() {
+ return (int) Math.pow(distribution, length);
+ }
+}
diff --git a/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java
new file mode 100644
index 00000000..e786ca48
--- /dev/null
+++ b/src/main/java/fr/xephi/authme/data/limbo/persistence/SegmentFilesPersistenceHolder.java
@@ -0,0 +1,226 @@
+package fr.xephi.authme.data.limbo.persistence;
+
+import com.google.common.io.Files;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import fr.xephi.authme.ConsoleLogger;
+import fr.xephi.authme.data.limbo.LimboPlayer;
+import fr.xephi.authme.initialization.DataFolder;
+import fr.xephi.authme.service.BukkitService;
+import fr.xephi.authme.settings.Settings;
+import fr.xephi.authme.settings.properties.LimboSettings;
+import fr.xephi.authme.util.FileUtils;
+import fr.xephi.authme.util.PlayerUtils;
+import org.bukkit.entity.Player;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.FileWriter;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Persistence handler for LimboPlayer objects by distributing the objects to store
+ * in various segments (buckets) based on the start of the player's UUID.
+ */
+class SegmentFilesPersistenceHolder implements LimboPersistenceHandler {
+
+ private static final Type LIMBO_MAP_TYPE = new TypeToken