From bdda99bc812c881c993dad39de32cac0ebf4b85b Mon Sep 17 00:00:00 2001 From: mohammed jasem alaajel Date: Thu, 7 Jul 2022 02:24:08 +0400 Subject: [PATCH] added Velocity support (NOT TESTED), it will not compile yet due config impl not done yet --- .../RedisBungee-Bungee/pom.xml | 125 ++++ .../redisbungee/BungeeDataManager.java | 35 + .../minecraft/redisbungee/RBUtils.java | 39 + .../redisbungee/RedisBungeeBungeePlugin.java | 639 +++++++++++++++++ .../redisbungee/RedisBungeeCommandSender.java | 74 ++ .../redisbungee/RedisBungeeListener.java | 261 +++++++ .../commands/RedisBungeeCommands.java | 343 +++++++++ .../PlayerChangedServerNetworkEvent.java | 38 + .../events/PlayerJoinedNetworkEvent.java | 26 + .../events/PlayerLeftNetworkEvent.java | 26 + .../events/PubSubMessageEvent.java | 29 + .../src/main/resources/plugin.yml | 9 + RedisBungee-Velocity/pom.xml | 125 ++++ .../minecraft/redisbungee/RBUtils.java | 38 + .../redisbungee/RedisBungeeCommandSource.java | 38 + .../redisbungee/RedisBungeeListener.java | 161 +++++ .../RedisBungeeVelocityPlugin.java | 667 ++++++++++++++++++ .../redisbungee/VelocityDataManager.java | 35 + .../commands/RedisBungeeCommands.java | 327 +++++++++ .../PlayerChangedServerNetworkEvent.java | 37 + .../events/PlayerJoinedNetworkEvent.java | 25 + .../events/PlayerLeftNetworkEvent.java | 25 + .../events/PubSubMessageEvent.java | 29 + pom.xml | 1 + 24 files changed, 3152 insertions(+) create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/pom.xml create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeBungeePlugin.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java create mode 100644 RedisBungee-Velocity/RedisBungee-Bungee/src/main/resources/plugin.yml create mode 100644 RedisBungee-Velocity/pom.xml create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSource.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/pom.xml b/RedisBungee-Velocity/RedisBungee-Bungee/pom.xml new file mode 100644 index 0000000..d927349 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/pom.xml @@ -0,0 +1,125 @@ + + + + RedisBungee + com.imaginarycode.minecraft + 0.8.0-SNAPSHOT + + 4.0.0 + + RedisBungee-Bungee + + + 8 + 8 + + + + bungeecord-repo + https://oss.sonatype.org/content/repositories/snapshots + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.2 + + 8 + ../javadoc + ${project.name} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + shade + + + + redis.clients.jedis + com.imaginarycode.minecraft.redisbungee.internal.jedis + + + + redis.clients.util + com.imaginarycode.minecraft.redisbungee.internal.jedisutil + + + + org.apache.commons.pool + com.imaginarycode.minecraft.redisbungee.internal.commonspool + + + + com.squareup.okhttp + com.imaginarycode.minecraft.redisbungee.internal.okhttp + + + + okio + com.imaginarycode.minecraft.redisbungee.internal.okio + + + + com.google + com.imaginarycode.minecraft.redisbungee.internal.google + + + + org.json + com.imaginarycode.minecraft.redisbungee.internal.json + + + + org.checkerframework + com.imaginarycode.minecraft.redisbungee.internal.checkframework + + + + + + + + + + + + + com.imaginarycode.minecraft + RedisBungee-API + ${parent.version} + + + net.md-5 + bungeecord-api + 1.17-R0.1-SNAPSHOT + jar + provided + + + + \ No newline at end of file diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java new file mode 100644 index 0000000..d5f9fea --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java @@ -0,0 +1,35 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin; +import net.md_5.bungee.api.connection.ProxiedPlayer; +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; + +public class BungeeDataManager extends DataManager implements Listener { + + public BungeeDataManager(RedisBungeePlugin plugin) { + super(plugin); + } + + @Override + @EventHandler + public void onPostLogin(PostLoginEvent event) { + invalidate(event.getPlayer().getUniqueId()); + } + + @Override + @EventHandler + public void onPlayerDisconnect(PlayerDisconnectEvent event) { + invalidate(event.getPlayer().getUniqueId()); + } + + @Override + @EventHandler + public void onPubSubMessage(PubSubMessageEvent event) { + handlePubSubMessage(event.getChannel(), event.getMessage()); + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java new file mode 100644 index 0000000..580944f --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java @@ -0,0 +1,39 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.google.gson.Gson; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import redis.clients.jedis.Pipeline; + +import java.util.HashMap; +import java.util.Map; + +public class RBUtils { + + private static final Gson gson = new Gson(); + + protected static void createPlayer(ProxiedPlayer player, Pipeline pipeline, boolean fireEvent) { + createPlayer(player.getPendingConnection(), pipeline, fireEvent); + if (player.getServer() != null) + pipeline.hset("player:" + player.getUniqueId().toString(), "server", player.getServer().getInfo().getName()); + } + + protected static void createPlayer(PendingConnection connection, Pipeline pipeline, boolean fireEvent) { + Map playerData = new HashMap<>(4); + playerData.put("online", "0"); + playerData.put("ip", connection.getAddress().getAddress().getHostAddress()); + playerData.put("proxy", RedisBungeeAPI.getRedisBungeeApi().getServerId()); + + pipeline.sadd("proxy:" + RedisBungeeAPI.getRedisBungeeApi().getServerId() + ":usersOnline", connection.getUniqueId().toString()); + pipeline.hmset("player:" + connection.getUniqueId().toString(), playerData); + + if (fireEvent) { + pipeline.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage<>( + connection.getUniqueId(), RedisBungeeAPI.getRedisBungeeApi().getServerId(), DataManager.DataManagerMessage.Action.JOIN, + new DataManager.LoginPayload(connection.getAddress().getAddress())))); + } + } + + +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeBungeePlugin.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeBungeePlugin.java new file mode 100644 index 0000000..c1a580b --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeBungeePlugin.java @@ -0,0 +1,639 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import com.imaginarycode.minecraft.redisbungee.commands.RedisBungeeCommands; +import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PlayerJoinedNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.*; +import com.imaginarycode.minecraft.redisbungee.internal.summoners.JedisSummoner; +import com.imaginarycode.minecraft.redisbungee.internal.summoners.SinglePoolJedisSummoner; +import com.imaginarycode.minecraft.redisbungee.internal.util.IOUtil; +import com.imaginarycode.minecraft.redisbungee.internal.util.LuaManager; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.NameFetcher; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.UUIDFetcher; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.UUIDTranslator; +import com.squareup.okhttp.Dispatcher; +import com.squareup.okhttp.OkHttpClient; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Event; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.io.*; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +import static com.google.common.base.Preconditions.checkArgument; + +public class RedisBungeeBungeePlugin extends Plugin implements RedisBungeePlugin { + + private static final Gson gson = new Gson(); + private RedisBungeeAPI api; + private PubSubListener psl = null; + private JedisSummoner jedisSummoner; + private UUIDTranslator uuidTranslator; + private RedisBungeeConfiguration configuration; + private BungeeDataManager dataManager; + private OkHttpClient httpClient; + private volatile List serverIds; + private final AtomicInteger nagAboutServers = new AtomicInteger(); + private final AtomicInteger globalPlayerCount = new AtomicInteger(); + private Future integrityCheck; + private Future heartbeatTask; + private LuaManager.Script serverToPlayersScript; + private LuaManager.Script getPlayerCountScript; + + private static final Object SERVER_TO_PLAYERS_KEY = new Object(); + private final Cache> serverToPlayersCache = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.SECONDS) + .build(); + + + @Override + public RedisBungeeConfiguration getConfiguration() { + return this.configuration; + } + + @Override + public int getCount() { + return this.globalPlayerCount.get(); + } + + @Override + public int getCurrentCount() { + Long count = (Long) getPlayerCountScript.eval(ImmutableList.of(), ImmutableList.of()); + return count.intValue(); + } + + @Override + public Set getLocalPlayersAsUuidStrings() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (ProxiedPlayer player : getProxy().getPlayers()) { + builder.add(player.getUniqueId().toString()); + } + return builder.build(); + } + + @Override + public DataManager getDataManager() { + return this.dataManager; + } + + @Override + public Set getPlayers() { + ImmutableSet.Builder setBuilder = ImmutableSet.builder(); + if (isJedisAvailable()) { + try (Jedis rsc = requestJedis()) { + List keys = new ArrayList<>(); + for (String i : getServerIds()) { + keys.add("proxy:" + i + ":usersOnline"); + } + if (!keys.isEmpty()) { + Set users = rsc.sunion(keys.toArray(new String[keys.size()])); + if (users != null && !users.isEmpty()) { + for (String user : users) { + try { + setBuilder = setBuilder.add(UUID.fromString(user)); + } catch (IllegalArgumentException ignored) { + } + } + } + } + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); + throw new RuntimeException("Unable to get all players online", e); + } + } + return setBuilder.build(); + } + + @Override + public Jedis requestJedis() { + return this.jedisSummoner.requestJedis(); + } + + @Override + public boolean isJedisAvailable() { + return this.jedisSummoner.isJedisAvailable(); + } + + @Override + public RedisBungeeAPI getApi() { + return this.api; + } + + @Override + public UUIDTranslator getUuidTranslator() { + return this.uuidTranslator; + } + + @Override + public Multimap serversToPlayers() { + try { + return serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, new Callable>() { + @Override + public Multimap call() throws Exception { + Collection data = (Collection) serverToPlayersScript.eval(ImmutableList.of(), getServerIds()); + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + String key = null; + for (String s : data) { + if (key == null) { + key = s; + continue; + } + + builder.put(key, UUID.fromString(s)); + key = null; + } + + return builder.build(); + } + }); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public Set getPlayersOnProxy(String proxyId) { + checkArgument(getServerIds().contains(proxyId), proxyId + " is not a valid proxy ID"); + try (Jedis jedis = requestJedis()) { + Set users = jedis.smembers("proxy:" + proxyId + ":usersOnline"); + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (String user : users) { + builder.add(UUID.fromString(user)); + } + return builder.build(); + } + } + + @Override + public void sendProxyCommand(String serverId, String command) { + checkArgument(getServerIds().contains(serverId) || serverId.equals("allservers"), "proxyId is invalid"); + sendChannelMessage("redisbungee-" + serverId, command); + } + + @Override + public List getServerIds() { + return serverIds; + } + + @Override + public List getCurrentServerIds(boolean nag, boolean lagged) { + try (Jedis jedis = requestJedis()) { + long time = getRedisTime(jedis.time()); + int nagTime = 0; + if (nag) { + nagTime = nagAboutServers.decrementAndGet(); + if (nagTime <= 0) { + nagAboutServers.set(10); + } + } + ImmutableList.Builder servers = ImmutableList.builder(); + Map heartbeats = jedis.hgetAll("heartbeats"); + for (Map.Entry entry : heartbeats.entrySet()) { + try { + long stamp = Long.parseLong(entry.getValue()); + if (lagged ? time >= stamp + 30 : time <= stamp + 30) + servers.add(entry.getKey()); + else if (nag && nagTime <= 0) { + getLogger().warning(entry.getKey() + " is " + (time - stamp) + " seconds behind! (Time not synchronized or server down?) and was removed from heartbeat."); + jedis.hdel("heartbeats", entry.getKey()); + } + } catch (NumberFormatException ignored) { + } + } + return servers.build(); + } catch (JedisConnectionException e) { + getLogger().log(Level.SEVERE, "Unable to fetch server IDs", e); + return Collections.singletonList(configuration.getServerId()); + } + } + + @Override + public PubSubListener getPubSubListener() { + return this.psl; + } + + @Override + public void sendChannelMessage(String channel, String message) { + try (Jedis jedis = requestJedis()) { + jedis.publish(channel, message); + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().log(Level.SEVERE, "Unable to get connection from pool - did your Redis server go away?", e); + throw new RuntimeException("Unable to publish channel message", e); + } + } + + @Override + public void executeAsync(Runnable runnable) { + this.getProxy().getScheduler().runAsync(this, runnable); + } + + @Override + public void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int time) { + this.getProxy().getScheduler().schedule(this, runnable, time, timeUnit); + } + + @Override + public void callEvent(Object event) { + this.getProxy().getPluginManager().callEvent((Event) event); + } + + @Override + public boolean isOnlineMode() { + return this.getProxy().getConfig().isOnlineMode(); + } + + @Override + public void logInfo(String msg) { + this.getLogger().info(msg); + } + + @Override + public void logWarn(String msg) { + this.getLogger().warning(msg); + } + + @Override + public void logFatal(String msg) { + this.getLogger().severe(msg); + } + + @Override + public ProxiedPlayer getPlayer(UUID uuid) { + return this.getProxy().getPlayer(uuid); + } + + @Override + public ProxiedPlayer getPlayer(String name) { + return this.getProxy().getPlayer(name); + } + + @Override + public UUID getPlayerUUID(String player) { + return this.getProxy().getPlayer(player).getUniqueId(); + } + + @Override + public String getPlayerName(UUID player) { + return this.getProxy().getPlayer(player).getName(); + } + + @Override + public String getPlayerServerName(ProxiedPlayer player) { + return player.getServer().getInfo().getName(); + } + + @Override + public boolean isPlayerOnAServer(ProxiedPlayer player) { + return player.getServer() != null; + } + + @Override + public InetAddress getPlayerIp(ProxiedPlayer player) { + return player.getAddress().getAddress(); + } + + @Override + public void sendProxyCommand(String cmd) { + checkArgument(getServerIds().contains(this.configuration.getServerId()) || this.configuration.getServerId().equals("allservers"), "proxyId is invalid"); + sendChannelMessage("redisbungee-" + this.configuration.getServerId(), cmd); + } + + @Override + public long getRedisTime(List timeRes) { + return Long.parseLong(timeRes.get(0)); + } + + + @Override + public void start() { + ThreadFactory factory = ((ThreadPoolExecutor) getExecutorService()).getThreadFactory(); + ScheduledExecutorService service = Executors.newScheduledThreadPool(24, factory); + try { + Field field = Plugin.class.getDeclaredField("service"); + field.setAccessible(true); + ExecutorService builtinService = (ExecutorService) field.get(this); + field.set(this, service); + builtinService.shutdownNow(); + } catch (IllegalAccessException | NoSuchFieldException e) { + getLogger().log(Level.WARNING, "Can't replace BungeeCord thread pool with our own"); + getLogger().log(Level.INFO, "skipping replacement....."); + } + try { + loadConfig(); + } catch (IOException e) { + throw new RuntimeException("Unable to load/save config", e); + } catch (JedisConnectionException e) { + throw new RuntimeException("Unable to connect to your Redis server!", e); + } + this.api = new RedisBungeeAPI(this); + // call old plugin class to support old plugins + new RedisBungee(api); + if (isJedisAvailable()) { + try (Jedis tmpRsc = requestJedis()) { + // This is more portable than INFO
+ String info = tmpRsc.info(); + for (String s : info.split("\r\n")) { + if (s.startsWith("redis_version:")) { + String version = s.split(":")[1]; + getLogger().info(version + " <- redis version"); + if (!RedisUtil.isRedisVersionRight(version)) { + getLogger().warning("Your version of Redis (" + version + ") is not at least version 6.0 RedisBungee requires a newer version of Redis."); + throw new RuntimeException("Unsupported Redis version detected"); + } else { + LuaManager manager = new LuaManager(this); + serverToPlayersScript = manager.createScript(IOUtil.readInputStreamAsString(getResourceAsStream("lua/server_to_players.lua"))); + getPlayerCountScript = manager.createScript(IOUtil.readInputStreamAsString(getResourceAsStream("lua/get_player_count.lua"))); + } + break; + } + } + + tmpRsc.hset("heartbeats", configuration.getServerId(), tmpRsc.time().get(0)); + + long uuidCacheSize = tmpRsc.hlen("uuid-cache"); + if (uuidCacheSize > 750000) { + getLogger().info("Looks like you have a really big UUID cache! Run https://www.spigotmc.org/resources/redisbungeecleaner.8505/ as soon as possible."); + } + } + serverIds = getCurrentServerIds(true, false); + uuidTranslator = new UUIDTranslator(this); + heartbeatTask = service.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try (Jedis rsc = requestJedis()) { + long redisTime = getRedisTime(rsc.time()); + rsc.hset("heartbeats", configuration.getServerId(), String.valueOf(redisTime)); + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().log(Level.SEVERE, "Unable to update heartbeat - did your Redis server go away?", e); + return; + } + try { + serverIds = getCurrentServerIds(true, false); + globalPlayerCount.set(getCurrentCount()); + } catch (Throwable e) { + getLogger().log(Level.SEVERE, "Unable to update data - did your Redis server go away?", e); + } + } + }, 0, 3, TimeUnit.SECONDS); + dataManager = new BungeeDataManager(this); + getProxy().getPluginManager().registerListener(this, new RedisBungeeListener(this, configuration.getExemptAddresses())); + getProxy().getPluginManager().registerListener(this, dataManager); + psl = new PubSubListener(this); + getProxy().getScheduler().runAsync(this, psl); + integrityCheck = service.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try (Jedis tmpRsc = requestJedis()) { + Set players = getLocalPlayersAsUuidStrings(); + Set playersInRedis = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline"); + List lagged = getCurrentServerIds(false, true); + + // Clean up lagged players. + for (String s : lagged) { + Set laggedPlayers = tmpRsc.smembers("proxy:" + s + ":usersOnline"); + tmpRsc.del("proxy:" + s + ":usersOnline"); + if (!laggedPlayers.isEmpty()) { + getLogger().info("Cleaning up lagged proxy " + s + " (" + laggedPlayers.size() + " players)..."); + for (String laggedPlayer : laggedPlayers) { + RedisUtil.cleanUpPlayer(laggedPlayer, tmpRsc); + } + } + } + + Set absentLocally = new HashSet<>(playersInRedis); + absentLocally.removeAll(players); + Set absentInRedis = new HashSet<>(players); + absentInRedis.removeAll(playersInRedis); + + for (String member : absentLocally) { + boolean found = false; + for (String proxyId : getServerIds()) { + if (proxyId.equals(configuration.getServerId())) continue; + if (tmpRsc.sismember("proxy:" + proxyId + ":usersOnline", member)) { + // Just clean up the set. + found = true; + break; + } + } + if (!found) { + RedisUtil.cleanUpPlayer(member, tmpRsc); + getLogger().warning("Player found in set that was not found locally and globally: " + member); + } else { + tmpRsc.srem("proxy:" + configuration.getServerId() + ":usersOnline", member); + getLogger().warning("Player found in set that was not found locally, but is on another proxy: " + member); + } + } + + Pipeline pipeline = tmpRsc.pipelined(); + + for (String player : absentInRedis) { + // Player not online according to Redis but not BungeeCord. + getLogger().warning("Player " + player + " is on the proxy but not in Redis."); + + ProxiedPlayer proxiedPlayer = ProxyServer.getInstance().getPlayer(UUID.fromString(player)); + if (proxiedPlayer == null) + continue; // We'll deal with it later. + + RBUtils.createPlayer(proxiedPlayer, pipeline, true); + } + + pipeline.sync(); + } catch (Throwable e) { + getLogger().log(Level.SEVERE, "Unable to fix up stored player data", e); + } + } + }, 0, 1, TimeUnit.MINUTES); + } + getProxy().registerChannel("legacy:redisbungee"); + getProxy().registerChannel("RedisBungee"); + // register commands + if (configuration.doOverrideBungeeCommands()) { + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.GlistCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.FindCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.LastSeenCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.IpCommand(this)); + } + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.SendToAll(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerId(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerIds(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.PlayerProxyCommand(this)); + getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.PlistCommand(this)); + } + + @Override + public void stop() { + if (isJedisAvailable()) { + // Poison the PubSub listener + psl.poison(); + integrityCheck.cancel(true); + heartbeatTask.cancel(true); + getProxy().getPluginManager().unregisterListeners(this); + + try (Jedis tmpRsc = requestJedis()) { + tmpRsc.hdel("heartbeats", configuration.getServerId()); + if (tmpRsc.scard("proxy:" + configuration.getServerId() + ":usersOnline") > 0) { + Set players = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline"); + for (String member : players) + RedisUtil.cleanUpPlayer(member, tmpRsc); + } + } + try { + this.jedisSummoner.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void loadConfig() throws IOException { + if (!getDataFolder().exists()) { + getDataFolder().mkdir(); + } + + File file = new File(getDataFolder(), "config.yml"); + + if (!file.exists()) { + file.createNewFile(); + try (InputStream in = getResourceAsStream("example_config.yml"); + OutputStream out = new FileOutputStream(file)) { + ByteStreams.copy(in, out); + } + } + + final Configuration yamlConfiguration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); + + final String redisServer = yamlConfiguration.getString("redis-server", "localhost"); + final int redisPort = yamlConfiguration.getInt("redis-port", 6379); + final boolean useSSL = yamlConfiguration.getBoolean("useSSL", false); + String redisPassword = yamlConfiguration.getString("redis-password", ""); + String serverId = yamlConfiguration.getString("server-id"); + + // check redis password + if (redisPassword != null && (redisPassword.isEmpty() || redisPassword.equals("none"))) { + redisPassword = null; + getLogger().warning("INSECURE setup was detected Please set password for your redis instance."); + } + if (!useSSL) { + getLogger().warning("INSECURE setup was detected Please setup ssl for your redis instance."); + } + // Configuration sanity checks. + if (serverId == null || serverId.isEmpty()) { + /* + * this check causes the config comments to disappear somehow + * I think due snake yaml limitations so as todo: write our own yaml parser? + */ + String genId = UUID.randomUUID().toString(); + getLogger().info("Generated server id " + genId + " and saving it to config."); + yamlConfiguration.set("server-id", genId); + ConfigurationProvider.getProvider(YamlConfiguration.class).save(yamlConfiguration, new File(getDataFolder(), "config.yml")); + getLogger().info("Server id was generated: " + serverId); + } else { + getLogger().info("Loaded server id " + serverId + '.'); + } + this.configuration = new RedisBungeeConfiguration(serverId, yamlConfiguration.getStringList("exempt-ip-addresses"), yamlConfiguration.getBoolean("register-bungee-commands", true)); + + if (redisServer != null && !redisServer.isEmpty()) { + try { + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(yamlConfiguration.getInt("max-redis-connections", 8)); + this.jedisSummoner = new SinglePoolJedisSummoner(new JedisPool(config, redisServer, redisPort, 0, redisPassword, useSSL)); + + } catch (JedisConnectionException e) { + throw new RuntimeException("Unable to create Redis pool", e); + } + + // Test the connection + try (Jedis rsc = requestJedis()) { + rsc.ping(); + // If that worked, now we can check for an existing, alive Bungee: + File crashFile = new File(getDataFolder(), "restarted_from_crash.txt"); + if (crashFile.exists()) { + crashFile.delete(); + } else if (rsc.hexists("heartbeats", serverId)) { + try { + long value = Long.parseLong(rsc.hget("heartbeats", serverId)); + long redisTime = getRedisTime(rsc.time()); + if (redisTime < value + 20) { + getLogger().severe("You have launched a possible impostor BungeeCord instance. Another instance is already running."); + getLogger().severe("For data consistency reasons, RedisBungee will now disable itself."); + getLogger().severe("If this instance is coming up from a crash, create a file in your RedisBungee plugins directory with the name 'restarted_from_crash.txt' and RedisBungee will not perform this check."); + throw new RuntimeException("Possible impostor instance!"); + } + } catch (NumberFormatException ignored) { + } + } + + + httpClient = new OkHttpClient(); + Dispatcher dispatcher = new Dispatcher(getExecutorService()); + httpClient.setDispatcher(dispatcher); + NameFetcher.setHttpClient(httpClient); + UUIDFetcher.setHttpClient(httpClient); + + getLogger().log(Level.INFO, "Successfully connected to Redis."); + } catch (JedisConnectionException e) { + this.jedisSummoner.close(); + throw e; + } + } else { + throw new RuntimeException("No redis server specified!"); + } + } + + @Override + public void onEnable() { + start(); + } + + @Override + public void onDisable() { + stop(); + } + + @Override + public Class getPubSubEventClass() { + return PubSubMessageEvent.class; + } + + @Override + public Class getNetworkJoinEventClass() { + return PlayerJoinedNetworkEvent.class; + } + + @Override + public Class getServerChangeEventClass() { + return PlayerChangedServerNetworkEvent.class; + } + + @Override + public Class getNetworkQuitEventClass() { + return PlayerJoinedNetworkEvent.class; + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java new file mode 100644 index 0000000..c675eae --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSender.java @@ -0,0 +1,74 @@ +package com.imaginarycode.minecraft.redisbungee; + +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.BaseComponent; + +import java.util.Collection; +import java.util.Collections; + +public class RedisBungeeCommandSender implements CommandSender { + private static final RedisBungeeCommandSender singleton; + + static { + singleton = new RedisBungeeCommandSender(); + } + + public static RedisBungeeCommandSender getSingleton() { + return singleton; + } + + @Override + public String getName() { + return "RedisBungee"; + } + + @Override + public void sendMessage(String s) { + + } + + @Override + public void sendMessages(String... strings) { + + } + + @Override + public void sendMessage(BaseComponent... baseComponents) { + + } + + @Override + public void sendMessage(BaseComponent baseComponent) { + + } + + @Override + public Collection getGroups() { + return null; + } + + @Override + public void addGroups(String... strings) { + + } + + @Override + public void removeGroups(String... strings) { + + } + + @Override + public boolean hasPermission(String s) { + return true; + } + + @Override + public void setPermission(String s, boolean b) { + + } + + @Override + public Collection getPermissions() { + return Collections.emptySet(); + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java new file mode 100644 index 0000000..797ba98 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java @@ -0,0 +1,261 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.imaginarycode.minecraft.redisbungee.internal.AbstractRedisBungeeListener; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.RedisUtil; +import com.imaginarycode.minecraft.redisbungee.internal.util.RedisCallable; +import net.md_5.bungee.api.AbstractReconnectHandler; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.connection.Server; +import net.md_5.bungee.api.event.*; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Pipeline; + +import java.net.InetAddress; +import java.util.*; + +public class RedisBungeeListener extends AbstractRedisBungeeListener implements Listener { + + + public RedisBungeeListener(RedisBungeePlugin plugin, List exemptAddresses) { + super(plugin, exemptAddresses); + } + + @Override + @EventHandler + public void onLogin(LoginEvent event) { + event.registerIntent((Plugin) plugin); + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + try { + if (event.isCancelled()) { + return null; + } + + // We make sure they aren't trying to use an existing player's name. + // This is problematic for online-mode servers as they always disconnect old clients. + if (plugin.isOnlineMode()) { + ProxiedPlayer player = (ProxiedPlayer) plugin.getPlayer(event.getConnection().getName()); + + if (player != null) { + event.setCancelled(true); + // TODO: Make it accept a BaseComponent[] like everything else. + event.setCancelReason(ONLINE_MODE_RECONNECT); + return null; + } + } + + for (String s : plugin.getServerIds()) { + if (jedis.sismember("proxy:" + s + ":usersOnline", event.getConnection().getUniqueId().toString())) { + event.setCancelled(true); + // TODO: Make it accept a BaseComponent[] like everything else. + event.setCancelReason(ALREADY_LOGGED_IN); + return null; + } + } + return null; + } finally { + event.completeIntent((Plugin) plugin); + } + } + }); + } + + @Override + @EventHandler + public void onPostLogin(PostLoginEvent event) { + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + // this code was moved out from login event due being async.. + // and it can be cancelled but it will show as false in redis-bungee + // which will register the player into the redis database. + Pipeline pipeline = jedis.pipelined(); + plugin.getUuidTranslator().persistInfo(event.getPlayer().getName(), event.getPlayer().getUniqueId(), pipeline); + RBUtils.createPlayer(event.getPlayer(), pipeline, false); + pipeline.sync(); + // the end of moved code. + + jedis.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage( + event.getPlayer().getUniqueId(), plugin.getApi().getServerId(), DataManager.DataManagerMessage.Action.JOIN, + new DataManager.LoginPayload(event.getPlayer().getAddress().getAddress())))); + return null; + } + }); + } + + @Override + @EventHandler + public void onPlayerDisconnect(PlayerDisconnectEvent event) { + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + Pipeline pipeline = jedis.pipelined(); + RedisUtil.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), pipeline); + pipeline.sync(); + return null; + } + }); + + } + + @Override + @EventHandler + public void onServerChange(ServerConnectedEvent event) { + final String currentServer = event.getPlayer().getServer() == null ? null : event.getPlayer().getServer().getInfo().getName(); + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + jedis.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", event.getServer().getInfo().getName()); + jedis.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage( + event.getPlayer().getUniqueId(), plugin.getApi().getServerId(), DataManager.DataManagerMessage.Action.SERVER_CHANGE, + new DataManager.ServerChangePayload(event.getServer().getInfo().getName(), currentServer)))); + return null; + } + }); + } + + @Override + @EventHandler + public void onPing(ProxyPingEvent event) { + if (exemptAddresses.contains(event.getConnection().getAddress().getAddress())) { + return; + } + ServerInfo forced = AbstractReconnectHandler.getForcedHost(event.getConnection()); + + if (forced != null && event.getConnection().getListener().isPingPassthrough()) { + return; + } + event.getResponse().getPlayers().setOnline(plugin.getCount()); + } + + @Override + @SuppressWarnings("UnstableApiUsage") + @EventHandler + public void onPluginMessage(PluginMessageEvent event) { + if ((event.getTag().equals("legacy:redisbungee") || event.getTag().equals("RedisBungee")) && event.getSender() instanceof Server) { + final String currentChannel = event.getTag(); + final byte[] data = Arrays.copyOf(event.getData(), event.getData().length); + plugin.executeAsync(() -> { + ByteArrayDataInput in = ByteStreams.newDataInput(data); + + String subchannel = in.readUTF(); + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + String type; + + switch (subchannel) { + case "PlayerList": + out.writeUTF("PlayerList"); + Set original = Collections.emptySet(); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + original = plugin.getPlayers(); + } else { + try { + original = plugin.getApi().getPlayersOnServer(type); + } catch (IllegalArgumentException ignored) { + } + } + Set players = new HashSet<>(); + for (UUID uuid : original) + players.add(plugin.getUuidTranslator().getNameFromUuid(uuid, false)); + out.writeUTF(Joiner.on(',').join(players)); + break; + case "PlayerCount": + out.writeUTF("PlayerCount"); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + out.writeInt(plugin.getCount()); + } else { + out.writeUTF(type); + try { + out.writeInt(plugin.getApi().getPlayersOnServer(type).size()); + } catch (IllegalArgumentException e) { + out.writeInt(0); + } + } + break; + case "LastOnline": + String user = in.readUTF(); + out.writeUTF("LastOnline"); + out.writeUTF(user); + out.writeLong(plugin.getApi().getLastOnline(plugin.getUuidTranslator().getTranslatedUuid(user, true))); + break; + case "ServerPlayers": + String type1 = in.readUTF(); + out.writeUTF("ServerPlayers"); + Multimap multimap = plugin.getApi().getServerToPlayers(); + + boolean includesUsers; + + switch (type1) { + case "COUNT": + includesUsers = false; + break; + case "PLAYERS": + includesUsers = true; + break; + default: + // TODO: Should I raise an error? + return; + } + + out.writeUTF(type1); + + if (includesUsers) { + Multimap human = HashMultimap.create(); + for (Map.Entry entry : multimap.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + serializeMultimap(human, true, out); + } else { + serializeMultiset(multimap.keys(), out); + } + break; + case "Proxy": + out.writeUTF("Proxy"); + out.writeUTF(plugin.getConfiguration().getServerId()); + break; + case "PlayerProxy": + String username = in.readUTF(); + out.writeUTF("PlayerProxy"); + out.writeUTF(username); + out.writeUTF(plugin.getApi().getProxy(plugin.getUuidTranslator().getTranslatedUuid(username, true))); + break; + default: + return; + } + + ((Server) event.getSender()).sendData(currentChannel, out.toByteArray()); + }); + } + } + + @Override + @EventHandler + public void onPubSubMessage(PubSubMessageEvent event) { + if (event.getChannel().equals("redisbungee-allservers") || event.getChannel().equals("redisbungee-" + plugin.getApi().getServerId())) { + String message = event.getMessage(); + if (message.startsWith("/")) + message = message.substring(1); + plugin.logInfo("Invoking command via PubSub: /" + message); + ((Plugin) plugin).getProxy().getPluginManager().dispatchCommand(RedisBungeeCommandSender.getSingleton(), message); + } + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java new file mode 100644 index 0000000..cdc135f --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java @@ -0,0 +1,343 @@ +package com.imaginarycode.minecraft.redisbungee.commands; + +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; +import com.imaginarycode.minecraft.redisbungee.RedisBungeeBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; + +import java.net.InetAddress; +import java.text.SimpleDateFormat; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +/** + * This class contains subclasses that are used for the commands RedisBungee overrides or includes: /glist, /find and /lastseen. + *

+ * All classes use the {@link RedisBungeeAPI}. + * + * @author tuxed + * @since 0.2.3 + */ +public class RedisBungeeCommands { + private static final BaseComponent[] NO_PLAYER_SPECIFIED = + new ComponentBuilder("You must specify a player name.").color(ChatColor.RED).create(); + private static final BaseComponent[] PLAYER_NOT_FOUND = + new ComponentBuilder("No such player found.").color(ChatColor.RED).create(); + private static final BaseComponent[] NO_COMMAND_SPECIFIED = + new ComponentBuilder("You must specify a command to be run.").color(ChatColor.RED).create(); + + private static String playerPlural(int num) { + return num == 1 ? num + " player is" : num + " players are"; + } + + public static class GlistCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public GlistCommand(RedisBungeeBungeePlugin plugin) { + super("glist", "bungeecord.command.list", "redisbungee", "rglist"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + int count = plugin.getApi().getPlayerCount(); + BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) + .append(playerPlural(count) + " currently online.").create(); + if (args.length > 0 && args[0].equals("showall")) { + Multimap serverToPlayers = plugin.getApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + for (Map.Entry entry : serverToPlayers.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + for (String server : new TreeSet<>(serverToPlayers.keySet())) { + TextComponent serverName = new TextComponent(); + serverName.setColor(ChatColor.GREEN); + serverName.setText("[" + server + "] "); + TextComponent serverCount = new TextComponent(); + serverCount.setColor(ChatColor.YELLOW); + serverCount.setText("(" + serverToPlayers.get(server).size() + "): "); + TextComponent serverPlayers = new TextComponent(); + serverPlayers.setColor(ChatColor.WHITE); + serverPlayers.setText(Joiner.on(", ").join(human.get(server))); + sender.sendMessage(serverName, serverCount, serverPlayers); + } + sender.sendMessage(playersOnline); + } else { + sender.sendMessage(playersOnline); + sender.sendMessage(new ComponentBuilder("To see all players online, use /glist showall.").color(ChatColor.YELLOW).create()); + } + } + }); + } + } + + public static class FindCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public FindCommand(RedisBungeeBungeePlugin plugin) { + super("find", "bungeecord.command.find", "rfind"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + ServerInfo si = plugin.getProxy().getServerInfo(plugin.getApi().getServerFor(uuid)); + if (si != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " is on " + si.getName() + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class LastSeenCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public LastSeenCommand(RedisBungeeBungeePlugin plugin) { + super("lastseen", "redisbungee.command.lastseen", "rlastseen"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + long secs = plugin.getApi().getLastOnline(uuid); + TextComponent message = new TextComponent(); + if (secs == 0) { + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is currently online."); + } else if (secs != -1) { + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); + } else { + message.setColor(ChatColor.RED); + message.setText(args[0] + " has never been online."); + } + sender.sendMessage(message); + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class IpCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public IpCommand(RedisBungeeBungeePlugin plugin) { + super("ip", "redisbungee.command.ip", "playerip", "rip", "rplayerip"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + InetAddress ia = plugin.getApi().getPlayerIp(uuid); + if (ia != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is connected from " + ia.toString() + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class PlayerProxyCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public PlayerProxyCommand(RedisBungeeBungeePlugin plugin) { + super("pproxy", "redisbungee.command.pproxy"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + String proxy = plugin.getApi().getProxy(uuid); + if (proxy != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is connected to " + proxy + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class SendToAll extends Command { + private final RedisBungeeBungeePlugin plugin; + + public SendToAll(RedisBungeeBungeePlugin plugin) { + super("sendtoall", "redisbungee.command.sendtoall", "rsendtoall"); + this.plugin = plugin; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length > 0) { + String command = Joiner.on(" ").skipNulls().join(args); + plugin.getApi().sendProxyCommand(command); + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText("Sent the command /" + command + " to all proxies."); + sender.sendMessage(message); + } else { + sender.sendMessage(NO_COMMAND_SPECIFIED); + } + } + } + + public static class ServerId extends Command { + private final RedisBungeeBungeePlugin plugin; + + public ServerId(RedisBungeeBungeePlugin plugin) { + super("serverid", "redisbungee.command.serverid", "rserverid"); + this.plugin = plugin; + } + + @Override + public void execute(CommandSender sender, String[] args) { + TextComponent textComponent = new TextComponent(); + textComponent.setText("You are on " + plugin.getApi().getServerId() + "."); + textComponent.setColor(ChatColor.YELLOW); + sender.sendMessage(textComponent); + } + } + + public static class ServerIds extends Command { + private final RedisBungeeBungeePlugin plugin; + public ServerIds(RedisBungeeBungeePlugin plugin) { + super("serverids", "redisbungee.command.serverids"); + this.plugin =plugin; + } + + @Override + public void execute(CommandSender sender, String[] strings) { + TextComponent textComponent = new TextComponent(); + textComponent.setText("All server IDs: " + Joiner.on(", ").join(plugin.getApi().getAllServers())); + textComponent.setColor(ChatColor.YELLOW); + sender.sendMessage(textComponent); + } + } + + public static class PlistCommand extends Command { + private final RedisBungeeBungeePlugin plugin; + + public PlistCommand(RedisBungeeBungeePlugin plugin) { + super("plist", "redisbungee.command.plist", "rplist"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + String proxy = args.length >= 1 ? args[0] : plugin.getConfiguration().getServerId(); + if (!plugin.getServerIds().contains(proxy)) { + sender.sendMessage(new ComponentBuilder(proxy + " is not a valid proxy. See /serverids for valid proxies.").color(ChatColor.RED).create()); + return; + } + Set players = plugin.getApi().getPlayersOnProxy(proxy); + BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) + .append(playerPlural(players.size()) + " currently on proxy " + proxy + ".").create(); + if (args.length >= 2 && args[1].equals("showall")) { + Multimap serverToPlayers = plugin.getApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + for (Map.Entry entry : serverToPlayers.entries()) { + if (players.contains(entry.getValue())) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + } + for (String server : new TreeSet<>(human.keySet())) { + TextComponent serverName = new TextComponent(); + serverName.setColor(ChatColor.RED); + serverName.setText("[" + server + "] "); + TextComponent serverCount = new TextComponent(); + serverCount.setColor(ChatColor.YELLOW); + serverCount.setText("(" + human.get(server).size() + "): "); + TextComponent serverPlayers = new TextComponent(); + serverPlayers.setColor(ChatColor.WHITE); + serverPlayers.setText(Joiner.on(", ").join(human.get(server))); + sender.sendMessage(serverName, serverCount, serverPlayers); + } + sender.sendMessage(playersOnline); + } else { + sender.sendMessage(playersOnline); + sender.sendMessage(new ComponentBuilder("To see all players online, use /plist " + proxy + " showall.").color(ChatColor.YELLOW).create()); + } + } + }); + } + } +} \ No newline at end of file diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java new file mode 100644 index 0000000..ac01a96 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java @@ -0,0 +1,38 @@ +package com.imaginarycode.minecraft.redisbungee.events; + +import net.md_5.bungee.api.plugin.Event; + +import java.util.UUID; + +/** + * This event is sent when a player connects to a new server. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.ServerConnectedEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerChangedServerNetworkEvent extends Event { + private final UUID uuid; + private final String previousServer; + private final String server; + + public PlayerChangedServerNetworkEvent(UUID uuid, String previousServer, String server) { + this.uuid = uuid; + this.previousServer = previousServer; + this.server = server; + } + + public UUID getUuid() { + return uuid; + } + + public String getServer() { + return server; + } + + public String getPreviousServer() { + return previousServer; + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java new file mode 100644 index 0000000..b9eac18 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java @@ -0,0 +1,26 @@ +package com.imaginarycode.minecraft.redisbungee.events; + +import net.md_5.bungee.api.plugin.Event; + +import java.util.UUID; + +/** + * This event is sent when a player joins the network. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.PostLoginEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerJoinedNetworkEvent extends Event { + private final UUID uuid; + + public PlayerJoinedNetworkEvent(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUuid() { + return uuid; + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java new file mode 100644 index 0000000..5e9e5ab --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java @@ -0,0 +1,26 @@ +package com.imaginarycode.minecraft.redisbungee.events; + +import net.md_5.bungee.api.plugin.Event; + +import java.util.UUID; + +/** + * This event is sent when a player disconnects. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.PlayerDisconnectEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerLeftNetworkEvent extends Event { + private final UUID uuid; + + public PlayerLeftNetworkEvent(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUuid() { + return uuid; + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java new file mode 100644 index 0000000..32bcb40 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java @@ -0,0 +1,29 @@ +package com.imaginarycode.minecraft.redisbungee.events; + +import net.md_5.bungee.api.plugin.Event; + +/** + * This event is posted when a PubSub message is received. + *

+ * Warning: This event is fired in a separate thread! + * + * @since 0.2.6 + */ + +public class PubSubMessageEvent extends Event { + private final String channel; + private final String message; + + public PubSubMessageEvent(String channel, String message) { + this.channel = channel; + this.message = message; + } + + public String getChannel() { + return channel; + } + + public String getMessage() { + return message; + } +} diff --git a/RedisBungee-Velocity/RedisBungee-Bungee/src/main/resources/plugin.yml b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/resources/plugin.yml new file mode 100644 index 0000000..797ef14 --- /dev/null +++ b/RedisBungee-Velocity/RedisBungee-Bungee/src/main/resources/plugin.yml @@ -0,0 +1,9 @@ +name: RedisBungee +main: com.imaginarycode.minecraft.redisbungee.RedisBungeeVelocityPlugin +version: ${project.version} +author: Chunkr and Govindas limework +authors: + - chunkr + - Govindas Limework +# This is used so that we can automatically override default BungeeCord behavior. +softDepends: ["cmd_find", "cmd_list"] \ No newline at end of file diff --git a/RedisBungee-Velocity/pom.xml b/RedisBungee-Velocity/pom.xml new file mode 100644 index 0000000..2106afb --- /dev/null +++ b/RedisBungee-Velocity/pom.xml @@ -0,0 +1,125 @@ + + + + RedisBungee + com.imaginarycode.minecraft + 0.8.0-SNAPSHOT + + 4.0.0 + + RedisBungee-Velocity + + + 8 + 8 + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.2 + + 8 + ../javadoc + ${project.name} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + shade + + + + redis.clients.jedis + com.imaginarycode.minecraft.redisbungee.internal.jedis + + + + redis.clients.util + com.imaginarycode.minecraft.redisbungee.internal.jedisutil + + + + org.apache.commons.pool + com.imaginarycode.minecraft.redisbungee.internal.commonspool + + + + com.squareup.okhttp + com.imaginarycode.minecraft.redisbungee.internal.okhttp + + + + okio + com.imaginarycode.minecraft.redisbungee.internal.okio + + + + com.google + com.imaginarycode.minecraft.redisbungee.internal.google + + + + org.json + com.imaginarycode.minecraft.redisbungee.internal.json + + + + org.checkerframework + com.imaginarycode.minecraft.redisbungee.internal.checkframework + + + + + + + + + + + + + com.imaginarycode.minecraft + RedisBungee-API + ${parent.version} + + + com.velocitypowered + velocity-api + 3.1.0 + jar + provided + + + + \ No newline at end of file diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java new file mode 100644 index 0000000..4c9c9d1 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RBUtils.java @@ -0,0 +1,38 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.google.gson.Gson; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import redis.clients.jedis.Pipeline; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class RBUtils { + + private static final Gson gson = new Gson(); + + protected static void createPlayer(Player player, Pipeline pipeline, boolean fireEvent) { + Optional server = player.getCurrentServer(); + server.ifPresent(serverConnection -> pipeline.hset("player:" + player.getUniqueId().toString(), "server", serverConnection.getServerInfo().getName())); + + Map playerData = new HashMap<>(4); + playerData.put("online", "0"); + playerData.put("ip", player.getRemoteAddress().getHostName()); + playerData.put("proxy", RedisBungeeAPI.getRedisBungeeApi().getServerId()); + + pipeline.sadd("proxy:" + RedisBungeeAPI.getRedisBungeeApi().getServerId() + ":usersOnline", player.getUniqueId().toString()); + pipeline.hmset("player:" + player.getUniqueId().toString(), playerData); + + if (fireEvent) { + pipeline.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage<>( + player.getUniqueId(), RedisBungeeAPI.getRedisBungeeApi().getServerId(), DataManager.DataManagerMessage.Action.JOIN, + new DataManager.LoginPayload(player.getRemoteAddress().getAddress())))); + } + } + + +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSource.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSource.java new file mode 100644 index 0000000..f843028 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeCommandSource.java @@ -0,0 +1,38 @@ +package com.imaginarycode.minecraft.redisbungee; + + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.permission.Tristate; +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.TriState; + +import java.util.Collection; +import java.util.Collections; + +public class RedisBungeeCommandSource implements CommandSource { + private static final RedisBungeeCommandSource singleton; + + static { + singleton = new RedisBungeeCommandSource(); + } + + public static RedisBungeeCommandSource getSingleton() { + return singleton; + } + + + @Override + public boolean hasPermission(String permission) { + return true; + } + + @Override + public Tristate getPermissionValue(String s) { + return null; + } + + @Override + public PermissionChecker getPermissionChecker() { + return PermissionChecker.always(TriState.TRUE); + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java new file mode 100644 index 0000000..16e2443 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java @@ -0,0 +1,161 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.internal.AbstractRedisBungeeListener; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.RedisUtil; +import com.imaginarycode.minecraft.redisbungee.internal.util.RedisCallable; +import com.velocitypowered.api.event.Continuation; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.ServerPing; +import net.kyori.adventure.text.Component; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Pipeline; + +import java.net.InetAddress; +import java.util.*; + +public class RedisBungeeListener extends AbstractRedisBungeeListener { + + + public RedisBungeeListener(RedisBungeePlugin plugin, List exemptAddresses) { + super(plugin, exemptAddresses); + } + + @Override + @Subscribe + public void onLogin(LoginEvent event, Continuation continuation) { + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + try { + if (!event.getResult().isAllowed()) { + return null; + } + + // We make sure they aren't trying to use an existing player's name. + // This is problematic for online-mode servers as they always disconnect old clients. + if (plugin.isOnlineMode()) { + Player player = (Player) plugin.getPlayer(event.getPlayer().getUsername()); + + if (player != null) { + event.setResult(ResultedEvent.ComponentResult.denied(Component.text(ONLINE_MODE_RECONNECT))); + return null; + } + } + + for (String s : plugin.getServerIds()) { + if (jedis.sismember("proxy:" + s + ":usersOnline", event.getPlayer().getUniqueId().toString())) { + event.setResult(ResultedEvent.ComponentResult.denied(Component.text(ALREADY_LOGGED_IN))); + return null; + } + } + return null; + } finally { + continuation.resume(); + } + } + }); + } + + @Override + @Subscribe + public void onPostLogin(PostLoginEvent event) { + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + // this code was moved out from login event due being async.. + // and it can be cancelled but it will show as false in redis-bungee + // which will register the player into the redis database. + Pipeline pipeline = jedis.pipelined(); + plugin.getUuidTranslator().persistInfo(event.getPlayer().getUsername(), event.getPlayer().getUniqueId(), pipeline); + RBUtils.createPlayer(event.getPlayer(), pipeline, false); + pipeline.sync(); + // the end of moved code. + + jedis.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage<>( + event.getPlayer().getUniqueId(), plugin.getApi().getServerId(), DataManager.DataManagerMessage.Action.JOIN, + new DataManager.LoginPayload(event.getPlayer().getRemoteAddress().getAddress())))); + return null; + } + }); + } + + @Override + @Subscribe + public void onPlayerDisconnect(DisconnectEvent event) { + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + Pipeline pipeline = jedis.pipelined(); + RedisUtil.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), pipeline); + pipeline.sync(); + return null; + } + }); + + } + + @Override + @Subscribe + public void onServerChange(ServerConnectedEvent event) { + Optional optionalServerConnection = event.getPlayer().getCurrentServer(); + final String currentServer = optionalServerConnection.map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null); + plugin.executeAsync(new RedisCallable(plugin) { + @Override + protected Void call(Jedis jedis) { + jedis.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", event.getServer().getServerInfo().getName()); + jedis.publish("redisbungee-data", gson.toJson(new DataManager.DataManagerMessage<>( + event.getPlayer().getUniqueId(), plugin.getApi().getServerId(), DataManager.DataManagerMessage.Action.SERVER_CHANGE, + new DataManager.ServerChangePayload(event.getServer().getServerInfo().getName(), currentServer)))); + return null; + } + }); + } + + @Override + @Subscribe(order = PostOrder.EARLY) + public void onPing(ProxyPingEvent event) { + if (exemptAddresses.contains(event.getConnection().getRemoteAddress().getAddress())) { + return; + } + ServerPing oldPing = event.getPing(); + int max = oldPing.getPlayers().map(ServerPing.Players::getMax).orElse(0); + List list = oldPing.getPlayers().map(ServerPing.Players::getSample).orElse(Collections.emptyList()); + event.setPing(new ServerPing(oldPing.getVersion(), new ServerPing.Players(plugin.getCount(), max, list), oldPing.getDescriptionComponent(), oldPing.getFavicon().orElse(null))); + } + + @Override + public void onPluginMessage(PluginMessageEvent event) { + /* + * Ham1255 note: for some reason plugin messages were not working in velocity? + * not sure how to fix, but for now i have removed the code until a fix is made. + * + */ + } + + + @Override + @Subscribe + public void onPubSubMessage(PubSubMessageEvent event) { + if (event.getChannel().equals("redisbungee-allservers") || event.getChannel().equals("redisbungee-" + plugin.getApi().getServerId())) { + String message = event.getMessage(); + if (message.startsWith("/")) + message = message.substring(1); + plugin.logInfo("Invoking command via PubSub: /" + message); + ((RedisBungeeVelocityPlugin)plugin).getProxy().getCommandManager().executeAsync(RedisBungeeCommandSource.getSingleton(), message);//.dispatchCommand(RedisBungeeCommandSource.getSingleton(), message); + + } + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java new file mode 100644 index 0000000..db90793 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java @@ -0,0 +1,667 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import com.google.inject.Inject; +import com.imaginarycode.minecraft.redisbungee.commands.RedisBungeeCommands; +import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PlayerJoinedNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.*; +import com.imaginarycode.minecraft.redisbungee.internal.summoners.JedisSummoner; +import com.imaginarycode.minecraft.redisbungee.internal.summoners.SinglePoolJedisSummoner; +import com.imaginarycode.minecraft.redisbungee.internal.util.IOUtil; +import com.imaginarycode.minecraft.redisbungee.internal.util.LuaManager; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.NameFetcher; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.UUIDFetcher; +import com.imaginarycode.minecraft.redisbungee.internal.util.uuid.UUIDTranslator; +import com.squareup.okhttp.Dispatcher; +import com.squareup.okhttp.OkHttpClient; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.ScheduledTask; +import org.slf4j.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.io.*; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +import static com.google.common.base.Preconditions.checkArgument; + +public class RedisBungeeVelocityPlugin implements RedisBungeePlugin { + + private static final Gson gson = new Gson(); + private final ProxyServer server; + private final Logger logger; + private final File dataFolder; + private RedisBungeeAPI api; + private PubSubListener psl = null; + private JedisSummoner jedisSummoner; + private UUIDTranslator uuidTranslator; + private RedisBungeeConfiguration configuration; + private VelocityDataManager dataManager; + private OkHttpClient httpClient; + private volatile List serverIds; + private final AtomicInteger nagAboutServers = new AtomicInteger(); + private final AtomicInteger globalPlayerCount = new AtomicInteger(); + private ScheduledTask integrityCheck; + private ScheduledTask heartbeatTask; + private LuaManager.Script serverToPlayersScript; + private LuaManager.Script getPlayerCountScript; + + private static final Object SERVER_TO_PLAYERS_KEY = new Object(); + private final Cache> serverToPlayersCache = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.SECONDS) + .build(); + + + @Inject + public RedisBungeeVelocityPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { + this.server = server; + this.logger = logger; + this.dataFolder = dataDirectory.toFile(); + } + + + @Override + public RedisBungeeConfiguration getConfiguration() { + return this.configuration; + } + + @Override + public int getCount() { + return this.globalPlayerCount.get(); + } + + @Override + public int getCurrentCount() { + Long count = (Long) getPlayerCountScript.eval(ImmutableList.of(), ImmutableList.of()); + return count.intValue(); + } + + @Override + public Set getLocalPlayersAsUuidStrings() { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (Player player : getProxy().getAllPlayers()) { + builder.add(player.getUniqueId().toString()); + } + return builder.build(); + } + + @Override + public DataManager getDataManager() { + return this.dataManager; + } + + @Override + public Set getPlayers() { + ImmutableSet.Builder setBuilder = ImmutableSet.builder(); + if (isJedisAvailable()) { + try (Jedis rsc = requestJedis()) { + List keys = new ArrayList<>(); + for (String i : getServerIds()) { + keys.add("proxy:" + i + ":usersOnline"); + } + if (!keys.isEmpty()) { + Set users = rsc.sunion(keys.toArray(new String[keys.size()])); + if (users != null && !users.isEmpty()) { + for (String user : users) { + try { + setBuilder = setBuilder.add(UUID.fromString(user)); + } catch (IllegalArgumentException ignored) { + } + } + } + } + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().error("Unable to get connection from pool - did your Redis server go away?", e); + throw new RuntimeException("Unable to get all players online", e); + } + } + return setBuilder.build(); + } + + @Override + public Jedis requestJedis() { + return this.jedisSummoner.requestJedis(); + } + + @Override + public boolean isJedisAvailable() { + return this.jedisSummoner.isJedisAvailable(); + } + + @Override + public RedisBungeeAPI getApi() { + return this.api; + } + + @Override + public UUIDTranslator getUuidTranslator() { + return this.uuidTranslator; + } + + @Override + public Multimap serversToPlayers() { + try { + return serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, new Callable>() { + @Override + public Multimap call() throws Exception { + Collection data = (Collection) serverToPlayersScript.eval(ImmutableList.of(), getServerIds()); + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + String key = null; + for (String s : data) { + if (key == null) { + key = s; + continue; + } + + builder.put(key, UUID.fromString(s)); + key = null; + } + + return builder.build(); + } + }); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + @Override + public Set getPlayersOnProxy(String proxyId) { + checkArgument(getServerIds().contains(proxyId), proxyId + " is not a valid proxy ID"); + try (Jedis jedis = requestJedis()) { + Set users = jedis.smembers("proxy:" + proxyId + ":usersOnline"); + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (String user : users) { + builder.add(UUID.fromString(user)); + } + return builder.build(); + } + } + + @Override + public void sendProxyCommand(String serverId, String command) { + checkArgument(getServerIds().contains(serverId) || serverId.equals("allservers"), "proxyId is invalid"); + sendChannelMessage("redisbungee-" + serverId, command); + } + + @Override + public List getServerIds() { + return serverIds; + } + + @Override + public List getCurrentServerIds(boolean nag, boolean lagged) { + try (Jedis jedis = requestJedis()) { + long time = getRedisTime(jedis.time()); + int nagTime = 0; + if (nag) { + nagTime = nagAboutServers.decrementAndGet(); + if (nagTime <= 0) { + nagAboutServers.set(10); + } + } + ImmutableList.Builder servers = ImmutableList.builder(); + Map heartbeats = jedis.hgetAll("heartbeats"); + for (Map.Entry entry : heartbeats.entrySet()) { + try { + long stamp = Long.parseLong(entry.getValue()); + if (lagged ? time >= stamp + 30 : time <= stamp + 30) + servers.add(entry.getKey()); + else if (nag && nagTime <= 0) { + getLogger().warn(entry.getKey() + " is " + (time - stamp) + " seconds behind! (Time not synchronized or server down?) and was removed from heartbeat."); + jedis.hdel("heartbeats", entry.getKey()); + } + } catch (NumberFormatException ignored) { + } + } + return servers.build(); + } catch (JedisConnectionException e) { + getLogger().error("Unable to fetch server IDs", e); + return Collections.singletonList(configuration.getServerId()); + } + } + + @Override + public PubSubListener getPubSubListener() { + return this.psl; + } + + @Override + public void sendChannelMessage(String channel, String message) { + try (Jedis jedis = requestJedis()) { + jedis.publish(channel, message); + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().error("Unable to get connection from pool - did your Redis server go away?", e); + throw new RuntimeException("Unable to publish channel message", e); + } + } + + @Override + public void executeAsync(Runnable runnable) { + this.getProxy().getScheduler().buildTask(this, runnable).schedule(); + } + + @Override + public void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int time) { + this.getProxy().getScheduler().buildTask(this, runnable).delay(time, timeUnit); + } + + @Override + public void callEvent(Object event) { + this.getProxy().getEventManager().fire(event); + } + + @Override + public boolean isOnlineMode() { + return this.getProxy().getConfiguration().isOnlineMode(); + } + + @Override + public void logInfo(String msg) { + this.getLogger().info(msg); + } + + @Override + public void logWarn(String msg) { + this.getLogger().warn(msg); + } + + @Override + public void logFatal(String msg) { + this.getLogger().error(msg); + } + + @Override + public Player getPlayer(UUID uuid) { + return this.getProxy().getPlayer(uuid).orElse(null); + } + + @Override + public Player getPlayer(String name) { + return this.getProxy().getPlayer(name).orElse(null); + } + + @Override + public UUID getPlayerUUID(String player) { + return this.getProxy().getPlayer(player).map(Player::getUniqueId).orElse(null); + } + + @Override + public String getPlayerName(UUID player) { + return this.getProxy().getPlayer(player).map(Player::getUsername).orElse(null); + } + + @Override + public String getPlayerServerName(Player player) { + return player.getCurrentServer().map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null); + } + + @Override + public boolean isPlayerOnAServer(Player player) { + return player.getCurrentServer().isPresent(); + } + + @Override + public InetAddress getPlayerIp(Player player) { + return player.getRemoteAddress().getAddress(); + } + + @Override + public void sendProxyCommand(String cmd) { + checkArgument(getServerIds().contains(this.configuration.getServerId()) || this.configuration.getServerId().equals("allservers"), "proxyId is invalid"); + sendChannelMessage("redisbungee-" + this.configuration.getServerId(), cmd); + } + + @Override + public long getRedisTime(List timeRes) { + return Long.parseLong(timeRes.get(0)); + } + + + @Override + public void start() { + try { + loadConfig(); + } catch (IOException e) { + throw new RuntimeException("Unable to load/save config", e); + } catch (JedisConnectionException e) { + throw new RuntimeException("Unable to connect to your Redis server!", e); + } + this.api = new RedisBungeeAPI(this); + // call old plugin class to support old plugins + new RedisBungee(api); + if (isJedisAvailable()) { + try (Jedis tmpRsc = requestJedis()) { + // This is more portable than INFO

+ String info = tmpRsc.info(); + for (String s : info.split("\r\n")) { + if (s.startsWith("redis_version:")) { + String version = s.split(":")[1]; + getLogger().info(version + " <- redis version"); + if (!RedisUtil.isRedisVersionRight(version)) { + getLogger().warn("Your version of Redis (" + version + ") is not at least version 6.0 RedisBungee requires a newer version of Redis."); + throw new RuntimeException("Unsupported Redis version detected"); + } else { + LuaManager manager = new LuaManager(this); + serverToPlayersScript = manager.createScript(IOUtil.readInputStreamAsString(getResourceAsStream("lua/server_to_players.lua"))); + getPlayerCountScript = manager.createScript(IOUtil.readInputStreamAsString(getResourceAsStream("lua/get_player_count.lua"))); + } + break; + } + } + + tmpRsc.hset("heartbeats", configuration.getServerId(), tmpRsc.time().get(0)); + + long uuidCacheSize = tmpRsc.hlen("uuid-cache"); + if (uuidCacheSize > 750000) { + getLogger().info("Looks like you have a really big UUID cache! Run https://www.spigotmc.org/resources/redisbungeecleaner.8505/ as soon as possible."); + } + } + serverIds = getCurrentServerIds(true, false); + uuidTranslator = new UUIDTranslator(this); + heartbeatTask = getProxy().getScheduler().buildTask(this, new Runnable() { + @Override + public void run() { + try (Jedis rsc = requestJedis()) { + long redisTime = getRedisTime(rsc.time()); + rsc.hset("heartbeats", configuration.getServerId(), String.valueOf(redisTime)); + } catch (JedisConnectionException e) { + // Redis server has disappeared! + getLogger().error("Unable to update heartbeat - did your Redis server go away?", e); + return; + } + try { + serverIds = getCurrentServerIds(true, false); + globalPlayerCount.set(getCurrentCount()); + } catch (Throwable e) { + getLogger().error("Unable to update data - did your Redis server go away?", e); + } + } + }).repeat(3, TimeUnit.SECONDS).schedule(); + dataManager = new VelocityDataManager(this); + getProxy().getEventManager().register(this, new RedisBungeeListener(this, configuration.getExemptAddresses())); + getProxy().getEventManager().register(this, dataManager); + psl = new PubSubListener(this); + getProxy().getScheduler().buildTask(this, psl).schedule(); + integrityCheck = getProxy().getScheduler().buildTask(null,new Runnable() { + @Override + public void run() { + try (Jedis tmpRsc = requestJedis()) { + Set players = getLocalPlayersAsUuidStrings(); + Set playersInRedis = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline"); + List lagged = getCurrentServerIds(false, true); + + // Clean up lagged players. + for (String s : lagged) { + Set laggedPlayers = tmpRsc.smembers("proxy:" + s + ":usersOnline"); + tmpRsc.del("proxy:" + s + ":usersOnline"); + if (!laggedPlayers.isEmpty()) { + getLogger().info("Cleaning up lagged proxy " + s + " (" + laggedPlayers.size() + " players)..."); + for (String laggedPlayer : laggedPlayers) { + RedisUtil.cleanUpPlayer(laggedPlayer, tmpRsc); + } + } + } + + Set absentLocally = new HashSet<>(playersInRedis); + absentLocally.removeAll(players); + Set absentInRedis = new HashSet<>(players); + absentInRedis.removeAll(playersInRedis); + + for (String member : absentLocally) { + boolean found = false; + for (String proxyId : getServerIds()) { + if (proxyId.equals(configuration.getServerId())) continue; + if (tmpRsc.sismember("proxy:" + proxyId + ":usersOnline", member)) { + // Just clean up the set. + found = true; + break; + } + } + if (!found) { + RedisUtil.cleanUpPlayer(member, tmpRsc); + getLogger().warn("Player found in set that was not found locally and globally: " + member); + } else { + tmpRsc.srem("proxy:" + configuration.getServerId() + ":usersOnline", member); + getLogger().warn("Player found in set that was not found locally, but is on another proxy: " + member); + } + } + + Pipeline pipeline = tmpRsc.pipelined(); + + for (String player : absentInRedis) { + // Player not online according to Redis but not BungeeCord. + getLogger().warn("Player " + player + " is on the proxy but not in Redis."); + + Player playerProxied = getProxy().getPlayer(UUID.fromString(player)).orElse(null); + if (playerProxied == null) + continue; // We'll deal with it later. + + RBUtils.createPlayer(playerProxied, pipeline, true); + } + + pipeline.sync(); + } catch (Throwable e) { + getLogger().error("Unable to fix up stored player data", e); + } + } + }).repeat(1, TimeUnit.MINUTES).schedule(); + } + // plugin messages are disabled for now + //getProxy().registerChannel("legacy:redisbungee"); + //getProxy().registerChannel("RedisBungee"); + + // register commands + // those still bungeecord commands will migrate them later. + // if (configuration.doOverrideBungeeCommands()) { + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.GlistCommand(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.FindCommand(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.LastSeenCommand(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.IpCommand(this)); + // + // } + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.SendToAll(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerId(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.ServerIds(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.PlayerProxyCommand(this)); + // getProxy().getPluginManager().registerCommand(this, new RedisBungeeCommands.PlistCommand(this)); + } + + @Override + public void stop() { + if (isJedisAvailable()) { + // Poison the PubSub listener + psl.poison(); + integrityCheck.cancel(); + heartbeatTask.cancel(); + try (Jedis tmpRsc = requestJedis()) { + tmpRsc.hdel("heartbeats", configuration.getServerId()); + if (tmpRsc.scard("proxy:" + configuration.getServerId() + ":usersOnline") > 0) { + Set players = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline"); + for (String member : players) + RedisUtil.cleanUpPlayer(member, tmpRsc); + } + } + try { + this.jedisSummoner.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + this.httpClient.getDispatcher().getExecutorService().shutdown(); + try { + this.httpClient.getDispatcher().getExecutorService().awaitTermination(20, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void loadConfig() throws IOException { + if (!getDataFolder().exists()) { + getDataFolder().mkdir(); + } + + File file = new File(getDataFolder(), "config.yml"); + + if (!file.exists()) { + file.createNewFile(); + try (InputStream in = getResourceAsStream("example_config.yml"); + OutputStream out = Files.newOutputStream(file.toPath())) { + ByteStreams.copy(in, out); + } + } + + final Configuration yamlConfiguration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(file); + + final String redisServer = yamlConfiguration.getString("redis-server", "localhost"); + final int redisPort = yamlConfiguration.getInt("redis-port", 6379); + final boolean useSSL = yamlConfiguration.getBoolean("useSSL", false); + String redisPassword = yamlConfiguration.getString("redis-password", ""); + String serverId = yamlConfiguration.getString("server-id"); + + // check redis password + if (redisPassword != null && (redisPassword.isEmpty() || redisPassword.equals("none"))) { + redisPassword = null; + getLogger().warning("INSECURE setup was detected Please set password for your redis instance."); + } + if (!useSSL) { + getLogger().warning("INSECURE setup was detected Please setup ssl for your redis instance."); + } + // Configuration sanity checks. + if (serverId == null || serverId.isEmpty()) { + /* + * this check causes the config comments to disappear somehow + * I think due snake yaml limitations so as todo: write our own yaml parser? + */ + String genId = UUID.randomUUID().toString(); + getLogger().info("Generated server id " + genId + " and saving it to config."); + yamlConfiguration.set("server-id", genId); + ConfigurationProvider.getProvider(YamlConfiguration.class).save(yamlConfiguration, new File(getDataFolder(), "config.yml")); + getLogger().info("Server id was generated: " + serverId); + } else { + getLogger().info("Loaded server id " + serverId + '.'); + } + this.configuration = new RedisBungeeConfiguration(serverId, yamlConfiguration.getStringList("exempt-ip-addresses"), yamlConfiguration.getBoolean("register-bungee-commands", true)); + + if (redisServer != null && !redisServer.isEmpty()) { + try { + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(yamlConfiguration.getInt("max-redis-connections", 8)); + this.jedisSummoner = new SinglePoolJedisSummoner(new JedisPool(config, redisServer, redisPort, 0, redisPassword, useSSL)); + + } catch (JedisConnectionException e) { + throw new RuntimeException("Unable to create Redis pool", e); + } + + // Test the connection + try (Jedis rsc = requestJedis()) { + rsc.ping(); + // If that worked, now we can check for an existing, alive Bungee: + File crashFile = new File(getDataFolder(), "restarted_from_crash.txt"); + if (crashFile.exists()) { + crashFile.delete(); + } else if (rsc.hexists("heartbeats", serverId)) { + try { + long value = Long.parseLong(rsc.hget("heartbeats", serverId)); + long redisTime = getRedisTime(rsc.time()); + if (redisTime < value + 20) { + getLogger().error("You have launched a possible impostor BungeeCord instance. Another instance is already running."); + getLogger().error("For data consistency reasons, RedisBungee will now disable itself."); + getLogger().error("If this instance is coming up from a crash, create a file in your RedisBungee plugins directory with the name 'restarted_from_crash.txt' and RedisBungee will not perform this check."); + throw new RuntimeException("Possible impostor instance!"); + } + } catch (NumberFormatException ignored) { + } + } + + + httpClient = new OkHttpClient(); + Dispatcher dispatcher = new Dispatcher(Executors.newFixedThreadPool(6)); + httpClient.setDispatcher(dispatcher); + NameFetcher.setHttpClient(httpClient); + UUIDFetcher.setHttpClient(httpClient); + + getLogger().info("Successfully connected to Redis."); + } catch (JedisConnectionException e) { + this.jedisSummoner.close(); + throw e; + } + } else { + throw new RuntimeException("No redis server specified!"); + } + } + + @Subscribe + public void proxyInit(ProxyInitializeEvent event) { + start(); + } + + @Subscribe + public void proxyShutdownEvent(ProxyShutdownEvent event) { + stop(); + } + + + @Override + public Class getPubSubEventClass() { + return PubSubMessageEvent.class; + } + + @Override + public Class getNetworkJoinEventClass() { + return PlayerJoinedNetworkEvent.class; + } + + @Override + public Class getServerChangeEventClass() { + return PlayerChangedServerNetworkEvent.class; + } + + @Override + public Class getNetworkQuitEventClass() { + return PlayerJoinedNetworkEvent.class; + } + + + public ProxyServer getProxy() { + return server; + } + + public Logger getLogger() { + return logger; + } + + public File getDataFolder() { + return this.dataFolder; + } + + public final InputStream getResourceAsStream(String name) { + return getClass().getClassLoader().getResourceAsStream(name); + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java new file mode 100644 index 0000000..544159e --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java @@ -0,0 +1,35 @@ +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.internal.DataManager; +import com.imaginarycode.minecraft.redisbungee.internal.RedisBungeePlugin; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.proxy.Player; + + +public class VelocityDataManager extends DataManager { + + public VelocityDataManager(RedisBungeePlugin plugin) { + super(plugin); + } + + @Override + @Subscribe + public void onPostLogin(PostLoginEvent event) { + invalidate(event.getPlayer().getUniqueId()); + } + + @Override + @Subscribe + public void onPlayerDisconnect(DisconnectEvent event) { + invalidate(event.getPlayer().getUniqueId()); + } + + @Override + @Subscribe + public void onPubSubMessage(PubSubMessageEvent event) { + handlePubSubMessage(event.getChannel(), event.getMessage()); + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java new file mode 100644 index 0000000..899f7b5 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java @@ -0,0 +1,327 @@ +package com.imaginarycode.minecraft.redisbungee.commands; + +import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; + + +/** + * This class contains subclasses that are used for the commands RedisBungee overrides or includes: /glist, /find and /lastseen. + *

+ * All classes use the {@link RedisBungeeAPI}. + * + * @author tuxed + * @since 0.2.3 + */ +public class RedisBungeeCommands { + /* + private static final BaseComponent[] NO_PLAYER_SPECIFIED = + new ComponentBuilder("You must specify a player name.").color(ChatColor.RED).create(); + private static final BaseComponent[] PLAYER_NOT_FOUND = + new ComponentBuilder("No such player found.").color(ChatColor.RED).create(); + private static final BaseComponent[] NO_COMMAND_SPECIFIED = + new ComponentBuilder("You must specify a command to be run.").color(ChatColor.RED).create(); + + private static String playerPlural(int num) { + return num == 1 ? num + " player is" : num + " players are"; + } + + public static class GlistCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public GlistCommand(RedisBungeeVelocityPlugin plugin) { + super("glist", "bungeecord.command.list", "redisbungee", "rglist"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + int count = plugin.getApi().getPlayerCount(); + BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) + .append(playerPlural(count) + " currently online.").create(); + if (args.length > 0 && args[0].equals("showall")) { + Multimap serverToPlayers = plugin.getApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + for (Map.Entry entry : serverToPlayers.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + for (String server : new TreeSet<>(serverToPlayers.keySet())) { + TextComponent serverName = new TextComponent(); + serverName.setColor(ChatColor.GREEN); + serverName.setText("[" + server + "] "); + TextComponent serverCount = new TextComponent(); + serverCount.setColor(ChatColor.YELLOW); + serverCount.setText("(" + serverToPlayers.get(server).size() + "): "); + TextComponent serverPlayers = new TextComponent(); + serverPlayers.setColor(ChatColor.WHITE); + serverPlayers.setText(Joiner.on(", ").join(human.get(server))); + sender.sendMessage(serverName, serverCount, serverPlayers); + } + sender.sendMessage(playersOnline); + } else { + sender.sendMessage(playersOnline); + sender.sendMessage(new ComponentBuilder("To see all players online, use /glist showall.").color(ChatColor.YELLOW).create()); + } + } + }); + } + } + + public static class FindCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public FindCommand(RedisBungeeVelocityPlugin plugin) { + super("find", "bungeecord.command.find", "rfind"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + ServerInfo si = plugin.getProxy().getServerInfo(plugin.getApi().getServerFor(uuid)); + if (si != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " is on " + si.getName() + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class LastSeenCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public LastSeenCommand(RedisBungeeVelocityPlugin plugin) { + super("lastseen", "redisbungee.command.lastseen", "rlastseen"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + long secs = plugin.getApi().getLastOnline(uuid); + TextComponent message = new TextComponent(); + if (secs == 0) { + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is currently online."); + } else if (secs != -1) { + message.setColor(ChatColor.BLUE); + message.setText(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); + } else { + message.setColor(ChatColor.RED); + message.setText(args[0] + " has never been online."); + } + sender.sendMessage(message); + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class IpCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public IpCommand(RedisBungeeVelocityPlugin plugin) { + super("ip", "redisbungee.command.ip", "playerip", "rip", "rplayerip"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + InetAddress ia = plugin.getApi().getPlayerIp(uuid); + if (ia != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is connected from " + ia.toString() + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class PlayerProxyCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public PlayerProxyCommand(RedisBungeeVelocityPlugin plugin) { + super("pproxy", "redisbungee.command.pproxy"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sender.sendMessage(PLAYER_NOT_FOUND); + return; + } + String proxy = plugin.getApi().getProxy(uuid); + if (proxy != null) { + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText(args[0] + " is connected to " + proxy + "."); + sender.sendMessage(message); + } else { + sender.sendMessage(PLAYER_NOT_FOUND); + } + } else { + sender.sendMessage(NO_PLAYER_SPECIFIED); + } + } + }); + } + } + + public static class SendToAll extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public SendToAll(RedisBungeeVelocityPlugin plugin) { + super("sendtoall", "redisbungee.command.sendtoall", "rsendtoall"); + this.plugin = plugin; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (args.length > 0) { + String command = Joiner.on(" ").skipNulls().join(args); + plugin.getApi().sendProxyCommand(command); + TextComponent message = new TextComponent(); + message.setColor(ChatColor.GREEN); + message.setText("Sent the command /" + command + " to all proxies."); + sender.sendMessage(message); + } else { + sender.sendMessage(NO_COMMAND_SPECIFIED); + } + } + } + + public static class ServerId extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public ServerId(RedisBungeeVelocityPlugin plugin) { + super("serverid", "redisbungee.command.serverid", "rserverid"); + this.plugin = plugin; + } + + @Override + public void execute(CommandSender sender, String[] args) { + TextComponent textComponent = new TextComponent(); + textComponent.setText("You are on " + plugin.getApi().getServerId() + "."); + textComponent.setColor(ChatColor.YELLOW); + sender.sendMessage(textComponent); + } + } + + public static class ServerIds extends Command { + private final RedisBungeeVelocityPlugin plugin; + public ServerIds(RedisBungeeVelocityPlugin plugin) { + super("serverids", "redisbungee.command.serverids"); + this.plugin =plugin; + } + + @Override + public void execute(CommandSender sender, String[] strings) { + TextComponent textComponent = new TextComponent(); + textComponent.setText("All server IDs: " + Joiner.on(", ").join(plugin.getApi().getAllServers())); + textComponent.setColor(ChatColor.YELLOW); + sender.sendMessage(textComponent); + } + } + + public static class PlistCommand extends Command { + private final RedisBungeeVelocityPlugin plugin; + + public PlistCommand(RedisBungeeVelocityPlugin plugin) { + super("plist", "redisbungee.command.plist", "rplist"); + this.plugin = plugin; + } + + @Override + public void execute(final CommandSender sender, final String[] args) { + plugin.getProxy().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + String proxy = args.length >= 1 ? args[0] : plugin.getConfiguration().getServerId(); + if (!plugin.getServerIds().contains(proxy)) { + sender.sendMessage(new ComponentBuilder(proxy + " is not a valid proxy. See /serverids for valid proxies.").color(ChatColor.RED).create()); + return; + } + Set players = plugin.getApi().getPlayersOnProxy(proxy); + BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) + .append(playerPlural(players.size()) + " currently on proxy " + proxy + ".").create(); + if (args.length >= 2 && args[1].equals("showall")) { + Multimap serverToPlayers = plugin.getApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + for (Map.Entry entry : serverToPlayers.entries()) { + if (players.contains(entry.getValue())) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + } + for (String server : new TreeSet<>(human.keySet())) { + TextComponent serverName = new TextComponent(); + serverName.setColor(ChatColor.RED); + serverName.setText("[" + server + "] "); + TextComponent serverCount = new TextComponent(); + serverCount.setColor(ChatColor.YELLOW); + serverCount.setText("(" + human.get(server).size() + "): "); + TextComponent serverPlayers = new TextComponent(); + serverPlayers.setColor(ChatColor.WHITE); + serverPlayers.setText(Joiner.on(", ").join(human.get(server))); + sender.sendMessage(serverName, serverCount, serverPlayers); + } + sender.sendMessage(playersOnline); + } else { + sender.sendMessage(playersOnline); + sender.sendMessage(new ComponentBuilder("To see all players online, use /plist " + proxy + " showall.").color(ChatColor.YELLOW).create()); + } + } + }); + } + } + */ + +} \ No newline at end of file diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java new file mode 100644 index 0000000..ab6f1f5 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerChangedServerNetworkEvent.java @@ -0,0 +1,37 @@ +package com.imaginarycode.minecraft.redisbungee.events; + + +import java.util.UUID; + +/** + * This event is sent when a player connects to a new server. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.ServerConnectedEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerChangedServerNetworkEvent { + private final UUID uuid; + private final String previousServer; + private final String server; + + public PlayerChangedServerNetworkEvent(UUID uuid, String previousServer, String server) { + this.uuid = uuid; + this.previousServer = previousServer; + this.server = server; + } + + public UUID getUuid() { + return uuid; + } + + public String getServer() { + return server; + } + + public String getPreviousServer() { + return previousServer; + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java new file mode 100644 index 0000000..0640bdd --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerJoinedNetworkEvent.java @@ -0,0 +1,25 @@ +package com.imaginarycode.minecraft.redisbungee.events; + + +import java.util.UUID; + +/** + * This event is sent when a player joins the network. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.PostLoginEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerJoinedNetworkEvent { + private final UUID uuid; + + public PlayerJoinedNetworkEvent(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUuid() { + return uuid; + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java new file mode 100644 index 0000000..817945b --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PlayerLeftNetworkEvent.java @@ -0,0 +1,25 @@ +package com.imaginarycode.minecraft.redisbungee.events; + + +import java.util.UUID; + +/** + * This event is sent when a player disconnects. RedisBungee sends the event only when + * the proxy the player has been connected to is different than the local proxy. + *

+ * This event corresponds to {@link net.md_5.bungee.api.event.PlayerDisconnectEvent}, and is fired + * asynchronously. + * + * @since 0.3.4 + */ +public class PlayerLeftNetworkEvent { + private final UUID uuid; + + public PlayerLeftNetworkEvent(UUID uuid) { + this.uuid = uuid; + } + + public UUID getUuid() { + return uuid; + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java new file mode 100644 index 0000000..f168fd3 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/events/PubSubMessageEvent.java @@ -0,0 +1,29 @@ +package com.imaginarycode.minecraft.redisbungee.events; + + + +/** + * This event is posted when a PubSub message is received. + *

+ * Warning: This event is fired in a separate thread! + * + * @since 0.2.6 + */ + +public class PubSubMessageEvent { + private final String channel; + private final String message; + + public PubSubMessageEvent(String channel, String message) { + this.channel = channel; + this.message = message; + } + + public String getChannel() { + return channel; + } + + public String getMessage() { + return message; + } +} diff --git a/pom.xml b/pom.xml index c09e505..cde5767 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,7 @@ RedisBungee-API RedisBungee-Bungee + RedisBungee-Velocity