Add complete offline-mode support

This commit is contained in:
Luck 2016-07-25 11:18:35 +01:00
parent df209f6515
commit 9ad40be210
14 changed files with 185 additions and 36 deletions

View File

@ -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 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 * **Temporary groups** - users/groups can be added to/inherit other groups temporarily
* **Multi-server support** - data is synced across all servers/platforms * **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 * **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 * **Server-specific groups** - define groups that only apply on certain servers
* **Tracks / paths** - users can be promoted/demoted along multiple group tracks * **Tracks / paths** - users can be promoted/demoted along multiple group tracks

View File

@ -15,6 +15,7 @@ import me.lucko.luckperms.tracks.TrackManager;
import me.lucko.luckperms.users.BukkitUserManager; import me.lucko.luckperms.users.BukkitUserManager;
import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.users.UserManager;
import me.lucko.luckperms.utils.LPConfiguration; import me.lucko.luckperms.utils.LPConfiguration;
import me.lucko.luckperms.utils.UuidCache;
import me.lucko.luckperms.vaulthooks.VaultHook; import me.lucko.luckperms.vaulthooks.VaultHook;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
@ -38,6 +39,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
private GroupManager groupManager; private GroupManager groupManager;
private TrackManager trackManager; private TrackManager trackManager;
private Datastore datastore; private Datastore datastore;
private UuidCache uuidCache;
@Override @Override
public void onEnable() { public void onEnable() {
@ -81,6 +83,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
datastore.init(); datastore.init();
getLogger().info("Loading internal permission managers..."); getLogger().info("Loading internal permission managers...");
uuidCache = new UuidCache(getConfiguration().getOnlineMode());
userManager = new BukkitUserManager(this); userManager = new BukkitUserManager(this);
groupManager = new GroupManager(this); groupManager = new GroupManager(this);
trackManager = new TrackManager(); trackManager = new TrackManager();

View File

@ -1,11 +1,12 @@
package me.lucko.luckperms.listeners; package me.lucko.luckperms.listeners;
import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor;
import me.lucko.luckperms.LPBukkitPlugin; import me.lucko.luckperms.LPBukkitPlugin;
import me.lucko.luckperms.commands.Util; import me.lucko.luckperms.commands.Util;
import me.lucko.luckperms.constants.Message; import me.lucko.luckperms.constants.Message;
import me.lucko.luckperms.users.BukkitUser; import me.lucko.luckperms.users.BukkitUser;
import me.lucko.luckperms.users.User; import me.lucko.luckperms.users.User;
import me.lucko.luckperms.utils.UuidCache;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; 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.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
@AllArgsConstructor import java.util.UUID;
@RequiredArgsConstructor
public class PlayerListener implements Listener { 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 static final String KICK_MESSAGE = Util.color(Message.PREFIX + "User data could not be loaded. Please contact an administrator.");
private final LPBukkitPlugin plugin; 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 @EventHandler
public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) { public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) {
if (!plugin.getDatastore().isAcceptingLogins()) { if (!plugin.getDatastore().isAcceptingLogins()) {
@ -26,13 +49,27 @@ public class PlayerListener implements Listener {
e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE); e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, KICK_MESSAGE);
return; 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 @EventHandler
public void onPlayerLogin(PlayerLoginEvent e) { public void onPlayerLogin(PlayerLoginEvent e) {
final Player player = e.getPlayer(); 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) { if (user == null) {
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, KICK_MESSAGE); e.disallow(PlayerLoginEvent.Result.KICK_OTHER, KICK_MESSAGE);
@ -49,23 +86,22 @@ public class PlayerListener implements Listener {
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent e) { 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());
if (user != null) {
// Refresh permissions again // Refresh permissions again
final User user = plugin.getUserManager().getUser(plugin.getUuidCache().getUUID(e.getPlayer().getName(), e.getPlayer().getUniqueId()));
if (user != null) {
user.refreshPermissions(); user.refreshPermissions();
} }
} }
@EventHandler @EventHandler
public void onPlayerQuit(PlayerQuitEvent e) { public void onPlayerQuit(PlayerQuitEvent e) {
final Player player = e.getPlayer(); final Player player = e.getPlayer();
final UuidCache cache = plugin.getUuidCache();
// Unload the user from memory when they disconnect // Unload the user from memory when they disconnect;
final User user = plugin.getUserManager().getUser(player.getUniqueId()); cache.clearCache(player.getName());
final User user = plugin.getUserManager().getUser(cache.getUUID(player.getName(), player.getUniqueId()));
plugin.getUserManager().unloadUser(user); plugin.getUserManager().unloadUser(user);
} }

View File

@ -57,7 +57,9 @@ public class BukkitUserManager extends UserManager {
public void updateAllUsers() { public void updateAllUsers() {
// Sometimes called async, so we need to get the players on the Bukkit thread. // Sometimes called async, so we need to get the players on the Bukkit thread.
plugin.doSync(() -> { plugin.doSync(() -> {
Set<UUID> players = plugin.getServer().getOnlinePlayers().stream().map(Player::getUniqueId).collect(Collectors.toSet()); Set<UUID> 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))); plugin.doAsync(() -> players.forEach(u -> plugin.getDatastore().loadUser(u)));
}); });
} }

View File

@ -9,6 +9,19 @@ default-group: default
# If users on this server should have their global permissions/groups applied. # If users on this server should have their global permissions/groups applied.
include-global: true 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. # Which storage method the plugin should use.
# Currently supported: mysql, sqlite, flatfile # Currently supported: mysql, sqlite, flatfile
# Fill out connection info below if you're using MySQL # Fill out connection info below if you're using MySQL

View File

@ -14,6 +14,7 @@ import me.lucko.luckperms.tracks.TrackManager;
import me.lucko.luckperms.users.BungeeUserManager; import me.lucko.luckperms.users.BungeeUserManager;
import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.users.UserManager;
import me.lucko.luckperms.utils.LPConfiguration; 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.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
@ -31,6 +32,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
private GroupManager groupManager; private GroupManager groupManager;
private TrackManager trackManager; private TrackManager trackManager;
private Datastore datastore; private Datastore datastore;
private UuidCache uuidCache;
@Override @Override
public void onEnable() { public void onEnable() {
@ -69,6 +71,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
datastore.init(); datastore.init();
getLogger().info("Loading internal permission managers..."); getLogger().info("Loading internal permission managers...");
uuidCache = new UuidCache(getConfiguration().getOnlineMode());
userManager = new BungeeUserManager(this); userManager = new BungeeUserManager(this);
groupManager = new GroupManager(this); groupManager = new GroupManager(this);
trackManager = new TrackManager(); trackManager = new TrackManager();

View File

@ -5,14 +5,18 @@ import me.lucko.luckperms.LPBungeePlugin;
import me.lucko.luckperms.commands.Util; import me.lucko.luckperms.commands.Util;
import me.lucko.luckperms.constants.Message; import me.lucko.luckperms.constants.Message;
import me.lucko.luckperms.users.User; 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.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer; 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.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventHandler;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@AllArgsConstructor @AllArgsConstructor
@ -22,39 +26,62 @@ public class PlayerListener implements Listener {
); );
private final LPBungeePlugin plugin; 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 @EventHandler
public void onPlayerPostLogin(PostLoginEvent e) { public void onPlayerPostLogin(PostLoginEvent e) {
final ProxiedPlayer player = e.getPlayer(); final ProxiedPlayer player = e.getPlayer();
final WeakReference<ProxiedPlayer> p = new WeakReference<>(player); final WeakReference<ProxiedPlayer> 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. final User user = plugin.getUserManager().getUser(plugin.getUuidCache().getUUID(e.getPlayer().getName(), e.getPlayer().getUniqueId()));
plugin.getDatastore().loadOrCreateUser(player.getUniqueId(), player.getName(), success -> { if (user == null) {
if (!success) {
plugin.getProxy().getScheduler().schedule(plugin, () -> { plugin.getProxy().getScheduler().schedule(plugin, () -> {
final ProxiedPlayer pl = p.get(); final ProxiedPlayer pl = p.get();
if (pl != null) { if (pl != null) {
pl.sendMessage(WARN_MESSAGE); pl.sendMessage(WARN_MESSAGE);
} }
}, 3, TimeUnit.SECONDS); }, 3, TimeUnit.SECONDS);
} else { } else {
final ProxiedPlayer pl = p.get();
if (pl != null) {
final User user = plugin.getUserManager().getUser(pl.getUniqueId());
user.refreshPermissions(); user.refreshPermissions();
} }
} }
});
plugin.getDatastore().saveUUIDData(player.getName(), player.getUniqueId(), success -> {});
}
@EventHandler @EventHandler
public void onPlayerQuit(PlayerDisconnectEvent e) { public void onPlayerQuit(PlayerDisconnectEvent e) {
final ProxiedPlayer player = e.getPlayer(); final ProxiedPlayer player = e.getPlayer();
final UuidCache cache = plugin.getUuidCache();
// Unload the user from memory when they disconnect // Unload the user from memory when they disconnect;
final User user = plugin.getUserManager().getUser(player.getUniqueId()); cache.clearCache(player.getName());
final User user = plugin.getUserManager().getUser(cache.getUUID(player.getName(), player.getUniqueId()));
plugin.getUserManager().unloadUser(user); plugin.getUserManager().unloadUser(user);
} }
} }

View File

@ -1,7 +1,6 @@
package me.lucko.luckperms.users; package me.lucko.luckperms.users;
import me.lucko.luckperms.LPBungeePlugin; import me.lucko.luckperms.LPBungeePlugin;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.UUID; import java.util.UUID;
@ -41,6 +40,8 @@ public class BungeeUserManager extends UserManager {
@Override @Override
public void updateAllUsers() { 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));
} }
} }

View File

@ -9,6 +9,19 @@ default-group: default
# If users on this server should have their global permissions/groups applied. # If users on this server should have their global permissions/groups applied.
include-global: false 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. # Which storage method the plugin should use.
# Currently supported: mysql & flatfile # Currently supported: mysql & flatfile
# Fill out connection info below if you're using MySQL # Fill out connection info below if you're using MySQL

View File

@ -5,6 +5,7 @@ import me.lucko.luckperms.groups.GroupManager;
import me.lucko.luckperms.tracks.TrackManager; import me.lucko.luckperms.tracks.TrackManager;
import me.lucko.luckperms.users.UserManager; import me.lucko.luckperms.users.UserManager;
import me.lucko.luckperms.utils.LPConfiguration; import me.lucko.luckperms.utils.LPConfiguration;
import me.lucko.luckperms.utils.UuidCache;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -48,6 +49,12 @@ public interface LuckPermsPlugin {
*/ */
Logger getLogger(); Logger getLogger();
/**
* Retrieves the {@link UuidCache} for the plugin
* @return the plugin's {@link UuidCache}
*/
UuidCache getUuidCache();
/** /**
* @return the version of the plugin * @return the version of the plugin
*/ */

View File

@ -20,7 +20,7 @@ public class InfoCommand extends MainCommand {
protected void execute(LuckPermsPlugin plugin, Sender sender, List<String> args, String label) { protected void execute(LuckPermsPlugin plugin, Sender sender, List<String> args, String label) {
final LPConfiguration c = plugin.getConfiguration(); final LPConfiguration c = plugin.getConfiguration();
Message.INFO.send(sender, plugin.getVersion(), plugin.getDatastore().getName(), c.getServer(), 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 @Override

View File

@ -99,7 +99,8 @@ public enum Message {
PREFIX + "&eServer Name: &6%s" + "\n" + PREFIX + "&eServer Name: &6%s" + "\n" +
PREFIX + "&eDefault Group: &6%s" + "\n" + PREFIX + "&eDefault Group: &6%s" + "\n" +
PREFIX + "&eSync Interval: &6%s minutes" + "\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 false
), ),
DEBUG( DEBUG(

View File

@ -61,6 +61,10 @@ public abstract class LPConfiguration<T extends LuckPermsPlugin> {
return getBoolean("include-global", defaultIncludeGlobal); return getBoolean("include-global", defaultIncludeGlobal);
} }
public boolean getOnlineMode() {
return getBoolean("online-mode", true);
}
public String getDatabaseValue(String value) { public String getDatabaseValue(String value) {
return getString("sql." + value, null); return getString("sql." + value, null);
} }

View File

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