2
0
mirror of https://github.com/proxiodev/RedisBungee.git synced 2024-11-23 04:28:01 +00:00

added Velocity support (NOT TESTED), it will not compile yet due config impl not done yet

This commit is contained in:
mohammed jasem alaajel 2022-07-07 02:24:08 +04:00
parent ea665fd70f
commit bdda99bc81
24 changed files with 3152 additions and 0 deletions

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>RedisBungee</artifactId>
<groupId>com.imaginarycode.minecraft</groupId>
<version>0.8.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>RedisBungee-Bungee</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<source>8</source>
<reportOutputDirectory>../javadoc</reportOutputDirectory>
<destDir>${project.name}</destDir>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<relocations>
<relocation>
<pattern>redis.clients.jedis</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.jedis
</shadedPattern>
</relocation>
<relocation>
<pattern>redis.clients.util</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.jedisutil
</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.pool</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.commonspool
</shadedPattern>
</relocation>
<relocation>
<pattern>com.squareup.okhttp</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.okhttp
</shadedPattern>
</relocation>
<relocation>
<pattern>okio</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.okio
</shadedPattern>
</relocation>
<relocation>
<pattern>com.google</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.google
</shadedPattern>
</relocation>
<relocation>
<pattern>org.json</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.json
</shadedPattern>
</relocation>
<relocation>
<pattern>org.checkerframework</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.checkframework
</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.imaginarycode.minecraft</groupId>
<artifactId>RedisBungee-API</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-api</artifactId>
<version>1.17-R0.1-SNAPSHOT</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<ProxiedPlayer, PostLoginEvent, PlayerDisconnectEvent, PubSubMessageEvent> implements Listener {
public BungeeDataManager(RedisBungeePlugin<ProxiedPlayer> 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());
}
}

View File

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

View File

@ -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<ProxiedPlayer> {
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<String> 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<Object, Multimap<String, UUID>> 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.<String>of(), ImmutableList.<String>of());
return count.intValue();
}
@Override
public Set<String> getLocalPlayersAsUuidStrings() {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
for (ProxiedPlayer player : getProxy().getPlayers()) {
builder.add(player.getUniqueId().toString());
}
return builder.build();
}
@Override
public DataManager<ProxiedPlayer, ?, ?, ?> getDataManager() {
return this.dataManager;
}
@Override
public Set<UUID> getPlayers() {
ImmutableSet.Builder<UUID> setBuilder = ImmutableSet.builder();
if (isJedisAvailable()) {
try (Jedis rsc = requestJedis()) {
List<String> keys = new ArrayList<>();
for (String i : getServerIds()) {
keys.add("proxy:" + i + ":usersOnline");
}
if (!keys.isEmpty()) {
Set<String> 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<String, UUID> serversToPlayers() {
try {
return serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, new Callable<Multimap<String, UUID>>() {
@Override
public Multimap<String, UUID> call() throws Exception {
Collection<String> data = (Collection<String>) serverToPlayersScript.eval(ImmutableList.<String>of(), getServerIds());
ImmutableMultimap.Builder<String, UUID> 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<UUID> getPlayersOnProxy(String proxyId) {
checkArgument(getServerIds().contains(proxyId), proxyId + " is not a valid proxy ID");
try (Jedis jedis = requestJedis()) {
Set<String> users = jedis.smembers("proxy:" + proxyId + ":usersOnline");
ImmutableSet.Builder<UUID> 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<String> getServerIds() {
return serverIds;
}
@Override
public List<String> 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<String> servers = ImmutableList.builder();
Map<String, String> heartbeats = jedis.hgetAll("heartbeats");
for (Map.Entry<String, String> 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<String> 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 <section>
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<String> players = getLocalPlayersAsUuidStrings();
Set<String> playersInRedis = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline");
List<String> lagged = getCurrentServerIds(false, true);
// Clean up lagged players.
for (String s : lagged) {
Set<String> 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<String> absentLocally = new HashSet<>(playersInRedis);
absentLocally.removeAll(players);
Set<String> 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<String> 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;
}
}

View File

@ -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<String> 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<String> getPermissions() {
return Collections.emptySet();
}
}

View File

@ -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<LoginEvent, PostLoginEvent, PlayerDisconnectEvent, ServerConnectedEvent, ProxyPingEvent, PluginMessageEvent, PubSubMessageEvent> implements Listener {
public RedisBungeeListener(RedisBungeePlugin<?> plugin, List<InetAddress> exemptAddresses) {
super(plugin, exemptAddresses);
}
@Override
@EventHandler
public void onLogin(LoginEvent event) {
event.registerIntent((Plugin) plugin);
plugin.executeAsync(new RedisCallable<Void>(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<Void>(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<Void>(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<Void>(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<UUID> 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<String> 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<String, UUID> 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<String, String> human = HashMultimap.create();
for (Map.Entry<String, UUID> 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);
}
}
}

View File

@ -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.
* <p>
* 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<String, UUID> serverToPlayers = plugin.getApi().getServerToPlayers();
Multimap<String, String> human = HashMultimap.create();
for (Map.Entry<String, UUID> 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<UUID> 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<String, UUID> serverToPlayers = plugin.getApi().getServerToPlayers();
Multimap<String, String> human = HashMultimap.create();
for (Map.Entry<String, UUID> 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());
}
}
});
}
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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.
* <p>
* <strong>Warning</strong>: 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;
}
}

View File

@ -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"]

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>RedisBungee</artifactId>
<groupId>com.imaginarycode.minecraft</groupId>
<version>0.8.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>RedisBungee-Velocity</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>papermc-repo</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<source>8</source>
<reportOutputDirectory>../javadoc</reportOutputDirectory>
<destDir>${project.name}</destDir>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<relocations>
<relocation>
<pattern>redis.clients.jedis</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.jedis
</shadedPattern>
</relocation>
<relocation>
<pattern>redis.clients.util</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.jedisutil
</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.pool</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.commonspool
</shadedPattern>
</relocation>
<relocation>
<pattern>com.squareup.okhttp</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.okhttp
</shadedPattern>
</relocation>
<relocation>
<pattern>okio</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.okio
</shadedPattern>
</relocation>
<relocation>
<pattern>com.google</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.google
</shadedPattern>
</relocation>
<relocation>
<pattern>org.json</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.json
</shadedPattern>
</relocation>
<relocation>
<pattern>org.checkerframework</pattern>
<shadedPattern>com.imaginarycode.minecraft.redisbungee.internal.checkframework
</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.imaginarycode.minecraft</groupId>
<artifactId>RedisBungee-API</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.1.0</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<ServerConnection> server = player.getCurrentServer();
server.ifPresent(serverConnection -> pipeline.hset("player:" + player.getUniqueId().toString(), "server", serverConnection.getServerInfo().getName()));
Map<String, String> 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()))));
}
}
}

View File

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

View File

@ -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<LoginEvent, PostLoginEvent, DisconnectEvent, ServerConnectedEvent, ProxyPingEvent, PluginMessageEvent, PubSubMessageEvent, Continuation> {
public RedisBungeeListener(RedisBungeePlugin<?> plugin, List<InetAddress> exemptAddresses) {
super(plugin, exemptAddresses);
}
@Override
@Subscribe
public void onLogin(LoginEvent event, Continuation continuation) {
plugin.executeAsync(new RedisCallable<Void>(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<Void>(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<Void>(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<ServerConnection> optionalServerConnection = event.getPlayer().getCurrentServer();
final String currentServer = optionalServerConnection.map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null);
plugin.executeAsync(new RedisCallable<Void>(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<ServerPing.SamplePlayer> 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);
}
}
}

View File

@ -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<Player> {
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<String> 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<Object, Multimap<String, UUID>> 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.<String>of(), ImmutableList.<String>of());
return count.intValue();
}
@Override
public Set<String> getLocalPlayersAsUuidStrings() {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
for (Player player : getProxy().getAllPlayers()) {
builder.add(player.getUniqueId().toString());
}
return builder.build();
}
@Override
public DataManager<Player, ?, ?, ?> getDataManager() {
return this.dataManager;
}
@Override
public Set<UUID> getPlayers() {
ImmutableSet.Builder<UUID> setBuilder = ImmutableSet.builder();
if (isJedisAvailable()) {
try (Jedis rsc = requestJedis()) {
List<String> keys = new ArrayList<>();
for (String i : getServerIds()) {
keys.add("proxy:" + i + ":usersOnline");
}
if (!keys.isEmpty()) {
Set<String> 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<String, UUID> serversToPlayers() {
try {
return serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, new Callable<Multimap<String, UUID>>() {
@Override
public Multimap<String, UUID> call() throws Exception {
Collection<String> data = (Collection<String>) serverToPlayersScript.eval(ImmutableList.<String>of(), getServerIds());
ImmutableMultimap.Builder<String, UUID> 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<UUID> getPlayersOnProxy(String proxyId) {
checkArgument(getServerIds().contains(proxyId), proxyId + " is not a valid proxy ID");
try (Jedis jedis = requestJedis()) {
Set<String> users = jedis.smembers("proxy:" + proxyId + ":usersOnline");
ImmutableSet.Builder<UUID> 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<String> getServerIds() {
return serverIds;
}
@Override
public List<String> 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<String> servers = ImmutableList.builder();
Map<String, String> heartbeats = jedis.hgetAll("heartbeats");
for (Map.Entry<String, String> 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<String> 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 <section>
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<String> players = getLocalPlayersAsUuidStrings();
Set<String> playersInRedis = tmpRsc.smembers("proxy:" + configuration.getServerId() + ":usersOnline");
List<String> lagged = getCurrentServerIds(false, true);
// Clean up lagged players.
for (String s : lagged) {
Set<String> 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<String> absentLocally = new HashSet<>(playersInRedis);
absentLocally.removeAll(players);
Set<String> 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<String> 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);
}
}

View File

@ -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<Player, PostLoginEvent, DisconnectEvent, PubSubMessageEvent> {
public VelocityDataManager(RedisBungeePlugin<Player> 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());
}
}

View File

@ -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.
* <p>
* 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<String, UUID> serverToPlayers = plugin.getApi().getServerToPlayers();
Multimap<String, String> human = HashMultimap.create();
for (Map.Entry<String, UUID> 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<UUID> 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<String, UUID> serverToPlayers = plugin.getApi().getServerToPlayers();
Multimap<String, String> human = HashMultimap.create();
for (Map.Entry<String, UUID> 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());
}
}
});
}
}
*/
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -0,0 +1,29 @@
package com.imaginarycode.minecraft.redisbungee.events;
/**
* This event is posted when a PubSub message is received.
* <p>
* <strong>Warning</strong>: 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;
}
}

View File

@ -23,6 +23,7 @@
<modules> <modules>
<module>RedisBungee-API</module> <module>RedisBungee-API</module>
<module>RedisBungee-Bungee</module> <module>RedisBungee-Bungee</module>
<module>RedisBungee-Velocity</module>
</modules> </modules>
<dependencies> <dependencies>