From 9ad40be2107c16c298f86c801c6870ae50cbcf80 Mon Sep 17 00:00:00 2001 From: Luck Date: Mon, 25 Jul 2016 11:18:35 +0100 Subject: [PATCH] Add complete offline-mode support --- README.md | 1 + .../me/lucko/luckperms/LPBukkitPlugin.java | 3 + .../luckperms/listeners/PlayerListener.java | 60 +++++++++++++---- .../luckperms/users/BukkitUserManager.java | 4 +- bukkit/src/main/resources/config.yml | 13 ++++ .../me/lucko/luckperms/LPBungeePlugin.java | 3 + .../luckperms/listeners/PlayerListener.java | 65 +++++++++++++------ .../luckperms/users/BungeeUserManager.java | 5 +- bungee/src/main/resources/config.yml | 13 ++++ .../me/lucko/luckperms/LuckPermsPlugin.java | 7 ++ .../luckperms/commands/misc/InfoCommand.java | 2 +- .../me/lucko/luckperms/constants/Message.java | 3 +- .../luckperms/utils/LPConfiguration.java | 4 ++ .../me/lucko/luckperms/utils/UuidCache.java | 38 +++++++++++ 14 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 common/src/main/java/me/lucko/luckperms/utils/UuidCache.java diff --git a/README.md b/README.md index c558ae31..23898942 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A (fairly bad) permissions implementation for Bukkit/BungeeCord. * **Temporary permissions** - users/groups can be given permissions that expire after a given time * **Temporary groups** - users/groups can be added to/inherit other groups temporarily * **Multi-server support** - data is synced across all servers/platforms +* **Full offline-mode/mixed-mode support** - player permissions are synced properly over offline-mode or mixed online/offline-mode networks. * **Per-server permissions/groups** - define user/group permissions that only apply on certain servers * **Server-specific groups** - define groups that only apply on certain servers * **Tracks / paths** - users can be promoted/demoted along multiple group tracks diff --git a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java index 6638d24b..2d1b5460 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/LPBukkitPlugin.java @@ -15,6 +15,7 @@ import me.lucko.luckperms.tracks.TrackManager; import me.lucko.luckperms.users.BukkitUserManager; import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.utils.LPConfiguration; +import me.lucko.luckperms.utils.UuidCache; import me.lucko.luckperms.vaulthooks.VaultHook; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; @@ -38,6 +39,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Datastore datastore; + private UuidCache uuidCache; @Override public void onEnable() { @@ -81,6 +83,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { datastore.init(); getLogger().info("Loading internal permission managers..."); + uuidCache = new UuidCache(getConfiguration().getOnlineMode()); userManager = new BukkitUserManager(this); groupManager = new GroupManager(this); trackManager = new TrackManager(); diff --git a/bukkit/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java b/bukkit/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java index a2c68eda..9e8dafa8 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java +++ b/bukkit/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java @@ -1,11 +1,12 @@ package me.lucko.luckperms.listeners; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import me.lucko.luckperms.LPBukkitPlugin; import me.lucko.luckperms.commands.Util; import me.lucko.luckperms.constants.Message; import me.lucko.luckperms.users.BukkitUser; import me.lucko.luckperms.users.User; +import me.lucko.luckperms.utils.UuidCache; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -14,11 +15,33 @@ import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; -@AllArgsConstructor +import java.util.UUID; + +@RequiredArgsConstructor public class PlayerListener implements Listener { private static final String KICK_MESSAGE = Util.color(Message.PREFIX + "User data could not be loaded. Please contact an administrator."); private final LPBukkitPlugin plugin; + /* + cache: username --> uuid + returns mojang if not in offline mode + + + if server in offline mode: + go to datastore, look for uuid, add to cache. + + *** player prelogin, load or create, using CACHE uuid + + + + *** player login, we get their username and check if it's there + + *** player join, save uuid data and refresh + + *** player quit, unload + + */ + @EventHandler public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) { if (!plugin.getDatastore().isAcceptingLogins()) { @@ -26,13 +49,27 @@ public class PlayerListener implements Listener { e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE); return; } - plugin.getDatastore().loadOrCreateUser(e.getUniqueId(), e.getName()); + + final UuidCache cache = plugin.getUuidCache(); + if (!cache.isOnlineMode()) { + UUID uuid = plugin.getDatastore().getUUID(e.getName()); + if (uuid != null) { + cache.addToCache(e.getName(), uuid); + } else { + cache.addToCache(e.getName(), e.getUniqueId()); + plugin.getDatastore().saveUUIDData(e.getName(), e.getUniqueId(), b -> {}); + } + } else { + plugin.getDatastore().saveUUIDData(e.getName(), e.getUniqueId(), b -> {}); + } + + plugin.getDatastore().loadOrCreateUser(cache.getUUID(e.getName(), e.getUniqueId()), e.getName()); } @EventHandler public void onPlayerLogin(PlayerLoginEvent e) { final Player player = e.getPlayer(); - final User user = plugin.getUserManager().getUser(player.getUniqueId()); + final User user = plugin.getUserManager().getUser(plugin.getUuidCache().getUUID(e.getPlayer().getName(), e.getPlayer().getUniqueId())); if (user == null) { e.disallow(PlayerLoginEvent.Result.KICK_OTHER, KICK_MESSAGE); @@ -49,23 +86,22 @@ public class PlayerListener implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent e) { - // Save UUID data for the player - plugin.getDatastore().saveUUIDData(e.getPlayer().getName(), e.getPlayer().getUniqueId(), success -> {}); - - final User user = plugin.getUserManager().getUser(e.getPlayer().getUniqueId()); + // Refresh permissions again + final User user = plugin.getUserManager().getUser(plugin.getUuidCache().getUUID(e.getPlayer().getName(), e.getPlayer().getUniqueId())); if (user != null) { - // Refresh permissions again user.refreshPermissions(); } - } @EventHandler public void onPlayerQuit(PlayerQuitEvent e) { final Player player = e.getPlayer(); + final UuidCache cache = plugin.getUuidCache(); - // Unload the user from memory when they disconnect - final User user = plugin.getUserManager().getUser(player.getUniqueId()); + // Unload the user from memory when they disconnect; + cache.clearCache(player.getName()); + + final User user = plugin.getUserManager().getUser(cache.getUUID(player.getName(), player.getUniqueId())); plugin.getUserManager().unloadUser(user); } diff --git a/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUserManager.java b/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUserManager.java index e7ad491f..2bec0238 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUserManager.java +++ b/bukkit/src/main/java/me/lucko/luckperms/users/BukkitUserManager.java @@ -57,7 +57,9 @@ public class BukkitUserManager extends UserManager { public void updateAllUsers() { // Sometimes called async, so we need to get the players on the Bukkit thread. plugin.doSync(() -> { - Set players = plugin.getServer().getOnlinePlayers().stream().map(Player::getUniqueId).collect(Collectors.toSet()); + Set players = plugin.getServer().getOnlinePlayers().stream() + .map(p -> plugin.getUuidCache().getUUID(p.getName(), p.getUniqueId())) + .collect(Collectors.toSet()); plugin.doAsync(() -> players.forEach(u -> plugin.getDatastore().loadUser(u))); }); } diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index bf9f9b8b..01ad7d85 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -9,6 +9,19 @@ default-group: default # If users on this server should have their global permissions/groups applied. include-global: true +# If this server is in offline or online mode. +# This setting allows a player to have the same UUID across a network of offline mode/mixed servers. + +# You should generally reflect the setting in server.properties here. Except when... + +# 1. You have Spigot servers connected to a BungeeCord proxy, with online-mode set to false, but 'bungeecord' set to true in the spigot.yml +# AND 'ip-forward' set to true in the BungeeCord config.yml +# In this case, set online-mode in LuckPerms to true, dispite the server being in offline mode. + +# 2. You are only running one server instance using LuckPerms, (not a network) +# In this case, set online-mode to true no matter what is set in server.properties. (we can just fallback to the servers uuid cache) +online-mode: true + # Which storage method the plugin should use. # Currently supported: mysql, sqlite, flatfile # Fill out connection info below if you're using MySQL diff --git a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java index 6ad6a204..c631c35b 100644 --- a/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/LPBungeePlugin.java @@ -14,6 +14,7 @@ import me.lucko.luckperms.tracks.TrackManager; import me.lucko.luckperms.users.BungeeUserManager; import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.utils.LPConfiguration; +import me.lucko.luckperms.utils.UuidCache; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Plugin; @@ -31,6 +32,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { private GroupManager groupManager; private TrackManager trackManager; private Datastore datastore; + private UuidCache uuidCache; @Override public void onEnable() { @@ -69,6 +71,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { datastore.init(); getLogger().info("Loading internal permission managers..."); + uuidCache = new UuidCache(getConfiguration().getOnlineMode()); userManager = new BungeeUserManager(this); groupManager = new GroupManager(this); trackManager = new TrackManager(); diff --git a/bungee/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java b/bungee/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java index 49e07cd7..ec822d55 100644 --- a/bungee/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java +++ b/bungee/src/main/java/me/lucko/luckperms/listeners/PlayerListener.java @@ -5,14 +5,18 @@ import me.lucko.luckperms.LPBungeePlugin; import me.lucko.luckperms.commands.Util; import me.lucko.luckperms.constants.Message; import me.lucko.luckperms.users.User; +import me.lucko.luckperms.utils.UuidCache; import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.PendingConnection; import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.LoginEvent; import net.md_5.bungee.api.event.PlayerDisconnectEvent; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import java.lang.ref.WeakReference; +import java.util.UUID; import java.util.concurrent.TimeUnit; @AllArgsConstructor @@ -22,39 +26,62 @@ public class PlayerListener implements Listener { ); private final LPBungeePlugin plugin; + @EventHandler + public void onPlayerLogin(LoginEvent e) { + /* Delay the login here, as we want to cache UUID data before the player is connected to a backend bukkit server. + This means that a player will have the same UUID across the network, even if parts of the network are running in + Offline mode. */ + e.registerIntent(plugin); + plugin.doAsync(() -> { + final UuidCache cache = plugin.getUuidCache(); + final PendingConnection c = e.getConnection(); + + if (!cache.isOnlineMode()) { + UUID uuid = plugin.getDatastore().getUUID(c.getName()); + if (uuid != null) { + cache.addToCache(c.getName(), uuid); + } else { + cache.addToCache(c.getName(), c.getUniqueId()); + plugin.getDatastore().saveUUIDData(c.getName(), c.getUniqueId()); + } + } else { + plugin.getDatastore().saveUUIDData(c.getName(), c.getUniqueId()); + } + + // We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues as the Bukkit server + // and the BungeeCord server try to make a new user at the same time. + plugin.getDatastore().loadOrCreateUser(cache.getUUID(c.getName(), c.getUniqueId()), c.getName()); + e.completeIntent(plugin); + }); + } + @EventHandler public void onPlayerPostLogin(PostLoginEvent e) { final ProxiedPlayer player = e.getPlayer(); final WeakReference p = new WeakReference<>(player); - // Create user async and at post login. We're not concerned if data couldn't be loaded, the player won't be kicked. - plugin.getDatastore().loadOrCreateUser(player.getUniqueId(), player.getName(), success -> { - if (!success) { - plugin.getProxy().getScheduler().schedule(plugin, () -> { - final ProxiedPlayer pl = p.get(); - if (pl != null) { - pl.sendMessage(WARN_MESSAGE); - } - }, 3, TimeUnit.SECONDS); - - } else { + final User user = plugin.getUserManager().getUser(plugin.getUuidCache().getUUID(e.getPlayer().getName(), e.getPlayer().getUniqueId())); + if (user == null) { + plugin.getProxy().getScheduler().schedule(plugin, () -> { final ProxiedPlayer pl = p.get(); if (pl != null) { - final User user = plugin.getUserManager().getUser(pl.getUniqueId()); - user.refreshPermissions(); + pl.sendMessage(WARN_MESSAGE); } - } - }); - - plugin.getDatastore().saveUUIDData(player.getName(), player.getUniqueId(), success -> {}); + }, 3, TimeUnit.SECONDS); + } else { + user.refreshPermissions(); + } } @EventHandler public void onPlayerQuit(PlayerDisconnectEvent e) { final ProxiedPlayer player = e.getPlayer(); + final UuidCache cache = plugin.getUuidCache(); - // Unload the user from memory when they disconnect - final User user = plugin.getUserManager().getUser(player.getUniqueId()); + // Unload the user from memory when they disconnect; + cache.clearCache(player.getName()); + + final User user = plugin.getUserManager().getUser(cache.getUUID(player.getName(), player.getUniqueId())); plugin.getUserManager().unloadUser(user); } } diff --git a/bungee/src/main/java/me/lucko/luckperms/users/BungeeUserManager.java b/bungee/src/main/java/me/lucko/luckperms/users/BungeeUserManager.java index 94226c04..517293d5 100644 --- a/bungee/src/main/java/me/lucko/luckperms/users/BungeeUserManager.java +++ b/bungee/src/main/java/me/lucko/luckperms/users/BungeeUserManager.java @@ -1,7 +1,6 @@ package me.lucko.luckperms.users; import me.lucko.luckperms.LPBungeePlugin; -import net.md_5.bungee.api.connection.ProxiedPlayer; import java.util.UUID; @@ -41,6 +40,8 @@ public class BungeeUserManager extends UserManager { @Override public void updateAllUsers() { - plugin.getProxy().getPlayers().stream().map(ProxiedPlayer::getUniqueId).forEach(u -> plugin.getDatastore().loadUser(u)); + plugin.getProxy().getPlayers().stream() + .map(p -> plugin.getUuidCache().getUUID(p.getName(), p.getUniqueId())) + .forEach(u -> plugin.getDatastore().loadUser(u)); } } diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 62a86b33..fdb4d060 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -9,6 +9,19 @@ default-group: default # If users on this server should have their global permissions/groups applied. include-global: false +# If this server is in offline or online mode. +# This setting allows a player to have the same UUID across a network of offline mode/mixed servers. + +# You should generally reflect the setting in server.properties here. Except when... + +# 1. You have Spigot servers connected to a BungeeCord proxy, with online-mode set to false, but 'bungeecord' set to true in the spigot.yml +# AND 'ip-forward' set to true in the BungeeCord config.yml +# In this case, set online-mode in LuckPerms to true, dispite the server being in offline mode. + +# 2. You are only running one server instance using LuckPerms, (not a network) +# In this case, set online-mode to true no matter what is set in server.properties. (we can just fallback to the servers uuid cache) +online-mode: true + # Which storage method the plugin should use. # Currently supported: mysql & flatfile # Fill out connection info below if you're using MySQL diff --git a/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java index aaf5e3ae..95148f5b 100644 --- a/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/LuckPermsPlugin.java @@ -5,6 +5,7 @@ import me.lucko.luckperms.groups.GroupManager; import me.lucko.luckperms.tracks.TrackManager; import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.utils.LPConfiguration; +import me.lucko.luckperms.utils.UuidCache; import java.util.List; import java.util.UUID; @@ -48,6 +49,12 @@ public interface LuckPermsPlugin { */ Logger getLogger(); + /** + * Retrieves the {@link UuidCache} for the plugin + * @return the plugin's {@link UuidCache} + */ + UuidCache getUuidCache(); + /** * @return the version of the plugin */ diff --git a/common/src/main/java/me/lucko/luckperms/commands/misc/InfoCommand.java b/common/src/main/java/me/lucko/luckperms/commands/misc/InfoCommand.java index 7f36b2db..0ce3b722 100644 --- a/common/src/main/java/me/lucko/luckperms/commands/misc/InfoCommand.java +++ b/common/src/main/java/me/lucko/luckperms/commands/misc/InfoCommand.java @@ -20,7 +20,7 @@ public class InfoCommand extends MainCommand { protected void execute(LuckPermsPlugin plugin, Sender sender, List args, String label) { final LPConfiguration c = plugin.getConfiguration(); Message.INFO.send(sender, plugin.getVersion(), plugin.getDatastore().getName(), c.getServer(), - c.getDefaultGroupName(), c.getSyncTime(), c.getIncludeGlobalPerms()); + c.getDefaultGroupName(), c.getSyncTime(), c.getIncludeGlobalPerms(), c.getOnlineMode()); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/constants/Message.java b/common/src/main/java/me/lucko/luckperms/constants/Message.java index ed88aacc..357cc772 100644 --- a/common/src/main/java/me/lucko/luckperms/constants/Message.java +++ b/common/src/main/java/me/lucko/luckperms/constants/Message.java @@ -99,7 +99,8 @@ public enum Message { PREFIX + "&eServer Name: &6%s" + "\n" + PREFIX + "&eDefault Group: &6%s" + "\n" + PREFIX + "&eSync Interval: &6%s minutes" + "\n" + - PREFIX + "&eInclude Global Perms: &6%s", + PREFIX + "&eInclude Global Perms: &6%s" + "\n" + + PREFIX + "&eOnline Mode: &6%s", false ), DEBUG( diff --git a/common/src/main/java/me/lucko/luckperms/utils/LPConfiguration.java b/common/src/main/java/me/lucko/luckperms/utils/LPConfiguration.java index a649a78f..e6808e0a 100644 --- a/common/src/main/java/me/lucko/luckperms/utils/LPConfiguration.java +++ b/common/src/main/java/me/lucko/luckperms/utils/LPConfiguration.java @@ -61,6 +61,10 @@ public abstract class LPConfiguration { return getBoolean("include-global", defaultIncludeGlobal); } + public boolean getOnlineMode() { + return getBoolean("online-mode", true); + } + public String getDatabaseValue(String value) { return getString("sql." + value, null); } diff --git a/common/src/main/java/me/lucko/luckperms/utils/UuidCache.java b/common/src/main/java/me/lucko/luckperms/utils/UuidCache.java new file mode 100644 index 00000000..3de1cad8 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/utils/UuidCache.java @@ -0,0 +1,38 @@ +package me.lucko.luckperms.utils; + +import lombok.Getter; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class UuidCache { + + private Map cache; + + @Getter + private final boolean onlineMode; + + public UuidCache(boolean onlineMode) { + this.onlineMode = onlineMode; + + if (!onlineMode) { + cache = new ConcurrentHashMap<>(); + } + } + + public UUID getUUID(String name, UUID fallback) { + return onlineMode ? fallback : (cache.containsKey(name) ? cache.get(name) : fallback); + } + + public void addToCache(String name, UUID uuid) { + if (onlineMode) return; + cache.put(name, uuid); + } + + public void clearCache(String name) { + if (onlineMode) return; + cache.remove(name); + } + +}