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.api.AbstractRedisBungeeListener; import com.imaginarycode.minecraft.redisbungee.api.GenericPlayerUtils; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; import com.imaginarycode.minecraft.redisbungee.api.util.payload.PayloadUtils; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; 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.connection.PluginMessageEvent.ForwardResult; 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.serializer.legacy.LegacyComponentSerializer; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.Pipeline; import java.net.InetAddress; import java.util.*; import java.util.stream.Collectors; public class RedisBungeeVelocityListener extends AbstractRedisBungeeListener { // Some messages are using legacy characters private final LegacyComponentSerializer serializer = LegacyComponentSerializer.legacySection(); public RedisBungeeVelocityListener(RedisBungeePlugin plugin, List exemptAddresses) { super(plugin, exemptAddresses); } @Subscribe public void onLogin(LoginEvent event, Continuation continuation) { plugin.executeAsync(new RedisTask(plugin) { @Override public Void jedisTask(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(serializer.deserialize(ONLINE_MODE_RECONNECT))); return null; } } for (String s : plugin.getProxiesIds()) { if (jedis.sismember("proxy:" + s + ":usersOnline", event.getPlayer().getUniqueId().toString())) { event.setResult(ResultedEvent.ComponentResult.denied(serializer.deserialize(ALREADY_LOGGED_IN))); return null; } } return null; } finally { continuation.resume(); } } @Override public Void clusterJedisTask(JedisCluster jedisCluster) { 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(serializer.deserialize(ONLINE_MODE_RECONNECT))); return null; } } for (String s : plugin.getProxiesIds()) { if (jedisCluster.sismember("proxy:" + s + ":usersOnline", event.getPlayer().getUniqueId().toString())) { event.setResult(ResultedEvent.ComponentResult.denied(serializer.deserialize(ALREADY_LOGGED_IN))); return null; } } return null; } finally { continuation.resume(); } } }); } @Override @Subscribe public void onPostLogin(PostLoginEvent event) { plugin.executeAsync(new RedisTask(plugin) { @Override public Void jedisTask(Jedis jedis) { Pipeline pipeline = jedis.pipelined(); plugin.getUuidTranslator().persistInfo(event.getPlayer().getUsername(), event.getPlayer().getUniqueId(), pipeline); VelocityPlayerUtils.createPlayer(event.getPlayer(), pipeline, true); pipeline.sync(); return null; } @Override public Void clusterJedisTask(JedisCluster jedisCluster) { plugin.getUuidTranslator().persistInfo(event.getPlayer().getUsername(), event.getPlayer().getUniqueId(), jedisCluster); VelocityPlayerUtils.createPlayer(event.getPlayer(), jedisCluster, true); return null; } }); } @Override @Subscribe public void onPlayerDisconnect(DisconnectEvent event) { plugin.executeAsync(new RedisTask(plugin) { @Override public Void jedisTask(Jedis jedis) { Pipeline pipeline = jedis.pipelined(); GenericPlayerUtils.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), pipeline, true); pipeline.sync(); return null; } @Override public Void clusterJedisTask(JedisCluster jedisCluster) { GenericPlayerUtils.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), jedisCluster, true); return null; } }); } @Override @Subscribe public void onServerChange(ServerConnectedEvent event) { final String currentServer = event.getServer().getServerInfo().getName(); final String oldServer = event.getPreviousServer().map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null); plugin.executeAsync(new RedisTask(plugin) { @Override public Void jedisTask(Jedis jedis) { jedis.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", currentServer); PayloadUtils.playerServerChangePayload(event.getPlayer().getUniqueId(), jedis, currentServer, oldServer); return null; } @Override public Void clusterJedisTask(JedisCluster jedisCluster) { jedisCluster.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", currentServer); PayloadUtils.playerServerChangePayload(event.getPlayer().getUniqueId(), jedisCluster, currentServer, oldServer); return null; } }); } @Override @Subscribe(order = PostOrder.EARLY) public void onPing(ProxyPingEvent event) { if (exemptAddresses.contains(event.getConnection().getRemoteAddress().getAddress())) { return; } ServerPing.Builder ping = event.getPing().asBuilder(); ping.onlinePlayers(plugin.getCount()); event.setPing(ping.build()); } @Override @Subscribe public void onPluginMessage(PluginMessageEvent event) { if(!(event.getSource() instanceof ServerConnection) || !RedisBungeeVelocityPlugin.IDENTIFIERS.contains(event.getIdentifier())) { return; } event.setResult(ForwardResult.handled()); plugin.executeAsync(() -> { ByteArrayDataInput in = event.dataAsDataStream(); 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 = original.stream() .map(uuid -> plugin.getUuidTranslator().getNameFromUuid(uuid, false)) .collect(Collectors.toSet()); 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(Objects.requireNonNull(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().getProxyId()); break; case "PlayerProxy": String username = in.readUTF(); out.writeUTF("PlayerProxy"); out.writeUTF(username); out.writeUTF(plugin.getApi().getProxy(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(username, true)))); break; default: return; } ((ServerConnection) event.getSource()).sendPluginMessage(event.getIdentifier(), out.toByteArray()); }); } @Override @Subscribe public void onPubSubMessage(PubSubMessageEvent event) { if (event.getChannel().equals("redisbungee-allservers") || event.getChannel().equals("redisbungee-" + plugin.getApi().getProxyId())) { 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); } } }