From 1593c2d628d9c254ff5c0b98a13e820c385c101b Mon Sep 17 00:00:00 2001 From: mohammed Alteneiji Date: Sun, 28 Apr 2024 15:29:53 +0400 Subject: [PATCH] 0.12.0 (#86) ## NOTES data system shouldn't effect anybody, unless you do any direct query to Redis query data, you should adapt the changes, by viewing classes `ProxyDataManager` and `PlayerDataManager` # Changes * RedisBungee is compiled with `java 17` now, Due java 11 support is ending at end of September * config version is now `2` which will reset your config if older version * Adventure API is included inside RedisBungee API * new Language infrastructure for RedisBungee built-in messages #85 *commands not included yet* * New data system which replaces Redis PubSub with Redis Streams *see below* * Ability to connect player to last server they where on using an config option #84 * new environment variable `REDISBUNGEE_PROXY_ID` which can be set before launch * new environment variable `REDISBUNGEE_NETWORK_ID` which can be set before launch * RedisBungee requires redis version 6.2 or above #88 * Better command system https://github.com/ProxioDev/RedisBungee/issues/93 ## New data system Due limitation of Redis PubSub in Cluster environment, Internals of RedisBungee were changed to support Redis Streams - Network Ids - networks ids used to group network proxies - example having 'test' network and 'main' network - Networks in the same redis server / cluster share the same UUID cache - Heartbeat system: - RedisBungee old heartbeat system used hastset on redisbungee to store the current unix time of the proxy to check what every proxy died or not, now instead we publish the heartbeat using unix time, and online count to proxy which proxy store it in their memory, which allow the `get number of online players` to be faster than pooling whole list in old data system. - PubSub - since redisbungee was initially designed with pubsub in mind, registration no longer required now for event to fire, see the api changes below. ## Commands System * rewritten using [acf lib](https://github.com/aikar/commands) to be platform independent * new command `/rb` or `/redisbungee` with sub commands `help`, `info`, 'clean', 'show'. * 'rb' * '/rb' and '/rb info' ![image](https://github.com/ProxioDev/RedisBungee/assets/34905970/70796ab0-b5fd-4578-8c93-c976e517df95) * '/rb show' ![image](https://github.com/ProxioDev/RedisBungee/assets/34905970/56332c37-701f-43e0-946b-6894b845fab3) * configuration to disable or override each command from legacy to new introduced one `/rb` ```yaml # For redis bungee legacy commands # either can be run using '/rbl glist' for example # or if 'install' is set to true '/glist' can be used. # 'install' also overrides the proxy installed commands # # In legacy commands each command got it own permissions since they had it own permission pre new command system, # so it's also applied to subcommands in '/rbl'. commands: # Permission redisbungee.legacy.use redisbungee-legacy: enabled: false subcommands: # Permission redisbungee.command.glist glist: enabled: false install: false # Permission redisbungee.command.find find: enabled: false install: false # Permission redisbungee.command.lastseen lastseen: enabled: false install: false # Permission redisbungee.command.ip ip: enabled: false install: false # Permission redisbungee.command.pproxy pproxy: enabled: false install: false # Permission redisbungee.command.sendtoall sendtoall: enabled: false install: false # Permission redisbungee.command.serverid serverid: enabled: false install: false # Permission redisbungee.command.serverids serverids: enabled: false install: false # Permission redisbungee.command.plist plist: enabled: false install: false # Permission redisbungee.command.use redisbungee: enabled: true ``` ## API changes - Kick api Deprecated: - `kickPlayer(String playerName, String message) ` - `kickPlayer(UUID playerUUID, String message) ` - newer where added using adventure api: - `kickPlayer(String playerName, Component message) ` - `kickPlayer(UUID playerUUID, Component message) ` - PubSub registration api Deprecated: ```java /** * Register (a) PubSub channel(s), so that you may handle PubSubMessageEvent for it. * * @param channels the channels to register * @since 0.3 * @deprecated No longer required */ @Deprecated public final void registerPubSubChannels(String... channels) { } /** * Unregister (a) PubSub channel(s). * * @param channels the channels to unregister * @since 0.3 * @deprecated No longer required */ @Deprecated public final void unregisterPubSubChannels(String... channels) { } ``` # Contributors * `summoncraft.us` for running this branch in production * @SrBedrock for providing [Brazilian Portuguese](https://en.wikipedia.org/wiki/Brazilian_Portuguese) translation #87 # issues closes #84 closes #88 closes #92 closes #81 closes #93 --------- Signed-off-by: mohammed jasem alaajel Co-authored-by: ThiagoROX <51332006+SrBedrock@users.noreply.github.com> --- README.md | 186 +------- RedisBungee-API/build.gradle.kts | 49 +-- .../redisbungee/AbstractRedisBungeeAPI.java | 100 +++-- .../redisbungee/api/AbstractDataManager.java | 310 -------------- .../api/AbstractRedisBungeeListener.java | 50 --- .../redisbungee/api/JedisPubSubHandler.java | 36 -- .../redisbungee/api/PlayerDataManager.java | 271 ++++++++++++ .../redisbungee/api/ProxyDataManager.java | 399 ++++++++++++++++++ .../redisbungee/api/PubSubListener.java | 71 ---- .../redisbungee/api/RedisBungeePlugin.java | 243 ++--------- .../api/config/LangConfiguration.java | 155 +++++++ .../api/config/RedisBungeeConfiguration.java | 67 +-- .../config/{ => loaders}/ConfigLoader.java | 148 ++++--- .../config/loaders/GenericConfigLoader.java | 58 +++ .../api/config/loaders/LangConfigLoader.java | 55 +++ .../api/events/EventsPlatform.java | 1 - .../api/payloads/AbstractPayload.java | 24 ++ .../gson/AbstractPayloadSerializer.java | 34 ++ .../api/payloads/proxy/DeathPayload.java | 19 + .../api/payloads/proxy/HeartbeatPayload.java | 31 ++ .../api/payloads/proxy/PubSubPayload.java | 34 ++ .../api/payloads/proxy/RunCommandPayload.java | 36 ++ .../proxy/gson/DeathPayloadSerializer.java | 36 ++ .../gson/HeartbeatPayloadSerializer.java | 38 ++ .../proxy/gson/PubSubPayloadSerializer.java | 40 ++ .../gson/RunCommandPayloadSerializer.java | 38 ++ .../api/summoners/JedisClusterSummoner.java | 6 +- .../summoners/NotClosableJedisCluster.java | 2 - .../redisbungee/api/summoners/Summoner.java | 5 +- .../redisbungee/api/tasks/HeartbeatTask.java | 57 --- .../redisbungee/api/tasks/InitialUtils.java | 86 ---- .../api/tasks/IntegrityCheckTask.java | 91 ---- .../api/tasks/RedisPipelineTask.java | 49 +++ .../redisbungee/api/tasks/RedisTask.java | 27 +- .../redisbungee/api/tasks/ShutdownUtils.java | 39 -- .../api/tasks/UUIDCleanupTask.java | 56 +++ .../redisbungee/api/util/InitialUtils.java | 48 +++ .../redisbungee/api/util/RedisUtil.java | 9 +- .../redisbungee/api/util/io/IOUtil.java | 20 - .../api/util/payload/PayloadUtils.java | 41 -- .../api/util/player/PlayerUtils.java | 57 --- ...ations.java => MultiMapSerialization.java} | 3 +- .../api/util/uuid/NameFetcher.java | 58 +-- .../api/util/uuid/UUIDTranslator.java | 6 +- RedisBungee-API/src/main/resources/config.yml | 120 ++++-- RedisBungee-API/src/main/resources/lang.yml | 55 +++ .../src/main/resources/messages.yml | 2 - RedisBungee-Bungee/build.gradle.kts | 23 +- .../BungeeCommandPlatformHelper.java | 17 + .../redisbungee/BungeeDataManager.java | 58 --- .../redisbungee/BungeePlayerDataManager.java | 97 +++++ .../redisbungee/BungeePlayerUtils.java | 28 -- .../minecraft/redisbungee/RedisBungee.java | 276 ++++++------ .../RedisBungeeBungeeListener.java | 252 ----------- .../redisbungee/RedisBungeeListener.java | 168 ++++++++ .../commands/RedisBungeeCommands.java | 353 ---------------- RedisBungee-Commands/build.gradle.kts | 24 ++ .../redisbungee/commands/CommandLoader.java | 31 ++ .../commands/CommandRedisBungee.java | 189 +++++++++ .../commands/legacy/CommandFind.java | 34 ++ .../commands/legacy/CommandGList.java | 34 ++ .../commands/legacy/CommandIp.java | 34 ++ .../commands/legacy/CommandLastSeen.java | 34 ++ .../commands/legacy/CommandPProxy.java | 33 ++ .../commands/legacy/CommandPlist.java | 35 ++ .../commands/legacy/CommandSendToAll.java | 33 ++ .../commands/legacy/CommandServerId.java | 33 ++ .../commands/legacy/CommandServerIds.java | 36 ++ .../legacy/LegacyRedisBungeeCommands.java | 260 ++++++++++++ .../commands/utils/AdventureBaseCommand.java | 26 ++ .../commands/utils/CommandPlatformHelper.java | 36 ++ .../utils/StopperUUIDCleanupTask.java | 25 ++ RedisBungee-Velocity/build.gradle.kts | 24 +- .../minecraft/redisbungee/RedisBungeeAPI.java | 2 +- .../redisbungee/RedisBungeeListener.java | 173 ++++++++ .../RedisBungeeVelocityListener.java | 263 ------------ .../RedisBungeeVelocityPlugin.java | 228 +++++----- .../VelocityCommandPlatformHelper.java | 26 ++ .../redisbungee/VelocityDataManager.java | 61 --- .../VelocityPlayerDataManager.java | 97 +++++ .../redisbungee/VelocityPlayerUtils.java | 31 -- .../commands/RedisBungeeCommands.java | 362 ---------------- gradle.build.kts => build.gradle.kts | 0 gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 22 +- settings.gradle.kts | 65 +++ 88 files changed, 3715 insertions(+), 3181 deletions(-) delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractDataManager.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractRedisBungeeListener.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/JedisPubSubHandler.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PlayerDataManager.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/ProxyDataManager.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PubSubListener.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/LangConfiguration.java rename RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/{ => loaders}/ConfigLoader.java (54%) create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/GenericConfigLoader.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/LangConfigLoader.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/AbstractPayload.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/gson/AbstractPayloadSerializer.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/DeathPayload.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/HeartbeatPayload.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/PubSubPayload.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/RunCommandPayload.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/DeathPayloadSerializer.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/HeartbeatPayloadSerializer.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/PubSubPayloadSerializer.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/RunCommandPayloadSerializer.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/HeartbeatTask.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/InitialUtils.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/IntegrityCheckTask.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisPipelineTask.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/ShutdownUtils.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/UUIDCleanupTask.java create mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/InitialUtils.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/io/IOUtil.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/payload/PayloadUtils.java delete mode 100644 RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/player/PlayerUtils.java rename RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/{Serializations.java => MultiMapSerialization.java} (97%) create mode 100644 RedisBungee-API/src/main/resources/lang.yml delete mode 100644 RedisBungee-API/src/main/resources/messages.yml create mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeCommandPlatformHelper.java delete mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java create mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerDataManager.java delete mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerUtils.java delete mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeBungeeListener.java create mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java delete mode 100644 RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java create mode 100644 RedisBungee-Commands/build.gradle.kts create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandLoader.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandRedisBungee.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandFind.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandGList.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandIp.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandLastSeen.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPProxy.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPlist.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandSendToAll.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerId.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerIds.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/LegacyRedisBungeeCommands.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/AdventureBaseCommand.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/CommandPlatformHelper.java create mode 100644 RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/StopperUUIDCleanupTask.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java delete mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityListener.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityCommandPlatformHelper.java delete mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java create mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerDataManager.java delete mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerUtils.java delete mode 100644 RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java rename gradle.build.kts => build.gradle.kts (100%) diff --git a/README.md b/README.md index c9f1789..185da32 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,14 @@ -# RedisBungee fork By Limework - -*if you are here for transferring players to another proxy when the first proxy crashes or whatever this plugin won't do it, tell mojang to implement transfer packet*. -[Click here, for more information about transfer packet](https://hypixel.net/threads/why-do-we-need-transfer-packets.1390307/) +# RedisBungee Limework's Fork The original project of RedisBungee is no longer maintained, so we have forked the plugin. -RedisBungee uses [Redis](https://redis.io) with Java client [Jedis](https://github.com/redis/jedis/) -to Synchronize players data between [BungeeCord](https://github.com/SpigotMC/BungeeCord) or [Velocity*](https://github.com/PaperMC/Velocity) proxies +RedisBungee uses [Redis](https://redis.io) with Java client [Jedis](https://github.com/redis/jedis/) +to Synchronize players data between [BungeeCord](https://github.com/SpigotMC/BungeeCord) +or [Velocity*](https://github.com/PaperMC/Velocity) proxies -Velocity*: *version 3.1.2 or above is only supported, any version below that might work but might be unstable* [#40](https://github.com/ProxioDev/RedisBungee/pull/40) +## Downloads -## Downloads [![](https://raw.githubusercontent.com/Prospector/badges/master/modrinth-badge-72h-padded.png)](https://modrinth.com/plugin/redisbungee) -or from github releases - -https://github.com/ProxioDev/RedisBungee/releases - -## notes -If you are looking to use Original RedisBungee without a change to internals, -with critical bugs fixed, please use version [0.6.5](https://github.com/ProxioDev/RedisBungee/releases/tag/0.6.5) and java docs For legacy Version [0.6.5](https://proxiodev.github.io/RedisBungee-JavaDocs/0.6.5-SNAPSHOT/) -as its last version before internal changes. please note that you will not get support for any old builds unless critical bugs effecting both 0.6.5 and 0.7.0 or above. - -SpigotMC resource page: [click](https://www.spigotmc.org/resources/redisbungee.87700/) -## Supported Redis versions -| Redis version | Supported | -|:-------------:|:---------:| -| 1.x.x | ✖ | -| 2.x.x | ✖ | -| 3.x.x | ✔ | -| 4.x.x | ✔ | -| 5.x.x | ✔ | -| 6.x.x | ✔ | -| 7.x.x | ✔ | - - -## Implementing RedisBungee in your plugin: [![RedisBungee Build](https://github.com/proxiodev/RedisBungee/actions/workflows/maven.yml/badge.svg)](https://github.com/Limework/RedisBungee/actions/workflows/maven.yml) [![](https://jitpack.io/v/ProxioDev/redisbungee.svg)](https://jitpack.io/#ProxioDev/redisbungee) - -RedisBungee is distributed as a [Gradle](https://gradle.org/) project. - -By using jitpack [![](https://jitpack.io/v/ProxioDev/redisbungee.svg)](https://jitpack.io/#ProxioDev/redisbungee) - -# Setup jitpack repository -## maven -```xml - - - jitpack.io - https://jitpack.io - - -``` -## gradle (kotlin dsl) -```kotlin -repositories { - maven("https://jitpack.io/") -} -``` - -# [BungeeCord](https://github.com/SpigotMC/BungeeCord) -add this in your project dependencies -## maven -```xml - - com.github.proxiodev.redisbungee - RedisBungee-Bungee - VERSION - provided - - - -``` -## gradle (kotlin dsl) -``` -implementation("com.github.ProxioDev.redisbungee:RedisBungee-Bungee:0.11.0") - -// USE THIS IF YOU WANT TO USE INCLUDED JEDIS LIB BECAUSE OF RELOACTION AND REMOVE THE ABOVE STATEMENT -implementation("com.github.ProxioDev.redisbungee:RedisBungee-Bungee:0.11.0:all") { - exclude("redis.clients", "jedis") -} -``` -then in your project plugin.yml add `RedisBungee` to `depends` like this -```yaml -name: "yourplugin" -main: your.main.class -version: 1.0.0-SNAPSHOT -author: idk -depends: [ RedisBungee ] -``` - - -## [Velocity](https://github.com/PaperMC/Velocity) -## maven -```xml - - com.github.proxiodev.redisbungee - RedisBungee-Velocity - VERSION - provided - - -``` -## gradle (kotlin dsl) -``` -implementation("com.github.ProxioDev.redisbungee:RedisBungee-Velocity:0.11.0") - -// USE THIS IF YOU WANT TO USE INCLUDED JEDIS LIB BECAUSE OF RELOACTION AND REMOVE THE ABOVE STATEMENT -implementation("com.github.ProxioDev.redisbungee:RedisBungee-Velocity:0.11.0:all") { - exclude("redis.clients", "jedis") -} -``` - -then to make your plugin depends on RedisBungee, make sure your plugin class Annotation have `@Dependency(id = "redisbungee")` like this -```java -@Plugin( - id = "myplugin", - name = "My Plugin", - version = "0.1.0-beta", - dependencies = { - @Dependency(id = "redisbungee") - } -) -public class PluginMainClass { - -} -``` -## Getting the latest commits to your code -If you want to use the latest commits without waiting for releases. -first, install it to your maven local repo -```bash -git clone https://github.com/ProxioDev/RedisBungee.git -cd RedisBungee -./gradlew publishToMavenLocal -``` -then use any of these in your project. -```xml - - com.imaginarycode.minecraft - RedisBungee-Bungee - VERSION - provided - - -``` -```xml - - com.imaginarycode.minecraft - RedisBungee-Velocity - VERSION - provided - - -``` -## Javadocs - -* API: https://ci.limework.net/RedisBungee/RedisBungee-API/build/docs/javadoc/ -* Velocity: https://ci.limework.net/RedisBungee/RedisBungee-Velocity/build/docs/javadoc/ -* Bungeecord: https://ci.limework.net/RedisBungee/RedisBungee-Bungee/build/docs/javadoc/ - -## Configuration - -**REDISBUNGEE REQUIRES A REDIS SERVER**, preferably with reasonably low latency. The default [config](https://github.com/ProxioDev/RedisBungee/blob/develop/RedisBungee-API/src/main/resources/config.yml) is saved when the plugin first starts. - - -## compatibility with original RedisBungee in Bungeecord ecosystem -This fork ensures compatibility with old plugins, so it should work as drop replacement, -but since Api has been split from the platform there some changes that have to be done, so your plugin might not work if: - -* there is none at the moment, please report any findings at the issue page. - -Cluster mode compatibility in version 0.8.0: - -If you are using static legacy method `RedisBungee#getPool()` it might fail in: -* if Cluster mode is enabled, due fact its Uses different classes -* if JedisPool compatibility mode is disabled in the config due fact project internally switched to JedisPooled than Jedis - ## Support open an issue with question button @@ -184,12 +19,7 @@ This project is distributed under Eclipse Public License 1.0 You can find it [here](https://github.com/proxiodev/RedisBungee/blob/master/LICENSE) -You can find the original RedisBungee is by [astei](https://github.com/astei) and project can be found [here](https://github.com/minecrafter/RedisBungee) or spigot page [here, but its no longer available](https://www.spigotmc.org/resources/redisbungee.13494/) +You can find the original RedisBungee is by [astei](https://github.com/astei) and project can be +found [here](https://github.com/minecrafter/RedisBungee) or spigot +page [here, but its no longer available](https://www.spigotmc.org/resources/redisbungee.13494/) - - -## YourKit - -YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. YourKit is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler/), [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/) and [YourKit YouMonitor](https://www.yourkit.com/youmonitor/). - -![YourKit](https://www.yourkit.com/images/yklogo.png) diff --git a/RedisBungee-API/build.gradle.kts b/RedisBungee-API/build.gradle.kts index 3af847e..fbd915e 100644 --- a/RedisBungee-API/build.gradle.kts +++ b/RedisBungee-API/build.gradle.kts @@ -1,3 +1,4 @@ +import java.time.Instant import java.io.ByteArrayOutputStream plugins { @@ -7,32 +8,25 @@ plugins { } -repositories { - mavenCentral() -} - - -val jedisVersion = "5.1.2" -val configurateVersion = "3.7.3" -val guavaVersion = "31.1-jre" - - dependencies { - api("com.google.guava:guava:$guavaVersion") - api("redis.clients:jedis:$jedisVersion") - api("com.squareup.okhttp:okhttp:2.7.5") - api("org.spongepowered:configurate-yaml:$configurateVersion") - - // tests - testImplementation("junit:junit:4.13.2") + api(libs.guava) + api(libs.jedis) + api(libs.okhttp) + api(libs.configurate) + api(libs.caffeine) + api(libs.adventure.api) + api(libs.adventure.gson) + api(libs.adventure.legacy) + api(libs.adventure.plain) + api(libs.adventure.miniMessage) } -description = "RedisBungee interafaces" +description = "RedisBungee interfaces" blossom { replaceToken("@version@", "$version") // GIT - var commit: String = "" + val commit: String; val commitStdout = ByteArrayOutputStream() rootProject.exec { standardOutput = commitStdout @@ -43,6 +37,7 @@ blossom { replaceToken("@git_commit@", commit) } + java { withJavadocJar() withSourcesJar() @@ -54,25 +49,27 @@ tasks { val options = options as StandardJavadocDocletOptions options.use() options.isDocFilesSubDirs = true + val jedisVersion = libs.jedis.get().version + val configurateVersion = libs.configurate.get().version + val guavaVersion = libs.guava.get().version + val adventureVersion = libs.guava.get().version options.links( "https://configurate.aoeu.xyz/$configurateVersion/apidocs/", // configurate "https://javadoc.io/doc/redis.clients/jedis/$jedisVersion/", // jedis - "https://guava.dev/releases/$guavaVersion/api/docs/" // guava + "https://guava.dev/releases/$guavaVersion/api/docs/", // guava + "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine", + "https://jd.advntr.dev/api/$adventureVersion" + ) } - test { - useJUnitPlatform() - } - compileJava { options.encoding = Charsets.UTF_8.name() - options.release.set(8) + options.release.set(17) } javadoc { options.encoding = Charsets.UTF_8.name() - } processResources { filteringCharset = Charsets.UTF_8.name() diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/AbstractRedisBungeeAPI.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/AbstractRedisBungeeAPI.java index ccbb95b..690fe5b 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/AbstractRedisBungeeAPI.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/AbstractRedisBungeeAPI.java @@ -10,8 +10,6 @@ package com.imaginarycode.minecraft.redisbungee; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; @@ -19,6 +17,7 @@ import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import redis.clients.jedis.Jedis; @@ -40,21 +39,13 @@ import java.util.*; public abstract class AbstractRedisBungeeAPI { protected final RedisBungeePlugin plugin; private static AbstractRedisBungeeAPI abstractRedisBungeeAPI; - protected final List reservedChannels; - AbstractRedisBungeeAPI(RedisBungeePlugin plugin) { - // this does make sure that no one can place first initiated API class. + public AbstractRedisBungeeAPI(RedisBungeePlugin plugin) { + // this does make sure that no one can replace first initiated API class. if (abstractRedisBungeeAPI == null) { abstractRedisBungeeAPI = this; } - this.reservedChannels = ImmutableList.of( - "redisbungee-allservers", - "redisbungee-" + plugin.getConfiguration().getProxyId(), - "redisbungee-data" - ); - this.plugin = plugin; - } /** @@ -63,7 +54,7 @@ public abstract class AbstractRedisBungeeAPI { * @return a count of all players found */ public final int getPlayerCount() { - return plugin.getCount(); + return plugin.proxyDataManager().totalNetworkPlayers(); } /** @@ -74,7 +65,7 @@ public abstract class AbstractRedisBungeeAPI { * @return the last time a player was on, if online returns a 0 */ public final long getLastOnline(@NonNull UUID player) { - return plugin.getDataManager().getLastOnline(player); + return plugin.playerDataManager().getLastOnline(player); } /** @@ -86,7 +77,7 @@ public abstract class AbstractRedisBungeeAPI { */ @Nullable public final String getServerNameFor(@NonNull UUID player) { - return plugin.getDataManager().getServer(player); + return plugin.playerDataManager().getServerFor(player); } /** @@ -97,7 +88,7 @@ public abstract class AbstractRedisBungeeAPI { * @return a Set with all players found */ public final Set getPlayersOnline() { - return plugin.getPlayers(); + return plugin.proxyDataManager().networkPlayers(); } /** @@ -118,11 +109,11 @@ public abstract class AbstractRedisBungeeAPI { /** * Get a full list of players on all servers. * - * @return a immutable Multimap with all players found on this server + * @return a immutable Multimap with all players found on this network * @since 0.2.5 */ public final Multimap getServerToPlayers() { - return plugin.serverToPlayersCache(); + return plugin.playerDataManager().serversToPlayers(); } /** @@ -138,11 +129,11 @@ public abstract class AbstractRedisBungeeAPI { /** * Get a list of players on the specified proxy. * - * @param server a server name + * @param proxyID proxy id * @return a Set with all UUIDs found on this proxy */ - public final Set getPlayersOnProxy(@NonNull String server) { - return plugin.getPlayersOnProxy(server); + public final Set getPlayersOnProxy(@NonNull String proxyID) { + return plugin.proxyDataManager().getPlayersOn(proxyID); } /** @@ -163,7 +154,7 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.2.4 */ public final InetAddress getPlayerIp(@NonNull UUID player) { - return plugin.getDataManager().getIp(player); + return plugin.playerDataManager().getIpFor(player); } /** @@ -174,7 +165,7 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.3.3 */ public final String getProxy(@NonNull UUID player) { - return plugin.getDataManager().getProxy(player); + return plugin.playerDataManager().getProxyFor(player); } /** @@ -185,7 +176,7 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.2.5 */ public final void sendProxyCommand(@NonNull String command) { - plugin.sendProxyCommand("allservers", command); + sendProxyCommand("allservers", command); } /** @@ -198,19 +189,20 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.2.5 */ public final void sendProxyCommand(@NonNull String proxyId, @NonNull String command) { - plugin.sendProxyCommand(proxyId, command); + plugin.proxyDataManager().sendCommandTo(proxyId, command); } /** - * Sends a message to a PubSub channel. The channel has to be subscribed to on this, or another redisbungee instance for - * PubSubMessageEvent to fire. + * Sends a message to a PubSub channel which makes PubSubMessageEvent fire. + *

+ * Note: Since 0.12.0 registering a channel api is no longer required * * @param channel The PubSub channel * @param message the message body to send * @since 0.3.3 */ public final void sendChannelMessage(@NonNull String channel, @NonNull String message) { - plugin.sendChannelMessage(channel, message); + plugin.proxyDataManager().sendChannelMessage(channel, message); } /** @@ -221,7 +213,7 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.8.0 */ public final String getProxyId() { - return plugin.getConfiguration().getProxyId(); + return plugin.proxyDataManager().proxyId(); } /** @@ -245,7 +237,7 @@ public abstract class AbstractRedisBungeeAPI { * @since 0.8.0 */ public final List getAllProxies() { - return plugin.getProxiesIds(); + return plugin.proxyDataManager().proxiesIds(); } /** @@ -266,9 +258,10 @@ public abstract class AbstractRedisBungeeAPI { * * @param channels the channels to register * @since 0.3 + * @deprecated No longer required */ + @Deprecated public final void registerPubSubChannels(String... channels) { - plugin.getPubSubListener().addChannel(channels); } /** @@ -276,13 +269,10 @@ public abstract class AbstractRedisBungeeAPI { * * @param channels the channels to unregister * @since 0.3 + * @deprecated No longer required */ + @Deprecated public final void unregisterPubSubChannels(String... channels) { - for (String channel : channels) { - Preconditions.checkArgument(!reservedChannels.contains(channel), "attempting to unregister internal channel"); - } - - plugin.getPubSubListener().removeChannel(channels); } /** @@ -355,14 +345,16 @@ public abstract class AbstractRedisBungeeAPI { /** * Kicks a player from the network + * calls {@link #getUuidFromName(String)} to get uuid * * @param playerName player name - * @param message kick message that player will see on kick + * @param message kick message that player will see on kick * @since 0.8.0 + * @deprecated */ - + @Deprecated public void kickPlayer(String playerName, String message) { - plugin.kickPlayer(playerName, message); + kickPlayer(getUuidFromName(playerName), message); } /** @@ -371,11 +363,38 @@ public abstract class AbstractRedisBungeeAPI { * @param playerUUID player name * @param message kick message that player will see on kick * @since 0.8.0 + * @deprecated */ + @Deprecated public void kickPlayer(UUID playerUUID, String message) { - plugin.kickPlayer(playerUUID, message); + kickPlayer(playerUUID, Component.text(message)); } + /** + * Kicks a player from the network + * calls {@link #getUuidFromName(String)} to get uuid + * + * @param playerName player name + * @param message kick message that player will see on kick + * @since 0.12.0 + */ + + public void kickPlayer(String playerName, Component message) { + kickPlayer(getUuidFromName(playerName), message); + } + + /** + * Kicks a player from the network + * + * @param playerUUID player name + * @param message kick message that player will see on kick + * @since 0.12.0 + */ + public void kickPlayer(UUID playerUUID, Component message) { + this.plugin.playerDataManager().kickPlayer(playerUUID, message); + } + + /** * This gives you instance of Jedis * @@ -457,6 +476,7 @@ public abstract class AbstractRedisBungeeAPI { /** * shows what mode is RedisBungee is on + * Basically what every redis mode is used like cluster or single instance. * * @return {@link RedisBungeeMode} * @since 0.8.0 diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractDataManager.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractDataManager.java deleted file mode 100644 index 1bff5b5..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractDataManager.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.net.InetAddresses; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; -import redis.clients.jedis.UnifiedJedis; - -import java.net.InetAddress; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -/** - * This class manages all the data that RedisBungee fetches from Redis, along with updates to that data. - * - * @since 0.3.3 - */ -public abstract class AbstractDataManager { - protected final RedisBungeePlugin

plugin; - private final Cache serverCache = createCache(); - private final Cache proxyCache = createCache(); - private final Cache ipCache = createCache(); - private final Cache lastOnlineCache = createCache(); - private final Gson gson = new Gson(); - - public AbstractDataManager(RedisBungeePlugin

plugin) { - this.plugin = plugin; - } - - private static Cache createCache() { - // TODO: Allow customization via cache specification, ala ServerListPlus - return CacheBuilder.newBuilder() - .maximumSize(1000) - .expireAfterWrite(1, TimeUnit.HOURS) - .build(); - } - - public String getServer(final UUID uuid) { - P player = plugin.getPlayer(uuid); - - if (player != null) - return plugin.isPlayerOnAServer(player) ? plugin.getPlayerServerName(player) : null; - - try { - return serverCache.get(uuid, new RedisTask(plugin) { - @Override - public String unifiedJedisTask(UnifiedJedis unifiedJedis) { - return Objects.requireNonNull(unifiedJedis.hget("player:" + uuid, "server"), "user not found"); - - } - }); - } catch (ExecutionException | UncheckedExecutionException e) { - if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found")) - return null; // HACK - plugin.logFatal("Unable to get server"); - throw new RuntimeException("Unable to get server for " + uuid, e); - } - } - - - public String getProxy(final UUID uuid) { - P player = plugin.getPlayer(uuid); - - if (player != null) - return plugin.getConfiguration().getProxyId(); - - try { - return proxyCache.get(uuid, new RedisTask(plugin) { - @Override - public String unifiedJedisTask(UnifiedJedis unifiedJedis) { - return Objects.requireNonNull(unifiedJedis.hget("player:" + uuid, "proxy"), "user not found"); - } - }); - } catch (ExecutionException | UncheckedExecutionException e) { - if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found")) - return null; // HACK - plugin.logFatal("Unable to get proxy"); - throw new RuntimeException("Unable to get proxy for " + uuid, e); - } - } - - public InetAddress getIp(final UUID uuid) { - P player = plugin.getPlayer(uuid); - - if (player != null) - return plugin.getPlayerIp(player); - - try { - return ipCache.get(uuid, new RedisTask(plugin) { - @Override - public InetAddress unifiedJedisTask(UnifiedJedis unifiedJedis) { - String result = unifiedJedis.hget("player:" + uuid, "ip"); - if (result == null) - throw new NullPointerException("user not found"); - return InetAddresses.forString(result); - } - }); - } catch (ExecutionException | UncheckedExecutionException e) { - if (e.getCause() instanceof NullPointerException && e.getCause().getMessage().equals("user not found")) - return null; // HACK - plugin.logFatal("Unable to get IP"); - throw new RuntimeException("Unable to get IP for " + uuid, e); - } - } - - public long getLastOnline(final UUID uuid) { - P player = plugin.getPlayer(uuid); - - if (player != null) - return 0; - - try { - return lastOnlineCache.get(uuid, new RedisTask(plugin) { - - @Override - public Long unifiedJedisTask(UnifiedJedis unifiedJedis) { - String result = unifiedJedis.hget("player:" + uuid, "online"); - return result == null ? -1 : Long.parseLong(result); - } - }); - } catch (ExecutionException e) { - plugin.logFatal("Unable to get last time online"); - throw new RuntimeException("Unable to get last time online for " + uuid, e); - } - } - - protected void invalidate(UUID uuid) { - ipCache.invalidate(uuid); - lastOnlineCache.invalidate(uuid); - serverCache.invalidate(uuid); - proxyCache.invalidate(uuid); - } - - // Invalidate all entries related to this player, since they now lie. (call invalidate(uuid)) - public abstract void onPostLogin(PL event); - - // Invalidate all entries related to this player, since they now lie. (call invalidate(uuid)) - public abstract void onPlayerDisconnect(PD event); - - public abstract void onPubSubMessage(PS event); - - public abstract boolean handleKick(UUID target, String message); - - protected void handlePubSubMessage(String channel, String message) { - if (!channel.equals("redisbungee-data")) - return; - - // Partially deserialize the message so we can look at the action - JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject(); - - final String source = jsonObject.get("source").getAsString(); - - if (source.equals(plugin.getConfiguration().getProxyId())) - return; - - DataManagerMessage.Action action = DataManagerMessage.Action.valueOf(jsonObject.get("action").getAsString()); - - switch (action) { - case JOIN: - final DataManagerMessage message1 = gson.fromJson(jsonObject, new TypeToken>() { - }.getType()); - proxyCache.put(message1.getTarget(), message1.getSource()); - lastOnlineCache.put(message1.getTarget(), (long) 0); - ipCache.put(message1.getTarget(), message1.getPayload().getAddress()); - plugin.executeAsync(() -> { - Object event = plugin.createPlayerJoinedNetworkEvent(message1.getTarget()); - plugin.fireEvent(event); - }); - break; - case LEAVE: - final DataManagerMessage message2 = gson.fromJson(jsonObject, new TypeToken>() { - }.getType()); - invalidate(message2.getTarget()); - lastOnlineCache.put(message2.getTarget(), message2.getPayload().getTimestamp()); - plugin.executeAsync(() -> { - Object event = plugin.createPlayerLeftNetworkEvent(message2.getTarget()); - plugin.fireEvent(event); - }); - break; - case SERVER_CHANGE: - final DataManagerMessage message3 = gson.fromJson(jsonObject, new TypeToken>() { - }.getType()); - serverCache.put(message3.getTarget(), message3.getPayload().getServer()); - plugin.executeAsync(() -> { - Object event = plugin.createPlayerChangedServerNetworkEvent(message3.getTarget(), message3.getPayload().getOldServer(), message3.getPayload().getServer()); - plugin.fireEvent(event); - }); - break; - case KICK: - final DataManagerMessage kickPayload = gson.fromJson(jsonObject, new TypeToken>() { - }.getType()); - plugin.executeAsync(() -> handleKick(kickPayload.target, kickPayload.payload.message)); - break; - - } - } - - public static class DataManagerMessage { - private final UUID target; - private final String source; - private final Action action; // for future use! - private final T payload; - - public DataManagerMessage(UUID target, String source, Action action, T payload) { - this.target = target; - this.source = source; - this.action = action; - this.payload = payload; - } - - public UUID getTarget() { - return target; - } - - public String getSource() { - return source; - } - - public Action getAction() { - return action; - } - - public T getPayload() { - return payload; - } - - public enum Action { - JOIN, - LEAVE, - KICK, - SERVER_CHANGE - } - } - - public static abstract class Payload { - } - - public static class KickPayload extends Payload { - - private final String message; - - public KickPayload(String message) { - this.message = message; - } - - public String getMessage() { - return message; - } - } - - public static class LoginPayload extends Payload { - private final InetAddress address; - - public LoginPayload(InetAddress address) { - this.address = address; - } - - public InetAddress getAddress() { - return address; - } - } - - public static class ServerChangePayload extends Payload { - private final String server; - private final String oldServer; - - public ServerChangePayload(String server, String oldServer) { - this.server = server; - this.oldServer = oldServer; - } - - public String getServer() { - return server; - } - - public String getOldServer() { - return oldServer; - } - } - - - public static class LogoutPayload extends Payload { - private final long timestamp; - - public LogoutPayload(long timestamp) { - this.timestamp = timestamp; - } - - public long getTimestamp() { - return timestamp; - } - } -} \ No newline at end of file diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractRedisBungeeListener.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractRedisBungeeListener.java deleted file mode 100644 index 64b42ab..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/AbstractRedisBungeeListener.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api; - - -import com.google.common.collect.Multimap; -import com.google.common.collect.Multiset; -import com.google.common.io.ByteArrayDataOutput; -import com.google.gson.Gson; - -import java.net.InetAddress; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -public abstract class AbstractRedisBungeeListener { - - protected final RedisBungeePlugin plugin; - protected final List exemptAddresses; - protected final Gson gson = new Gson(); - - public AbstractRedisBungeeListener(RedisBungeePlugin plugin, List exemptAddresses) { - this.plugin = plugin; - this.exemptAddresses = exemptAddresses; - } - - public void onLogin(LE event) {} - - public abstract void onPostLogin(PLE event); - - public abstract void onPlayerDisconnect(PD event); - - public abstract void onServerChange(SC event); - - public abstract void onPing(PP event); - - public abstract void onPluginMessage(PM event); - - public abstract void onPubSubMessage(PS event); - - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/JedisPubSubHandler.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/JedisPubSubHandler.java deleted file mode 100644 index d3974a5..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/JedisPubSubHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api; - - -import redis.clients.jedis.JedisPubSub; - - -public class JedisPubSubHandler extends JedisPubSub { - - private final RedisBungeePlugin plugin; - - public JedisPubSubHandler(RedisBungeePlugin plugin) { - this.plugin = plugin; - } - - @Override - public void onMessage(final String s, final String s2) { - if (s2.trim().length() == 0) return; - plugin.executeAsync(new Runnable() { - @Override - public void run() { - Object event = plugin.createPubSubEvent(s, s2); - plugin.fireEvent(event); - } - }); - } -} \ No newline at end of file diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PlayerDataManager.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PlayerDataManager.java new file mode 100644 index 0000000..a6d600a --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PlayerDataManager.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.net.InetAddresses; +import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerChangedServerNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerLeftNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.api.events.IPubSubMessageEvent; +import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisPipelineTask; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; +import org.json.JSONObject; +import redis.clients.jedis.ClusterPipeline; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; +import redis.clients.jedis.UnifiedJedis; + +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public abstract class PlayerDataManager { + + protected final RedisBungeePlugin

plugin; + private final LoadingCache serverCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(this::getServerFromRedis); + private final LoadingCache lastServerCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(this::getLastServerFromRedis); + private final LoadingCache proxyCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(this::getProxyFromRedis); + private final LoadingCache ipCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(this::getIpAddressFromRedis); + private final LoadingCache lastOnlineCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(this::getLastOnlineFromRedis); + private final Object SERVERS_TO_PLAYERS_KEY = new Object(); + private final LoadingCache> serverToPlayersCache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(this::serversToPlayersBuilder); + private final UnifiedJedis unifiedJedis; + private final String proxyId; + private final String networkId; + + public PlayerDataManager(RedisBungeePlugin

plugin) { + this.plugin = plugin; + this.unifiedJedis = plugin.proxyDataManager().unifiedJedis(); + this.proxyId = plugin.proxyDataManager().proxyId(); + this.networkId = plugin.proxyDataManager().networkId(); + } + + // handle network wide + // server change + public abstract void onPlayerChangedServerNetworkEvent(SC event); + + public abstract void onNetworkPlayerQuit(NJE event); + + // local events + public abstract void onPubSubMessageEvent(PS event); + + public abstract void onServerConnectedEvent(CE event); + + public abstract void onLoginEvent(LE event); + + public abstract void onDisconnectEvent(DE event); + + + protected void handleNetworkPlayerServerChange(IPlayerChangedServerNetworkEvent event) { + this.serverCache.invalidate(event.getUuid()); + this.lastServerCache.invalidate(event.getUuid()); + } + + protected void handleNetworkPlayerQuit(IPlayerLeftNetworkEvent event) { + this.proxyCache.invalidate(event.getUuid()); + this.serverCache.invalidate(event.getUuid()); + this.ipCache.invalidate(event.getUuid()); + this.lastOnlineCache.invalidate(event.getUuid()); + } + + protected void handlePubSubMessageEvent(IPubSubMessageEvent event) { + // kick api + if (event.getChannel().equals("redisbungee-kick")) { + JSONObject data = new JSONObject(event.getMessage()); + String proxy = data.getString("proxy"); + if (proxy.equals(this.proxyId)) { + return; + } + UUID uuid = UUID.fromString(data.getString("uuid")); + String message = data.getString("message"); + plugin.handlePlatformKick(uuid, COMPONENT_SERIALIZER.deserialize(message)); + return; + } + if (event.getChannel().equals("redisbungee-serverchange")) { + JSONObject data = new JSONObject(event.getMessage()); + String proxy = data.getString("proxy"); + if (proxy.equals(this.proxyId)) { + return; + } + UUID uuid = UUID.fromString(data.getString("uuid")); + String from = null; + if (data.has("from")) from = data.getString("from"); + String to = data.getString("to"); + plugin.fireEvent(plugin.createPlayerChangedServerNetworkEvent(uuid, from, to)); + return; + } + if (event.getChannel().equals("redisbungee-player-join")) { + JSONObject data = new JSONObject(event.getMessage()); + String proxy = data.getString("proxy"); + if (proxy.equals(this.proxyId)) { + return; + } + UUID uuid = UUID.fromString(data.getString("uuid")); + plugin.fireEvent(plugin.createPlayerJoinedNetworkEvent(uuid)); + return; + } + if (event.getChannel().equals("redisbungee-player-leave")) { + JSONObject data = new JSONObject(event.getMessage()); + String proxy = data.getString("proxy"); + if (proxy.equals(this.proxyId)) { + return; + } + UUID uuid = UUID.fromString(data.getString("uuid")); + plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid)); + } + + } + + protected void playerChangedServer(UUID uuid, String from, String to) { + JSONObject data = new JSONObject(); + data.put("proxy", this.proxyId); + data.put("uuid", uuid); + data.put("from", from); + data.put("to", to); + plugin.proxyDataManager().sendChannelMessage("redisbungee-serverchange", data.toString()); + plugin.fireEvent(plugin.createPlayerChangedServerNetworkEvent(uuid, from, to)); + handleServerChangeRedis(uuid, to); + } + + private final JSONComponentSerializer COMPONENT_SERIALIZER =JSONComponentSerializer.json(); + + public void kickPlayer(UUID uuid, Component message) { + if (!plugin.handlePlatformKick(uuid, message)) { // handle locally before SENDING a message + JSONObject data = new JSONObject(); + data.put("proxy", this.proxyId); + data.put("uuid", uuid); + data.put("message", COMPONENT_SERIALIZER.serialize(message)); + plugin.proxyDataManager().sendChannelMessage("redisbungee-kick", data.toString()); + } + } + + private void handleServerChangeRedis(UUID uuid, String server) { + Map data = new HashMap<>(); + data.put("server", server); + data.put("last-server", server); + unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", data); + } + + protected void addPlayer(final UUID uuid, final InetAddress inetAddress) { + Map redisData = new HashMap<>(); + redisData.put("last-online", String.valueOf(0)); + redisData.put("proxy", this.proxyId); + redisData.put("ip", inetAddress.getHostAddress()); + unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", redisData); + + JSONObject data = new JSONObject(); + data.put("proxy", this.proxyId); + data.put("uuid", uuid); + plugin.proxyDataManager().sendChannelMessage("redisbungee-player-join", data.toString()); + plugin.fireEvent(plugin.createPlayerJoinedNetworkEvent(uuid)); + this.plugin.proxyDataManager().addPlayer(uuid); + } + + protected void removePlayer(UUID uuid) { + unifiedJedis.hset("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-online", String.valueOf(System.currentTimeMillis())); + unifiedJedis.hdel("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "server", "proxy", "ip"); + JSONObject data = new JSONObject(); + data.put("proxy", this.proxyId); + data.put("uuid", uuid); + plugin.proxyDataManager().sendChannelMessage("redisbungee-player-leave", data.toString()); + plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid)); + this.plugin.proxyDataManager().removePlayer(uuid); + } + + + protected String getProxyFromRedis(UUID uuid) { + return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "proxy"); + } + + protected String getServerFromRedis(UUID uuid) { + return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "server"); + } + + protected String getLastServerFromRedis(UUID uuid) { + return unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-server"); + } + + protected InetAddress getIpAddressFromRedis(UUID uuid) { + String ip = unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "ip"); + if (ip == null) return null; + return InetAddresses.forString(ip); + } + + protected long getLastOnlineFromRedis(UUID uuid) { + String unixString = unifiedJedis.hget("redis-bungee::" + this.networkId + "::player::" + uuid + "::data", "last-online"); + if (unixString == null) return -1; + return Long.parseLong(unixString); + } + + public String getLastServerFor(UUID uuid) { + return this.lastServerCache.get(uuid); + } + public String getServerFor(UUID uuid) { + return this.serverCache.get(uuid); + } + + public String getProxyFor(UUID uuid) { + return this.proxyCache.get(uuid); + } + + public InetAddress getIpFor(UUID uuid) { + return this.ipCache.get(uuid); + } + + public long getLastOnline(UUID uuid) { + return this.lastOnlineCache.get(uuid); + } + + public Multimap serversToPlayers() { + return this.serverToPlayersCache.get(SERVERS_TO_PLAYERS_KEY); + } + + protected Multimap serversToPlayersBuilder(Object o) { + try { + return new RedisPipelineTask>(plugin) { + private final Set uuids = plugin.proxyDataManager().networkPlayers(); + private final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + + @Override + public Multimap doPooledPipeline(Pipeline pipeline) { + HashMap> responses = new HashMap<>(); + for (UUID uuid : uuids) { + responses.put(uuid, pipeline.hget("redis-bungee::" + networkId + "::player::" + uuid + "::data", "server")); + } + pipeline.sync(); + responses.forEach((uuid, response) -> builder.put(response.get(), uuid)); + return builder.build(); + } + + @Override + public Multimap clusterPipeline(ClusterPipeline pipeline) { + HashMap> responses = new HashMap<>(); + for (UUID uuid : uuids) { + responses.put(uuid, pipeline.hget("redis-bungee::" + networkId + "::player::" + uuid + "::data", "server")); + } + pipeline.sync(); + responses.forEach((uuid, response) -> builder.put(response.get(), uuid)); + return builder.build(); + } + }.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/ProxyDataManager.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/ProxyDataManager.java new file mode 100644 index 0000000..05e608c --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/ProxyDataManager.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; +import com.imaginarycode.minecraft.redisbungee.api.payloads.gson.AbstractPayloadSerializer; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.DeathPayload; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.HeartbeatPayload; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.PubSubPayload; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.RunCommandPayload; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.DeathPayloadSerializer; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.HeartbeatPayloadSerializer; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.PubSubPayloadSerializer; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson.RunCommandPayloadSerializer; +import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisPipelineTask; +import com.imaginarycode.minecraft.redisbungee.api.util.RedisUtil; +import redis.clients.jedis.*; +import redis.clients.jedis.params.XAddParams; +import redis.clients.jedis.params.XReadParams; +import redis.clients.jedis.resps.StreamEntry; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.base.Preconditions.checkArgument; + +public abstract class ProxyDataManager implements Runnable { + + private static final int MAX_ENTRIES = 10000; + + private final AtomicBoolean closed = new AtomicBoolean(false); + + private final UnifiedJedis unifiedJedis; + + // data: + // Proxy id, heartbeat (unix epoch from instant), players as int + private final ConcurrentHashMap heartbeats = new ConcurrentHashMap<>(); + + private final String networkId; + + private final String proxyId; + + private final String STREAM_ID; + + // This different from proxy id, just to detect if there is duplicate proxy using same proxy id + private final UUID dataManagerUUID = UUID.randomUUID(); + + protected final RedisBungeePlugin plugin; + + private final Gson gson = new GsonBuilder().registerTypeAdapter(AbstractPayload.class, new AbstractPayloadSerializer()).registerTypeAdapter(HeartbeatPayload.class, new HeartbeatPayloadSerializer()).registerTypeAdapter(DeathPayload.class, new DeathPayloadSerializer()).registerTypeAdapter(PubSubPayload.class, new PubSubPayloadSerializer()).registerTypeAdapter(RunCommandPayload.class, new RunCommandPayloadSerializer()).create(); + + public ProxyDataManager(RedisBungeePlugin plugin) { + this.plugin = plugin; + this.proxyId = this.plugin.configuration().getProxyId(); + this.unifiedJedis = plugin.getSummoner().obtainResource(); + this.destroyProxyMembers(); + this.networkId = plugin.configuration().networkId(); + this.STREAM_ID = "network-" + this.networkId + "-redisbungee-stream"; + } + + public abstract Set getLocalOnlineUUIDs(); + + public Set getPlayersOn(String proxyId) { + checkArgument(proxiesIds().contains(proxyId), proxyId + " is not a valid proxy ID"); + if (proxyId.equals(this.proxyId)) return this.getLocalOnlineUUIDs(); + if (!this.heartbeats.containsKey(proxyId)) { + return new HashSet<>(); // return empty hashset or null? + } + return getProxyMembers(proxyId); + } + + public List proxiesIds() { + return Collections.list(this.heartbeats.keys()); + } + + public synchronized void sendCommandTo(String proxyToRun, String command) { + if (isClosed()) return; + publishPayload(new RunCommandPayload(this.proxyId, proxyToRun, command)); + } + + public synchronized void sendChannelMessage(String channel, String message) { + if (isClosed()) return; + this.plugin.fireEvent(this.plugin.createPubSubEvent(channel, message)); + publishPayload(new PubSubPayload(this.proxyId, channel, message)); + } + + // call every 1 second + public synchronized void publishHeartbeat() { + if (isClosed()) return; + HeartbeatPayload.HeartbeatData heartbeatData = new HeartbeatPayload.HeartbeatData(Instant.now().getEpochSecond(), this.getLocalOnlineUUIDs().size()); + this.heartbeats.put(this.proxyId(), heartbeatData); + publishPayload(new HeartbeatPayload(this.proxyId, heartbeatData)); + } + + public Set networkPlayers() { + try { + return new RedisPipelineTask>(this.plugin) { + @Override + public Set doPooledPipeline(Pipeline pipeline) { + HashSet>> responses = new HashSet<>(); + for (String proxyId : proxiesIds()) { + responses.add(pipeline.smembers("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players")); + } + pipeline.sync(); + HashSet uuids = new HashSet<>(); + for (Response> response : responses) { + for (String stringUUID : response.get()) { + uuids.add(UUID.fromString(stringUUID)); + } + } + return uuids; + } + + @Override + public Set clusterPipeline(ClusterPipeline pipeline) { + HashSet>> responses = new HashSet<>(); + for (String proxyId : proxiesIds()) { + responses.add(pipeline.smembers("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players")); + } + pipeline.sync(); + HashSet uuids = new HashSet<>(); + for (Response> response : responses) { + for (String stringUUID : response.get()) { + uuids.add(UUID.fromString(stringUUID)); + } + } + return uuids; + } + }.call(); + } catch (Exception e) { + throw new RuntimeException("unable to get network players", e); + } + + } + + public int totalNetworkPlayers() { + int players = 0; + for (HeartbeatPayload.HeartbeatData value : this.heartbeats.values()) { + players += value.players(); + } + return players; + } + + public Map eachProxyCount() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + heartbeats.forEach((proxy, data) -> builder.put(proxy, data.players())); + return builder.build(); + } + + // Call on close + private synchronized void publishDeath() { + publishPayload(new DeathPayload(this.proxyId)); + } + + private void publishPayload(AbstractPayload payload) { + Map data = new HashMap<>(); + data.put("payload", gson.toJson(payload)); + data.put("data-manager-uuid", this.dataManagerUUID.toString()); + data.put("class", payload.getClassName()); + this.unifiedJedis.xadd(STREAM_ID, XAddParams.xAddParams().maxLen(MAX_ENTRIES).id(StreamEntryID.NEW_ENTRY), data); + } + + + private void handleHeartBeat(HeartbeatPayload payload) { + String id = payload.senderProxy(); + if (!heartbeats.containsKey(id)) { + plugin.logInfo("Proxy {} has connected", id); + } + heartbeats.put(id, payload.data()); + } + + + // call every 1 minutes + public void correctionTask() { + // let's check this proxy players + Set localOnlineUUIDs = getLocalOnlineUUIDs(); + Set storedRedisUuids = getProxyMembers(this.proxyId); + + if (!localOnlineUUIDs.equals(storedRedisUuids)) { + plugin.logWarn("De-synced playerS set detected correcting...."); + Set add = new HashSet<>(localOnlineUUIDs); + Set remove = new HashSet<>(storedRedisUuids); + add.removeAll(storedRedisUuids); + remove.removeAll(localOnlineUUIDs); + for (UUID uuid : add) { + plugin.logWarn("found {} that isn't in the set, adding it to the Corrected set", uuid); + } + for (UUID uuid : remove) { + plugin.logWarn("found {} that does not belong to this proxy removing it from the corrected set", uuid); + } + try { + new RedisPipelineTask(plugin) { + @Override + public Void doPooledPipeline(Pipeline pipeline) { + Set removeString = new HashSet<>(); + for (UUID uuid : remove) { + removeString.add(uuid.toString()); + } + Set addString = new HashSet<>(); + for (UUID uuid : add) { + addString.add(uuid.toString()); + } + pipeline.srem("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", removeString.toArray(new String[]{})); + pipeline.sadd("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", addString.toArray(new String[]{})); + pipeline.sync(); + return null; + } + + @Override + public Void clusterPipeline(ClusterPipeline pipeline) { + Set removeString = new HashSet<>(); + for (UUID uuid : remove) { + removeString.add(uuid.toString()); + } + Set addString = new HashSet<>(); + for (UUID uuid : add) { + addString.add(uuid.toString()); + } + pipeline.srem("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", removeString.toArray(new String[]{})); + pipeline.sadd("redisbungee::" + networkId + "::proxies::" + proxyId + "::online-players", addString.toArray(new String[]{})); + pipeline.sync(); + return null; + } + }.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + plugin.logInfo("Player set has been corrected!"); + } + + + // handle dead proxies "THAT" Didn't send death payload but considered dead due TIMEOUT ~30 seconds + final Set deadProxies = new HashSet<>(); + for (Map.Entry stringHeartbeatDataEntry : this.heartbeats.entrySet()) { + String id = stringHeartbeatDataEntry.getKey(); + long heartbeat = stringHeartbeatDataEntry.getValue().heartbeat(); + if (Instant.now().getEpochSecond() - heartbeat > RedisUtil.PROXY_TIMEOUT) { + deadProxies.add(id); + cleanProxy(id); + } + } + try { + new RedisPipelineTask(plugin) { + @Override + public Void doPooledPipeline(Pipeline pipeline) { + for (String deadProxy : deadProxies) { + pipeline.del("redisbungee::" + networkId + "::proxies::" + deadProxy + "::online-players"); + } + pipeline.sync(); + return null; + } + + @Override + public Void clusterPipeline(ClusterPipeline pipeline) { + for (String deadProxy : deadProxies) { + pipeline.del("redisbungee::" + networkId + "::proxies::" + deadProxy + "::online-players"); + } + pipeline.sync(); + return null; + } + }.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void handleProxyDeath(DeathPayload payload) { + cleanProxy(payload.senderProxy()); + } + + private void cleanProxy(String id) { + if (id.equals(this.proxyId())) { + return; + } + for (UUID uuid : getProxyMembers(id)) plugin.fireEvent(plugin.createPlayerLeftNetworkEvent(uuid)); + this.heartbeats.remove(id); + plugin.logInfo("Proxy {} has disconnected", id); + } + + private void handleChannelMessage(PubSubPayload payload) { + String channel = payload.channel(); + String message = payload.message(); + this.plugin.fireEvent(this.plugin.createPubSubEvent(channel, message)); + } + + protected abstract void handlePlatformCommandExecution(String command); + + private void handleCommand(RunCommandPayload payload) { + String proxyToRun = payload.proxyToRun(); + String command = payload.command(); + if (proxyToRun.equals("allservers") || proxyToRun.equals(this.proxyId())) { + handlePlatformCommandExecution(command); + } + } + + + public void addPlayer(UUID uuid) { + this.unifiedJedis.sadd("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players", uuid.toString()); + } + + public void removePlayer(UUID uuid) { + this.unifiedJedis.srem("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players", uuid.toString()); + } + + private void destroyProxyMembers() { + unifiedJedis.del("redisbungee::" + this.networkId + "::proxies::" + this.proxyId + "::online-players"); + } + + private Set getProxyMembers(String proxyId) { + Set uuidsStrings = unifiedJedis.smembers("redisbungee::" + this.networkId + "::proxies::" + proxyId + "::online-players"); + HashSet uuids = new HashSet<>(); + for (String proxyMember : uuidsStrings) { + uuids.add(UUID.fromString(proxyMember)); + } + return uuids; + } + + private StreamEntryID lastStreamEntryID; + + // polling from stream + @Override + public void run() { + while (!isClosed()) { + try { + List>> data = unifiedJedis.xread(XReadParams.xReadParams().block(0), Collections.singletonMap(STREAM_ID, lastStreamEntryID != null ? lastStreamEntryID : StreamEntryID.LAST_ENTRY)); + for (Map.Entry> datum : data) { + for (StreamEntry streamEntry : datum.getValue()) { + this.lastStreamEntryID = streamEntry.getID(); + String payloadData = streamEntry.getFields().get("payload"); + String clazz = streamEntry.getFields().get("class"); + UUID payloadDataManagerUUID = UUID.fromString(streamEntry.getFields().get("data-manager-uuid")); + + AbstractPayload unknownPayload = (AbstractPayload) gson.fromJson(payloadData, Class.forName(clazz)); + + if (unknownPayload.senderProxy().equals(this.proxyId)) { + if (!payloadDataManagerUUID.equals(this.dataManagerUUID)) { + plugin.logWarn("detected other proxy is using same ID! {} this can cause issues, please shutdown this proxy and change the id!", this.proxyId); + } + continue; + } + if (unknownPayload instanceof HeartbeatPayload payload) { + handleHeartBeat(payload); + } else if (unknownPayload instanceof DeathPayload payload) { + handleProxyDeath(payload); + } else if (unknownPayload instanceof RunCommandPayload payload) { + handleCommand(payload); + } else if (unknownPayload instanceof PubSubPayload payload) { + handleChannelMessage(payload); + } else { + plugin.logWarn("got unknown data manager payload: {}", unknownPayload.getClassName()); + } + } + } + } catch (Exception e) { + this.plugin.logFatal("an error has occurred in the stream", e); + try { + Thread.sleep(5000); + } catch (InterruptedException ignored) { + } + } + } + } + + public void close() { + closed.set(true); + this.publishDeath(); + this.heartbeats.clear(); + this.destroyProxyMembers(); + } + + public boolean isClosed() { + return closed.get(); + } + + public String proxyId() { + return proxyId; + } + + public UnifiedJedis unifiedJedis() { + return unifiedJedis; + } + + public String networkId() { + return networkId; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PubSubListener.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PubSubListener.java deleted file mode 100644 index cd19d71..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/PubSubListener.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api; - -import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.UnifiedJedis; -import redis.clients.jedis.exceptions.JedisConnectionException; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -public class PubSubListener implements Runnable { - private JedisPubSubHandler jpsh; - private final Set addedChannels = new HashSet(); - - private final RedisBungeePlugin plugin; - - public PubSubListener(RedisBungeePlugin plugin) { - this.plugin = plugin; - } - - @Override - public void run() { - RedisTask subTask = new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - jpsh = new JedisPubSubHandler(plugin); - addedChannels.add("redisbungee-" + plugin.getConfiguration().getProxyId()); - addedChannels.add("redisbungee-allservers"); - addedChannels.add("redisbungee-data"); - unifiedJedis.subscribe(jpsh, addedChannels.toArray(new String[0])); - return null; - } - }; - - try { - subTask.execute(); - } catch (Exception e) { - plugin.logWarn("PubSub error, attempting to recover in 5 secs."); - plugin.executeAsyncAfter(this, TimeUnit.SECONDS, 5); - } - } - - public void addChannel(String... channel) { - addedChannels.addAll(Arrays.asList(channel)); - jpsh.subscribe(channel); - } - - public void removeChannel(String... channel) { - Arrays.asList(channel).forEach(addedChannels::remove); - jpsh.unsubscribe(channel); - } - - public void poison() { - addedChannels.clear(); - jpsh.unsubscribe(); - } -} - diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/RedisBungeePlugin.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/RedisBungeePlugin.java index a0e2471..8dc6887 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/RedisBungeePlugin.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/RedisBungeePlugin.java @@ -10,28 +10,18 @@ package com.imaginarycode.minecraft.redisbungee.api; -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.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; +import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration; import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; import com.imaginarycode.minecraft.redisbungee.api.events.EventsPlatform; import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; -import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; -import com.imaginarycode.minecraft.redisbungee.api.util.RedisUtil; -import com.imaginarycode.minecraft.redisbungee.api.util.payload.PayloadUtils; import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDTranslator; -import redis.clients.jedis.Protocol; -import redis.clients.jedis.UnifiedJedis; -import redis.clients.jedis.exceptions.JedisConnectionException; +import net.kyori.adventure.text.Component; import java.net.InetAddress; -import java.util.*; +import java.util.UUID; import java.util.concurrent.TimeUnit; -import static com.google.common.base.Preconditions.checkArgument; - /** * This Class has all internal methods needed by every redis bungee plugin, and it can be used to implement another platforms than bungeecord or another forks of RedisBungee @@ -51,225 +41,56 @@ public interface RedisBungeePlugin

extends EventsPlatform { } - Summoner getSummoner(); - - RedisBungeeConfiguration getConfiguration(); - - int getCount(); - - default int getCurrentCount() { - return new RedisTask(this) { - @Override - public Long unifiedJedisTask(UnifiedJedis unifiedJedis) { - long total = 0; - long redisTime = getRedisTime(unifiedJedis); - Map heartBeats = unifiedJedis.hgetAll("heartbeats"); - for (Map.Entry stringStringEntry : heartBeats.entrySet()) { - String k = stringStringEntry.getKey(); - String v = stringStringEntry.getValue(); - - long heartbeatTime = Long.parseLong(v); - if (heartbeatTime + RedisUtil.PROXY_TIMEOUT >= redisTime) { - total = total + unifiedJedis.scard("proxy:" + k + ":usersOnline"); - } - } - return total; - } - }.execute().intValue(); - } - - Set getLocalPlayersAsUuidStrings(); - - AbstractDataManager getDataManager(); - - default Set getPlayers() { - return new RedisTask>(this) { - @Override - public Set unifiedJedisTask(UnifiedJedis unifiedJedis) { - ImmutableSet.Builder setBuilder = ImmutableSet.builder(); - try { - List keys = new ArrayList<>(); - for (String i : getProxiesIds()) { - keys.add("proxy:" + i + ":usersOnline"); - } - if (!keys.isEmpty()) { - Set users = unifiedJedis.sunion(keys.toArray(new String[0])); - 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! - logFatal("Unable to get connection from pool - did your Redis server go away?"); - throw new RuntimeException("Unable to get all players online", e); - } - return setBuilder.build(); - } - }.execute(); - } - - AbstractRedisBungeeAPI getAbstractRedisBungeeApi(); - - UUIDTranslator getUuidTranslator(); - - Multimap serverToPlayersCache(); - - default Multimap serversToPlayers() { - return new RedisTask>(this) { - @Override - public Multimap unifiedJedisTask(UnifiedJedis unifiedJedis) { - ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - for (String serverId : getProxiesIds()) { - Set players = unifiedJedis.smembers("proxy:" + serverId + ":usersOnline"); - for (String player : players) { - String playerServer = unifiedJedis.hget("player:" + player, "server"); - if (playerServer == null) { - continue; - } - builder.put(playerServer, UUID.fromString(player)); - } - } - return builder.build(); - } - }.execute(); - } - - default Set getPlayersOnProxy(String proxyId) { - checkArgument(getProxiesIds().contains(proxyId), proxyId + " is not a valid proxy ID"); - return new RedisTask>(this) { - @Override - public Set unifiedJedisTask(UnifiedJedis unifiedJedis) { - Set users = unifiedJedis.smembers("proxy:" + proxyId + ":usersOnline"); - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (String user : users) { - builder.add(UUID.fromString(user)); - } - return builder.build(); - } - }.execute(); - } - - default void sendProxyCommand(String proxyId, String command) { - checkArgument(getProxiesIds().contains(proxyId) || proxyId.equals("allservers"), "proxyId is invalid"); - sendChannelMessage("redisbungee-" + proxyId, command); - } - - List getProxiesIds(); - - default List getCurrentProxiesIds(boolean lagged) { - return new RedisTask>(this) { - @Override - public List unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - long time = getRedisTime(unifiedJedis); - ImmutableList.Builder servers = ImmutableList.builder(); - Map heartbeats = unifiedJedis.hgetAll("heartbeats"); - for (Map.Entry entry : heartbeats.entrySet()) { - try { - long stamp = Long.parseLong(entry.getValue()); - if (lagged ? time >= stamp + RedisUtil.PROXY_TIMEOUT : time <= stamp + RedisUtil.PROXY_TIMEOUT) { - servers.add(entry.getKey()); - } else if (time > stamp + RedisUtil.PROXY_TIMEOUT) { - logWarn(entry.getKey() + " is " + (time - stamp) + " seconds behind! (Time not synchronized or server down?) and was removed from heartbeat."); - unifiedJedis.hdel("heartbeats", entry.getKey()); - } - } catch (NumberFormatException ignored) { - } - } - return servers.build(); - } catch (JedisConnectionException e) { - logFatal("Unable to fetch server IDs"); - e.printStackTrace(); - return Collections.singletonList(getConfiguration().getProxyId()); - } - } - }.execute(); - } - - PubSubListener getPubSubListener(); - - default void sendChannelMessage(String channel, String message) { - new RedisTask(this) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - unifiedJedis.publish(channel, message); - } catch (JedisConnectionException e) { - // Redis server has disappeared! - logFatal("Unable to get connection from pool - did your Redis server go away?"); - throw new RuntimeException("Unable to publish channel message", e); - } - return null; - } - }.execute(); - } - - void executeAsync(Runnable runnable); - - void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int time); - - boolean isOnlineMode(); - void logInfo(String msg); + void logInfo(String format, Object... object); + void logWarn(String msg); + void logWarn(String format, Object... object); + void logFatal(String msg); + void logFatal(String format, Throwable throwable); + + RedisBungeeConfiguration configuration(); + + LangConfiguration langConfiguration(); + + Summoner getSummoner(); + + RedisBungeeMode getRedisBungeeMode(); + + AbstractRedisBungeeAPI getAbstractRedisBungeeApi(); + + ProxyDataManager proxyDataManager(); + + PlayerDataManager playerDataManager(); + + UUIDTranslator getUuidTranslator(); + + boolean isOnlineMode(); + P getPlayer(UUID uuid); P getPlayer(String name); UUID getPlayerUUID(String player); + String getPlayerName(UUID player); + boolean handlePlatformKick(UUID uuid, Component message); + String getPlayerServerName(P player); boolean isPlayerOnAServer(P player); InetAddress getPlayerIp(P player); - default void sendProxyCommand(String cmd) { - sendProxyCommand(getConfiguration().getProxyId(), cmd); - } + void executeAsync(Runnable runnable); - default Long getRedisTime(UnifiedJedis unifiedJedis) { - List data = (List) unifiedJedis.sendCommand(Protocol.Command.TIME); - List times = new ArrayList<>(); - data.forEach((o) -> times.add(new String((byte[])o))); - return getRedisTime(times); - } - default long getRedisTime(List timeRes) { - return Long.parseLong(timeRes.get(0)); - } + void executeAsyncAfter(Runnable runnable, TimeUnit timeUnit, int time); - default void kickPlayer(UUID playerUniqueId, String message) { - // first handle on origin proxy if player not found publish the payload - if (!getDataManager().handleKick(playerUniqueId, message)) { - new RedisTask(this) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - PayloadUtils.kickPlayerPayload(playerUniqueId, message, unifiedJedis); - return null; - } - }.execute(); - } - } - - default void kickPlayer(String playerName, String message) { - // fetch the uuid from name - UUID playerUUID = getUuidTranslator().getTranslatedUuid(playerName, true); - kickPlayer(playerUUID, message); - } - - RedisBungeeMode getRedisBungeeMode(); - - void updateProxiesIds(); } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/LangConfiguration.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/LangConfiguration.java new file mode 100644 index 0000000..87aaa11 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/LangConfiguration.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.config; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * This language support implementation is temporarily + * until I come up with better system but for now we will use Maps instead :/ + * Todo: possible usage of adventure api + */ +public class LangConfiguration { + + private interface RegistrableMessages { + + void register(String id, Locale locale, String miniMessage); + + void test(Locale locale); + + default void throwError(Locale locale, String where) { + throw new IllegalStateException("Language system in `" + where + "` found missing entries for " + locale.toString()); + } + + } + + public static class Messages implements RegistrableMessages{ + + private final Map LOGGED_IN_FROM_OTHER_LOCATION; + private final Map ALREADY_LOGGED_IN; + private final Map SERVER_CONNECTING; + private final Map SERVER_NOT_FOUND; + + private final Locale defaultLocale; + + public Messages(Locale defaultLocale) { + LOGGED_IN_FROM_OTHER_LOCATION = new HashMap<>(); + ALREADY_LOGGED_IN = new HashMap<>(); + SERVER_CONNECTING = new HashMap<>(); + SERVER_NOT_FOUND = new HashMap<>(); + this.defaultLocale = defaultLocale; + } + + public void register(String id, Locale locale, String miniMessage) { + switch (id) { + case "server-not-found" -> SERVER_NOT_FOUND.put(locale, miniMessage); + case "server-connecting" -> SERVER_CONNECTING.put(locale, miniMessage); + case "logged-in-other-location" -> LOGGED_IN_FROM_OTHER_LOCATION.put(locale, MiniMessage.miniMessage().deserialize(miniMessage)); + case "already-logged-in" -> ALREADY_LOGGED_IN.put(locale, MiniMessage.miniMessage().deserialize(miniMessage)); + } + } + + public Component alreadyLoggedIn(Locale locale) { + if (ALREADY_LOGGED_IN.containsKey(locale)) return ALREADY_LOGGED_IN.get(locale); + return ALREADY_LOGGED_IN.get(defaultLocale); + } + + // there is no way to know whats client locale during login so just default to use default locale MESSAGES. + public Component alreadyLoggedIn() { + return this.alreadyLoggedIn(this.defaultLocale); + } + + public Component loggedInFromOtherLocation(Locale locale) { + if (LOGGED_IN_FROM_OTHER_LOCATION.containsKey(locale)) return LOGGED_IN_FROM_OTHER_LOCATION.get(locale); + return LOGGED_IN_FROM_OTHER_LOCATION.get(defaultLocale); + } + + // there is no way to know what's client locale during login so just default to use default locale MESSAGES. + public Component loggedInFromOtherLocation() { + return this.loggedInFromOtherLocation(this.defaultLocale); + } + + public Component serverConnecting(Locale locale, String server) { + String miniMessage; + if (SERVER_CONNECTING.containsKey(locale)) { + miniMessage = SERVER_CONNECTING.get(locale); + } else { + miniMessage = SERVER_CONNECTING.get(defaultLocale); + } + return MiniMessage.miniMessage().deserialize(miniMessage, Placeholder.parsed("server", server)); + } + + public Component serverConnecting(String server) { + return this.serverConnecting(this.defaultLocale, server); + } + + public Component serverNotFound(Locale locale, String server) { + String miniMessage; + if (SERVER_NOT_FOUND.containsKey(locale)) { + miniMessage = SERVER_NOT_FOUND.get(locale); + } else { + miniMessage = SERVER_NOT_FOUND.get(defaultLocale); + } + return MiniMessage.miniMessage().deserialize(miniMessage, Placeholder.parsed("server", server)); + } + + public Component serverNotFound(String server) { + return this.serverNotFound(this.defaultLocale, server); + } + + + // tests locale if set CORRECTLY or just throw if not + public void test(Locale locale) { + if (!(LOGGED_IN_FROM_OTHER_LOCATION.containsKey(locale) && ALREADY_LOGGED_IN.containsKey(locale) && SERVER_CONNECTING.containsKey(locale) && SERVER_NOT_FOUND.containsKey(locale))) { + throwError(locale, "messages"); + } + } + + } + + private final Component redisBungeePrefix; + + private final Locale defaultLanguage; + + private final boolean useClientLanguage; + + private final Messages messages; + + public LangConfiguration(Component redisBungeePrefix, Locale defaultLanguage, boolean useClientLanguage, Messages messages) { + this.redisBungeePrefix = redisBungeePrefix; + this.defaultLanguage = defaultLanguage; + this.useClientLanguage = useClientLanguage; + this.messages = messages; + } + + public Component redisBungeePrefix() { + return redisBungeePrefix; + } + + public Locale defaultLanguage() { + return defaultLanguage; + } + + public boolean useClientLanguage() { + return useClientLanguage; + } + + public Messages messages() { + return messages; + } + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/RedisBungeeConfiguration.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/RedisBungeeConfiguration.java index 2e595f4..f76fb39 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/RedisBungeeConfiguration.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/RedisBungeeConfiguration.java @@ -11,42 +11,39 @@ package com.imaginarycode.minecraft.redisbungee.api.config; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMultimap; import com.google.common.net.InetAddresses; +import javax.annotation.Nullable; import java.net.InetAddress; -import java.util.HashMap; import java.util.List; public class RedisBungeeConfiguration { - public enum MessageType { - LOGGED_IN_OTHER_LOCATION, - ALREADY_LOGGED_IN - } - - private final ImmutableMap messages; - public static final int CONFIG_VERSION = 1; private final String proxyId; private final List exemptAddresses; - private final boolean registerLegacyCommands; - private final boolean overrideBungeeCommands; + private final boolean kickWhenOnline; - private final boolean restoreOldKickBehavior; + private final boolean handleReconnectToLastServer; + private final boolean handleMotd; - public RedisBungeeConfiguration(String proxyId, List exemptAddresses, boolean registerLegacyCommands, boolean overrideBungeeCommands, ImmutableMap messages, boolean restoreOldKickBehavior) { + private final CommandsConfiguration commandsConfiguration; + private final String networkId; + + + public RedisBungeeConfiguration(String networkId, String proxyId, List exemptAddresses, boolean kickWhenOnline, boolean handleReconnectToLastServer, boolean handleMotd, CommandsConfiguration commandsConfiguration) { this.proxyId = proxyId; - this.messages = messages; ImmutableList.Builder addressBuilder = ImmutableList.builder(); for (String s : exemptAddresses) { addressBuilder.add(InetAddresses.forString(s)); } this.exemptAddresses = addressBuilder.build(); - this.registerLegacyCommands = registerLegacyCommands; - this.overrideBungeeCommands = overrideBungeeCommands; - this.restoreOldKickBehavior = restoreOldKickBehavior; + this.kickWhenOnline = kickWhenOnline; + this.handleReconnectToLastServer = handleReconnectToLastServer; + this.handleMotd = handleMotd; + this.commandsConfiguration = commandsConfiguration; + this.networkId = networkId; } + public String getProxyId() { return proxyId; } @@ -55,19 +52,37 @@ public class RedisBungeeConfiguration { return exemptAddresses; } - public boolean doRegisterLegacyCommands() { - return registerLegacyCommands; + public boolean kickWhenOnline() { + return kickWhenOnline; } - public boolean doOverrideBungeeCommands() { - return overrideBungeeCommands; + public boolean handleMotd() { + return this.handleMotd; } - public ImmutableMap getMessages() { - return messages; + public boolean handleReconnectToLastServer() { + return this.handleReconnectToLastServer; } - public boolean restoreOldKickBehavior() { - return restoreOldKickBehavior; + public record CommandsConfiguration(boolean redisbungeeEnabled, boolean redisbungeeLegacyEnabled, + @Nullable LegacySubCommandsConfiguration legacySubCommandsConfiguration) { + + } + + public record LegacySubCommandsConfiguration(boolean findEnabled, boolean glistEnabled, boolean ipEnabled, + boolean lastseenEnabled, boolean plistEnabled, boolean pproxyEnabled, + boolean sendtoallEnabled, boolean serveridEnabled, + boolean serveridsEnabled, boolean installFind, boolean installGlist, boolean installIp, + boolean installLastseen, boolean installPlist, boolean installPproxy, + boolean installSendtoall, boolean installServerid, + boolean installServerids) { + } + + public CommandsConfiguration commandsConfiguration() { + return commandsConfiguration; + } + + public String networkId() { + return networkId; } } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/ConfigLoader.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/ConfigLoader.java similarity index 54% rename from RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/ConfigLoader.java rename to RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/ConfigLoader.java index b92365e..a73b6ef 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/ConfigLoader.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/ConfigLoader.java @@ -8,13 +8,13 @@ * http://www.eclipse.org/legal/epl-v10.html */ -package com.imaginarycode.minecraft.redisbungee.api.config; +package com.imaginarycode.minecraft.redisbungee.api.config.loaders; -import com.google.common.collect.ImmutableMap; import com.google.common.reflect.TypeToken; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; @@ -26,35 +26,29 @@ import redis.clients.jedis.*; import redis.clients.jedis.providers.ClusterConnectionProvider; import redis.clients.jedis.providers.PooledConnectionProvider; -import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.*; -public interface ConfigLoader { +public interface ConfigLoader extends GenericConfigLoader { - default void loadConfig(RedisBungeePlugin plugin, File dataFolder) throws IOException { - loadConfig(plugin, dataFolder.toPath()); - } + int CONFIG_VERSION = 2; default void loadConfig(RedisBungeePlugin plugin, Path dataFolder) throws IOException { - Path configFile = createConfigFile(dataFolder); + Path configFile = createConfigFile(dataFolder, "config.yml", "config.yml"); final YAMLConfigurationLoader yamlConfigurationFileLoader = YAMLConfigurationLoader.builder().setPath(configFile).build(); ConfigurationNode node = yamlConfigurationFileLoader.load(); - if (node.getNode("config-version").getInt(0) != RedisBungeeConfiguration.CONFIG_VERSION) { - handleOldConfig(dataFolder); + if (node.getNode("config-version").getInt(0) != CONFIG_VERSION) { + handleOldConfig(dataFolder, "config.yml", "config.yml"); node = yamlConfigurationFileLoader.load(); } final boolean useSSL = node.getNode("useSSL").getBoolean(false); - final boolean overrideBungeeCommands = node.getNode("override-bungee-commands").getBoolean(false); - final boolean registerLegacyCommands = node.getNode("register-legacy-commands").getBoolean(false); - final boolean restoreOldKickBehavior = node.getNode("disable-kick-when-online").getBoolean(false); + final boolean kickWhenOnline = node.getNode("kick-when-online").getBoolean(true); String redisPassword = node.getNode("redis-password").getString(""); String redisUsername = node.getNode("redis-username").getString(""); - String proxyId = node.getNode("proxy-id").getString("test-1"); + String networkId = node.getNode("network-id").getString("main"); + String proxyId = node.getNode("proxy-id").getString("proxy-1"); + final int maxConnections = node.getNode("max-redis-connections").getInt(10); List exemptAddresses; try { @@ -71,10 +65,19 @@ public interface ConfigLoader { if ((redisUsername.isEmpty() || redisUsername.equals("none"))) { redisUsername = null; } - - if (useSSL) { - plugin.logInfo("Using ssl"); + // env var + String proxyIdFromEnv = System.getenv("REDISBUNGEE_PROXY_ID"); + if (proxyIdFromEnv != null) { + plugin.logInfo("Overriding current configured proxy id {} and been set to {} by Environment variable REDISBUNGEE_PROXY_ID", proxyId, proxyIdFromEnv); + proxyId = proxyIdFromEnv; } + + String networkIdFromEnv = System.getenv("REDISBUNGEE_NETWORK_ID"); + if (networkIdFromEnv != null) { + plugin.logInfo("Overriding current configured network id {} and been set to {} by Environment variable REDISBUNGEE_NETWORK_ID", networkId, networkIdFromEnv); + networkId = networkIdFromEnv; + } + // Configuration sanity checks. if (proxyId == null || proxyId.isEmpty()) { String genId = UUID.randomUUID().toString(); @@ -86,9 +89,62 @@ public interface ConfigLoader { } else { plugin.logInfo("Loaded proxy id " + proxyId); } - RedisBungeeConfiguration configuration = new RedisBungeeConfiguration(proxyId, exemptAddresses, registerLegacyCommands, overrideBungeeCommands, getMessagesFromPath(createMessagesFile(dataFolder)), restoreOldKickBehavior); + + if (networkId.isEmpty()) { + networkId = "main"; + plugin.logWarn("network id was empty and replaced with 'main'"); + } + + plugin.logInfo("Loaded network id " + networkId); + + + + boolean reconnectToLastServer = node.getNode("reconnect-to-last-server").getBoolean(); + boolean handleMotd = node.getNode("handle-motd").getBoolean(true); + plugin.logInfo("handle reconnect to last server: {}", reconnectToLastServer); + plugin.logInfo("handle motd: {}", handleMotd); + + + // commands + boolean redisBungeeEnabled = node.getNode("commands", "redisbungee", "enabled").getBoolean(true); + boolean redisBungeeLegacyEnabled =node.getNode("commands", "redisbungee-legacy", "enabled").getBoolean(false); + + boolean glistEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "glist", "enabled").getBoolean(false); + boolean findEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "find", "enabled").getBoolean(false); + boolean lastseenEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "lastseen", "enabled").getBoolean(false); + boolean ipEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "ip", "enabled").getBoolean(false); + boolean pproxyEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "pproxy", "enabled").getBoolean(false); + boolean sendToAllEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "sendtoall", "enabled").getBoolean(false); + boolean serverIdEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverid", "enabled").getBoolean(false); + boolean serverIdsEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverids", "enabled").getBoolean(false); + boolean pListEnabled = node.getNode("commands", "redisbungee-legacy", "subcommands", "plist", "enabled").getBoolean(false); + + boolean installGlist = node.getNode("commands", "redisbungee-legacy", "subcommands", "glist", "install").getBoolean(false); + boolean installFind = node.getNode("commands", "redisbungee-legacy", "subcommands", "find", "install").getBoolean(false); + boolean installLastseen = node.getNode("commands", "redisbungee-legacy", "subcommands", "lastseen", "install").getBoolean(false); + boolean installIp = node.getNode("commands", "redisbungee-legacy", "subcommands", "ip", "install").getBoolean(false); + boolean installPproxy = node.getNode("commands", "redisbungee-legacy", "subcommands", "pproxy", "install").getBoolean(false); + boolean installSendToAll = node.getNode("commands", "redisbungee-legacy", "subcommands", "sendtoall", "install").getBoolean(false); + boolean installServerid = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverid", "install").getBoolean(false); + boolean installServerIds = node.getNode("commands", "redisbungee-legacy", "subcommands", "serverids", "install").getBoolean(false); + boolean installPlist = node.getNode("commands", "redisbungee-legacy", "subcommands", "plist", "install").getBoolean(false); + + + RedisBungeeConfiguration configuration = new RedisBungeeConfiguration(networkId, proxyId, exemptAddresses, kickWhenOnline, reconnectToLastServer, handleMotd, new RedisBungeeConfiguration.CommandsConfiguration( + redisBungeeEnabled, redisBungeeLegacyEnabled, + new RedisBungeeConfiguration.LegacySubCommandsConfiguration( + findEnabled, glistEnabled, ipEnabled, + lastseenEnabled, pListEnabled, pproxyEnabled, + sendToAllEnabled, serverIdEnabled, serverIdsEnabled, + installFind, installGlist, installIp, + installLastseen, installPlist, installPproxy, + installSendToAll, installServerid, installServerIds) + )); Summoner summoner; RedisBungeeMode redisBungeeMode; + if (useSSL) { + plugin.logInfo("Using ssl"); + } if (node.getNode("cluster-mode-enabled").getBoolean(false)) { plugin.logInfo("RedisBungee MODE: CLUSTER"); Set hostAndPortSet = new HashSet<>(); @@ -115,7 +171,7 @@ public interface ConfigLoader { throw new RuntimeException("No redis server specified"); } JedisPool jedisPool = null; - if (node.getNode("enable-jedis-pool-compatibility").getBoolean(true)) { + if (node.getNode("enable-jedis-pool-compatibility").getBoolean(false)) { JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(node.getNode("compatibility-max-connections").getInt(3)); config.setBlockWhenExhausted(true); @@ -134,53 +190,5 @@ public interface ConfigLoader { void onConfigLoad(RedisBungeeConfiguration configuration, Summoner summoner, RedisBungeeMode mode); - default ImmutableMap getMessagesFromPath(Path path) throws IOException { - final YAMLConfigurationLoader yamlConfigurationFileLoader = YAMLConfigurationLoader.builder().setPath(path).build(); - ConfigurationNode node = yamlConfigurationFileLoader.load(); - HashMap messages = new HashMap<>(); - messages.put(RedisBungeeConfiguration.MessageType.LOGGED_IN_OTHER_LOCATION, node.getNode("logged-in-other-location").getString("§cLogged in from another location.")); - messages.put(RedisBungeeConfiguration.MessageType.ALREADY_LOGGED_IN, node.getNode("already-logged-in").getString("§cYou are already logged in!")); - return ImmutableMap.copyOf(messages); - } - - default Path createMessagesFile(Path dataFolder) throws IOException { - if (Files.notExists(dataFolder)) { - Files.createDirectory(dataFolder); - } - Path file = dataFolder.resolve("messages.yml"); - if (Files.notExists(file)) { - try (InputStream in = getClass().getClassLoader().getResourceAsStream("messages.yml")) { - Files.createFile(file); - assert in != null; - Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); - } - } - return file; - } - - default Path createConfigFile(Path dataFolder) throws IOException { - if (Files.notExists(dataFolder)) { - Files.createDirectory(dataFolder); - } - Path file = dataFolder.resolve("config.yml"); - if (Files.notExists(file)) { - try (InputStream in = getClass().getClassLoader().getResourceAsStream("config.yml")) { - Files.createFile(file); - assert in != null; - Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); - } - } - return file; - } - - default void handleOldConfig(Path dataFolder) throws IOException { - Path oldConfigFolder = dataFolder.resolve("old_config"); - if (Files.notExists(oldConfigFolder)) { - Files.createDirectory(oldConfigFolder); - } - Path oldConfigPath = dataFolder.resolve("config.yml"); - Files.move(oldConfigPath, oldConfigFolder.resolve(UUID.randomUUID() + "_config.yml")); - createConfigFile(dataFolder); - } } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/GenericConfigLoader.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/GenericConfigLoader.java new file mode 100644 index 0000000..78a5d29 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/GenericConfigLoader.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.config.loaders; + +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; + + +public interface GenericConfigLoader { + + // CHANGES on every reboot + String RANDOM_OLD = "backup-" + Instant.now().getEpochSecond(); + + default Path createConfigFile(Path dataFolder, String configFile, @Nullable String defaultResourceID) throws IOException { + if (Files.notExists(dataFolder)) { + Files.createDirectory(dataFolder); + } + Path file = dataFolder.resolve(configFile); + if (Files.notExists(file) && defaultResourceID != null) { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(defaultResourceID)) { + Files.createFile(file); + assert in != null; + Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING); + } + } + return file; + } + + default void handleOldConfig(Path dataFolder, String configFile, @Nullable String defaultResourceID) throws IOException { + Path oldConfigFolder = dataFolder.resolve("old_config"); + if (Files.notExists(oldConfigFolder)) { + Files.createDirectory(oldConfigFolder); + } + Path randomStoreConfigDirectory = oldConfigFolder.resolve(RANDOM_OLD); + if (Files.notExists(randomStoreConfigDirectory)) { + Files.createDirectory(randomStoreConfigDirectory); + } + Path oldConfigPath = dataFolder.resolve(configFile); + + Files.move(oldConfigPath, randomStoreConfigDirectory.resolve(configFile)); + createConfigFile(dataFolder, configFile, defaultResourceID); + } + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/LangConfigLoader.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/LangConfigLoader.java new file mode 100644 index 0000000..ad30c42 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/config/loaders/LangConfigLoader.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.config.loaders; + +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Locale; + +public interface LangConfigLoader extends GenericConfigLoader { + + int CONFIG_VERSION = 1; + + default void loadLangConfig(RedisBungeePlugin plugin, Path dataFolder) throws IOException { + Path configFile = createConfigFile(dataFolder, "lang.yml", "lang.yml"); + final YAMLConfigurationLoader yamlConfigurationFileLoader = YAMLConfigurationLoader.builder().setPath(configFile).build(); + ConfigurationNode node = yamlConfigurationFileLoader.load(); + if (node.getNode("config-version").getInt(0) != CONFIG_VERSION) { + handleOldConfig(dataFolder, "lang.yml", "lang.yml"); + node = yamlConfigurationFileLoader.load(); + } + // MINI message serializer + MiniMessage miniMessage = MiniMessage.miniMessage(); + + Component prefix = miniMessage.deserialize(node.getNode("prefix").getString("[RedisBungee]")); + Locale defaultLocale = Locale.forLanguageTag(node.getNode("default-locale").getString("en-us")); + boolean useClientLocale = node.getNode("use-client-locale").getBoolean(true); + LangConfiguration.Messages messages = new LangConfiguration.Messages(defaultLocale); + node.getNode("messages").getChildrenMap().forEach((key, childNode) -> childNode.getChildrenMap().forEach((childKey, childChildNode) -> { + messages.register(key.toString(), Locale.forLanguageTag(childKey.toString()), childChildNode.getString()); + })); + messages.test(defaultLocale); + + onLangConfigLoad(new LangConfiguration(prefix, defaultLocale, useClientLocale, messages)); + } + + + void onLangConfigLoad(LangConfiguration langConfiguration); + + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/events/EventsPlatform.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/events/EventsPlatform.java index 099c075..79dabfa 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/events/EventsPlatform.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/events/EventsPlatform.java @@ -17,7 +17,6 @@ import java.util.UUID; * * @author Ham1255 * @since 0.7.0 - * */ public interface EventsPlatform { diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/AbstractPayload.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/AbstractPayload.java new file mode 100644 index 0000000..e41ee5f --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/AbstractPayload.java @@ -0,0 +1,24 @@ + +package com.imaginarycode.minecraft.redisbungee.api.payloads; + +public abstract class AbstractPayload { + + private final String senderProxy; + + public AbstractPayload(String proxyId) { + this.senderProxy = proxyId; + } + + public AbstractPayload(String senderProxy, String className) { + this.senderProxy = senderProxy; + } + + public String senderProxy() { + return senderProxy; + } + + public String getClassName() { + return getClass().getName(); + } + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/gson/AbstractPayloadSerializer.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/gson/AbstractPayloadSerializer.java new file mode 100644 index 0000000..6769ff2 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/gson/AbstractPayloadSerializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.gson; + +import com.google.gson.*; +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; + +import java.lang.reflect.Type; + +public class AbstractPayloadSerializer implements JsonSerializer, JsonDeserializer { + + + @Override + public AbstractPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + return new AbstractPayload(jsonObject.get("proxy").getAsString(), jsonObject.get("class").getAsString()) { + }; + } + + @Override + public JsonElement serialize(AbstractPayload src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("proxy", new JsonPrimitive(src.senderProxy())); + return jsonObject; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/DeathPayload.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/DeathPayload.java new file mode 100644 index 0000000..399071a --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/DeathPayload.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy; + +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; + +public class DeathPayload extends AbstractPayload { + public DeathPayload(String proxyId) { + super(proxyId); + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/HeartbeatPayload.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/HeartbeatPayload.java new file mode 100644 index 0000000..02268fd --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/HeartbeatPayload.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy; + +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; + +public class HeartbeatPayload extends AbstractPayload { + + public record HeartbeatData(long heartbeat, int players) { + + } + + private final HeartbeatData data; + + public HeartbeatPayload(String proxyId, HeartbeatData data) { + super(proxyId); + this.data = data; + } + + public HeartbeatData data() { + return data; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/PubSubPayload.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/PubSubPayload.java new file mode 100644 index 0000000..eaa9092 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/PubSubPayload.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy; + +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; + +public class PubSubPayload extends AbstractPayload { + + private final String channel; + private final String message; + + + public PubSubPayload(String proxyId, String channel, String message) { + super(proxyId); + this.channel = channel; + this.message = message; + } + + public String channel() { + return channel; + } + + public String message() { + return message; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/RunCommandPayload.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/RunCommandPayload.java new file mode 100644 index 0000000..6374e5c --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/RunCommandPayload.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy; + +import com.imaginarycode.minecraft.redisbungee.api.payloads.AbstractPayload; + +public class RunCommandPayload extends AbstractPayload { + + + private final String proxyToRun; + + private final String command; + + + public RunCommandPayload(String proxyId, String proxyToRun, String command) { + super(proxyId); + this.proxyToRun = proxyToRun; + this.command = command; + } + + public String proxyToRun() { + return proxyToRun; + } + + public String command() { + return command; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/DeathPayloadSerializer.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/DeathPayloadSerializer.java new file mode 100644 index 0000000..d77dd51 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/DeathPayloadSerializer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson; + +import com.google.gson.*; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.DeathPayload; + +import java.lang.reflect.Type; + +public class DeathPayloadSerializer implements JsonSerializer, JsonDeserializer { + + private static final Gson gson = new Gson(); + + + @Override + public DeathPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String senderProxy = jsonObject.get("proxy").getAsString(); + return new DeathPayload(senderProxy); + } + + @Override + public JsonElement serialize(DeathPayload src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("proxy", new JsonPrimitive(src.senderProxy())); + return jsonObject; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/HeartbeatPayloadSerializer.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/HeartbeatPayloadSerializer.java new file mode 100644 index 0000000..1f301f2 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/HeartbeatPayloadSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson; + +import com.google.gson.*; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.HeartbeatPayload; + +import java.lang.reflect.Type; + +public class HeartbeatPayloadSerializer implements JsonSerializer, JsonDeserializer { + + + @Override + public HeartbeatPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String senderProxy = jsonObject.get("proxy").getAsString(); + long heartbeat = jsonObject.get("heartbeat").getAsLong(); + int players = jsonObject.get("players").getAsInt(); + return new HeartbeatPayload(senderProxy, new HeartbeatPayload.HeartbeatData(heartbeat, players)); + } + + @Override + public JsonElement serialize(HeartbeatPayload src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("proxy", new JsonPrimitive(src.senderProxy())); + jsonObject.add("heartbeat", new JsonPrimitive(src.data().heartbeat())); + jsonObject.add("players", new JsonPrimitive(src.data().players())); + return jsonObject; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/PubSubPayloadSerializer.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/PubSubPayloadSerializer.java new file mode 100644 index 0000000..01d66a5 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/PubSubPayloadSerializer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson; + +import com.google.gson.*; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.PubSubPayload; + +import java.lang.reflect.Type; + +public class PubSubPayloadSerializer implements JsonSerializer, JsonDeserializer { + + private static final Gson gson = new Gson(); + + + @Override + public PubSubPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String senderProxy = jsonObject.get("proxy").getAsString(); + String channel = jsonObject.get("channel").getAsString(); + String message = jsonObject.get("message").getAsString(); + return new PubSubPayload(senderProxy, channel, message); + } + + @Override + public JsonElement serialize(PubSubPayload src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("proxy", new JsonPrimitive(src.senderProxy())); + jsonObject.add("channel", new JsonPrimitive(src.channel())); + jsonObject.add("message", context.serialize(src.message())); + return jsonObject; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/RunCommandPayloadSerializer.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/RunCommandPayloadSerializer.java new file mode 100644 index 0000000..2a7de33 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/payloads/proxy/gson/RunCommandPayloadSerializer.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.gson; + +import com.google.gson.*; +import com.imaginarycode.minecraft.redisbungee.api.payloads.proxy.RunCommandPayload; + +import java.lang.reflect.Type; + +public class RunCommandPayloadSerializer implements JsonSerializer, JsonDeserializer { + + + @Override + public RunCommandPayload deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject jsonObject = json.getAsJsonObject(); + String senderProxy = jsonObject.get("proxy").getAsString(); + String proxyToRun = jsonObject.get("proxy-to-run").getAsString(); + String command = jsonObject.get("command").getAsString(); + return new RunCommandPayload(senderProxy, proxyToRun, command); + } + + @Override + public JsonElement serialize(RunCommandPayload src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("proxy", new JsonPrimitive(src.senderProxy())); + jsonObject.add("proxy-to-run", new JsonPrimitive(src.proxyToRun())); + jsonObject.add("command", context.serialize(src.command())); + return jsonObject; + } +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/JedisClusterSummoner.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/JedisClusterSummoner.java index 99d8e19..14c2514 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/JedisClusterSummoner.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/JedisClusterSummoner.java @@ -17,7 +17,7 @@ import java.io.IOException; import java.time.Duration; public class JedisClusterSummoner implements Summoner { - public final ClusterConnectionProvider clusterConnectionProvider; + private final ClusterConnectionProvider clusterConnectionProvider; public JedisClusterSummoner(ClusterConnectionProvider clusterConnectionProvider) { this.clusterConnectionProvider = clusterConnectionProvider; @@ -35,6 +35,8 @@ public class JedisClusterSummoner implements Summoner { @Override public JedisCluster obtainResource() { - return new NotClosableJedisCluster(this.clusterConnectionProvider, 60, Duration.ofSeconds(30000)); + return new NotClosableJedisCluster(this.clusterConnectionProvider, 60, Duration.ofSeconds(10)); } + + } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/NotClosableJedisCluster.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/NotClosableJedisCluster.java index 84eb85a..5e09859 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/NotClosableJedisCluster.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/NotClosableJedisCluster.java @@ -11,9 +11,7 @@ package com.imaginarycode.minecraft.redisbungee.api.summoners; import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.JedisPooled; import redis.clients.jedis.providers.ClusterConnectionProvider; -import redis.clients.jedis.providers.PooledConnectionProvider; import java.time.Duration; diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/Summoner.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/Summoner.java index 6b511e7..36beac5 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/Summoner.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/summoners/Summoner.java @@ -10,6 +10,8 @@ package com.imaginarycode.minecraft.redisbungee.api.summoners; +import redis.clients.jedis.UnifiedJedis; + import java.io.Closeable; @@ -18,9 +20,8 @@ import java.io.Closeable; * * @author Ham1255 * @since 0.7.0 - * */ -public interface Summoner

extends Closeable { +public interface Summoner

extends Closeable { P obtainResource(); diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/HeartbeatTask.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/HeartbeatTask.java deleted file mode 100644 index 669ba8c..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/HeartbeatTask.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api.tasks; - -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.UnifiedJedis; -import redis.clients.jedis.exceptions.JedisConnectionException; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -public class HeartbeatTask extends RedisTask{ - - public final static TimeUnit REPEAT_INTERVAL_TIME_UNIT = TimeUnit.SECONDS; - public final static int INTERVAL = 1; - private final AtomicInteger globalPlayerCount; - - public HeartbeatTask(RedisBungeePlugin plugin, AtomicInteger globalPlayerCount) { - super(plugin); - this.globalPlayerCount = globalPlayerCount; - } - - - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - long redisTime = plugin.getRedisTime(unifiedJedis); - unifiedJedis.hset("heartbeats", plugin.getConfiguration().getProxyId(), String.valueOf(redisTime)); - } catch (JedisConnectionException e) { - // Redis server has disappeared! - plugin.logFatal("Unable to update heartbeat - did your Redis server go away?"); - e.printStackTrace(); - return null; - } - try { - plugin.updateProxiesIds(); - globalPlayerCount.set(plugin.getCurrentCount()); - } catch (Throwable e) { - plugin.logFatal("Unable to update data - did your Redis server go away?"); - e.printStackTrace(); - } - return null; - } - - - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/InitialUtils.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/InitialUtils.java deleted file mode 100644 index 8a2986f..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/InitialUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api.tasks; - -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import com.imaginarycode.minecraft.redisbungee.api.util.RedisUtil; -import redis.clients.jedis.Protocol; -import redis.clients.jedis.UnifiedJedis; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public class InitialUtils { - - public static void checkRedisVersion(RedisBungeePlugin plugin) { - new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - // This is more portable than INFO

- String info = new String((byte[]) unifiedJedis.sendCommand(Protocol.Command.INFO)); - for (String s : info.split("\r\n")) { - if (s.startsWith("redis_version:")) { - String version = s.split(":")[1]; - plugin.logInfo("Redis server version: " + version); - if (!RedisUtil.isRedisVersionRight(version)) { - plugin.logFatal("Your version of Redis (" + version + ") is not at least version 3.0 RedisBungee requires a newer version of Redis."); - throw new RuntimeException("Unsupported Redis version detected"); - } - long uuidCacheSize = unifiedJedis.hlen("uuid-cache"); - if (uuidCacheSize > 750000) { - plugin.logInfo("Looks like you have a really big UUID cache! Run https://github.com/ProxioDev/Brains"); - } - break; - } - } - return null; - } - }.execute(); - } - - - public static void checkIfRecovering(RedisBungeePlugin plugin, Path dataFolder) { - new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - Path crashFile = dataFolder.resolve("restarted_from_crash.txt"); - if (Files.exists(crashFile)) { - try { - Files.delete(crashFile); - } catch (IOException e) { - throw new RuntimeException(e); - } - plugin.logInfo("crash file was deleted continuing RedisBungee startup "); - } else if (unifiedJedis.hexists("heartbeats", plugin.getConfiguration().getProxyId())) { - try { - long value = Long.parseLong(unifiedJedis.hget("heartbeats", plugin.getConfiguration().getProxyId())); - long redisTime = plugin.getRedisTime(unifiedJedis); - - if (redisTime < value + RedisUtil.PROXY_TIMEOUT) { - logImposter(plugin); - throw new RuntimeException("Possible impostor instance!"); - } - } catch (NumberFormatException ignored) { - } - } - return null; - } - }.execute(); - } - - private static void logImposter(RedisBungeePlugin plugin) { - plugin.logFatal("You have launched a possible impostor Velocity / Bungeecord instance. Another instance is already running."); - plugin.logFatal("For data consistency reasons, RedisBungee will now disable itself."); - plugin.logFatal("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."); - } - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/IntegrityCheckTask.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/IntegrityCheckTask.java deleted file mode 100644 index c13742e..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/IntegrityCheckTask.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api.tasks; - -import com.imaginarycode.minecraft.redisbungee.api.util.player.PlayerUtils; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import redis.clients.jedis.UnifiedJedis; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -public abstract class IntegrityCheckTask extends RedisTask { - - public static int INTERVAL = 30; - public static TimeUnit TIMEUNIT = TimeUnit.SECONDS; - - - public IntegrityCheckTask(RedisBungeePlugin plugin) { - super(plugin); - } - - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - Set players = plugin.getLocalPlayersAsUuidStrings(); - Set playersInRedis = unifiedJedis.smembers("proxy:" + plugin.getConfiguration().getProxyId() + ":usersOnline"); - List lagged = plugin.getCurrentProxiesIds(true); - - // Clean up lagged players. - for (String s : lagged) { - Set laggedPlayers = unifiedJedis.smembers("proxy:" + s + ":usersOnline"); - unifiedJedis.del("proxy:" + s + ":usersOnline"); - if (!laggedPlayers.isEmpty()) { - plugin.logInfo("Cleaning up lagged proxy " + s + " (" + laggedPlayers.size() + " players)..."); - for (String laggedPlayer : laggedPlayers) { - PlayerUtils.cleanUpPlayer(laggedPlayer, unifiedJedis, true); - } - } - } - - Set absentLocally = new HashSet<>(playersInRedis); - absentLocally.removeAll(players); - Set absentInRedis = new HashSet<>(players); - absentInRedis.removeAll(playersInRedis); - - for (String member : absentLocally) { - boolean found = false; - for (String proxyId : plugin.getProxiesIds()) { - if (proxyId.equals(plugin.getConfiguration().getProxyId())) continue; - if (unifiedJedis.sismember("proxy:" + proxyId + ":usersOnline", member)) { - // Just clean up the set. - found = true; - break; - } - } - if (!found) { - PlayerUtils.cleanUpPlayer(member, unifiedJedis, false); - plugin.logWarn("Player found in set that was not found locally and globally: " + member); - } else { - unifiedJedis.srem("proxy:" + plugin.getConfiguration().getProxyId() + ":usersOnline", member); - plugin.logWarn("Player found in set that was not found locally, but is on another proxy: " + member); - } - } - // due unifiedJedis does not support pipelined. - //Pipeline pipeline = jedis.pipelined(); - - for (String player : absentInRedis) { - // Player not online according to Redis but not BungeeCord. - handlePlatformPlayer(player, unifiedJedis); - } - } catch (Throwable e) { - plugin.logFatal("Unable to fix up stored player data"); - e.printStackTrace(); - } - return null; - } - - - public abstract void handlePlatformPlayer(String player, UnifiedJedis unifiedJedis); - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisPipelineTask.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisPipelineTask.java new file mode 100644 index 0000000..21a5d29 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisPipelineTask.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.tasks; + +import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import redis.clients.jedis.*; + +public abstract class RedisPipelineTask extends RedisTask { + + + public RedisPipelineTask(AbstractRedisBungeeAPI api) { + super(api); + } + + public RedisPipelineTask(RedisBungeePlugin plugin) { + super(plugin); + } + + + @Override + public T unifiedJedisTask(UnifiedJedis unifiedJedis) { + if (unifiedJedis instanceof JedisPooled pooled) { + try (Pipeline pipeline = pooled.pipelined()) { + return doPooledPipeline(pipeline); + } + } else if (unifiedJedis instanceof JedisCluster jedisCluster) { + try (ClusterPipeline pipeline = jedisCluster.pipelined()) { + return clusterPipeline(pipeline); + } + } + + return null; + } + + public abstract T doPooledPipeline(Pipeline pipeline); + + public abstract T clusterPipeline(ClusterPipeline pipeline); + + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisTask.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisTask.java index eb1b416..9a6da17 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisTask.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/RedisTask.java @@ -11,11 +11,11 @@ package com.imaginarycode.minecraft.redisbungee.api.tasks; import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisClusterSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.JedisPooledSummoner; import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; import redis.clients.jedis.UnifiedJedis; import java.util.concurrent.Callable; @@ -27,23 +27,22 @@ import java.util.concurrent.Callable; public abstract class RedisTask implements Runnable, Callable { protected final Summoner summoner; - protected final AbstractRedisBungeeAPI api; - protected RedisBungeePlugin plugin; + + protected final RedisBungeeMode mode; @Override public V call() throws Exception { - return execute(); + return this.execute(); } public RedisTask(AbstractRedisBungeeAPI api) { - this.api = api; this.summoner = api.getSummoner(); + this.mode = api.getMode(); } public RedisTask(RedisBungeePlugin plugin) { - this.plugin = plugin; - this.api = plugin.getAbstractRedisBungeeApi(); - this.summoner = api.getSummoner(); + this.summoner = plugin.getSummoner(); + this.mode = plugin.getRedisBungeeMode(); } public abstract V unifiedJedisTask(UnifiedJedis unifiedJedis); @@ -53,22 +52,16 @@ public abstract class RedisTask implements Runnable, Callable { this.execute(); } - public V execute(){ + public V execute() { // JedisCluster, JedisPooled in fact is just UnifiedJedis does not need new instance since its single instance anyway. - if (api.getMode() == RedisBungeeMode.SINGLE) { + if (mode == RedisBungeeMode.SINGLE) { JedisPooledSummoner jedisSummoner = (JedisPooledSummoner) summoner; return this.unifiedJedisTask(jedisSummoner.obtainResource()); - } else if (api.getMode() == RedisBungeeMode.CLUSTER) { + } else if (mode == RedisBungeeMode.CLUSTER) { JedisClusterSummoner jedisClusterSummoner = (JedisClusterSummoner) summoner; return this.unifiedJedisTask(jedisClusterSummoner.obtainResource()); } return null; } - public RedisBungeePlugin getPlugin() { - if (plugin == null) { - throw new NullPointerException("Plugin is null in the task"); - } - return plugin; - } } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/ShutdownUtils.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/ShutdownUtils.java deleted file mode 100644 index a3fdbcc..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/ShutdownUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.api.tasks; - -import com.imaginarycode.minecraft.redisbungee.api.util.player.PlayerUtils; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.UnifiedJedis; - -import java.util.Set; - -public class ShutdownUtils { - - public static void shutdownCleanup(RedisBungeePlugin plugin) { - new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - unifiedJedis.hdel("heartbeats", plugin.getConfiguration().getProxyId()); - if (unifiedJedis.scard("proxy:" + plugin.getConfiguration().getProxyId() + ":usersOnline") > 0) { - Set players = unifiedJedis.smembers("proxy:" + plugin.getConfiguration().getProxyId() + ":usersOnline"); - for (String member : players) - PlayerUtils.cleanUpPlayer(member, unifiedJedis, true); - } - return null; - } - }.execute(); - } - - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/UUIDCleanupTask.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/UUIDCleanupTask.java new file mode 100644 index 0000000..6e080c4 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/tasks/UUIDCleanupTask.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.tasks; + +import com.google.gson.Gson; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.util.uuid.CachedUUIDEntry; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.exceptions.JedisException; + +import java.util.ArrayList; + + +public class UUIDCleanupTask extends RedisTask{ + + private final Gson gson = new Gson(); + private final RedisBungeePlugin plugin; + + public UUIDCleanupTask(RedisBungeePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + // this code is inspired from https://github.com/minecrafter/redisbungeeclean + @Override + public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { + try { + final long number = unifiedJedis.hlen("uuid-cache"); + plugin.logInfo("Found {} entries", number); + ArrayList fieldsToRemove = new ArrayList<>(); + unifiedJedis.hgetAll("uuid-cache").forEach((field, data) -> { + CachedUUIDEntry cachedUUIDEntry = gson.fromJson(data, CachedUUIDEntry.class); + if (cachedUUIDEntry.expired()) { + fieldsToRemove.add(field); + } + }); + if (!fieldsToRemove.isEmpty()) { + unifiedJedis.hdel("uuid-cache", fieldsToRemove.toArray(new String[0])); + } + plugin.logInfo("deleted {} entries", fieldsToRemove.size()); + } catch (JedisException e) { + plugin.logFatal("There was an error fetching information", e); + } + return null; + } + + +} \ No newline at end of file diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/InitialUtils.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/InitialUtils.java new file mode 100644 index 0000000..8ebf8d0 --- /dev/null +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/InitialUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.api.util; + +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.UnifiedJedis; + + +public class InitialUtils { + + public static void checkRedisVersion(RedisBungeePlugin plugin) { + new RedisTask(plugin) { + @Override + public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { + // This is more portable than INFO
+ String info = new String((byte[]) unifiedJedis.sendCommand(Protocol.Command.INFO)); + for (String s : info.split("\r\n")) { + if (s.startsWith("redis_version:")) { + String version = s.split(":")[1]; + plugin.logInfo("Redis server version: " + version); + if (!RedisUtil.isRedisVersionRight(version)) { + plugin.logFatal("Your version of Redis (" + version + ") is not at least version " + RedisUtil.MAJOR_VERSION + "." + RedisUtil.MINOR_VERSION + " RedisBungee requires a newer version of Redis."); + throw new RuntimeException("Unsupported Redis version detected"); + } + long uuidCacheSize = unifiedJedis.hlen("uuid-cache"); + if (uuidCacheSize > 750000) { + plugin.logInfo("Looks like you have a really big UUID cache! Run https://github.com/ProxioDev/Brains"); + } + break; + } + } + return null; + } + }.execute(); + } + + +} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/RedisUtil.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/RedisUtil.java index 9e4bd92..7e337db 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/RedisUtil.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/RedisUtil.java @@ -5,6 +5,10 @@ import com.google.common.annotations.VisibleForTesting; @VisibleForTesting public class RedisUtil { public final static int PROXY_TIMEOUT = 30; + + public static final int MAJOR_VERSION = 6; + public static final int MINOR_VERSION = 2; + public static boolean isRedisVersionRight(String redisVersion) { String[] args = redisVersion.split("\\."); if (args.length < 2) { @@ -12,7 +16,10 @@ public class RedisUtil { } int major = Integer.parseInt(args[0]); int minor = Integer.parseInt(args[1]); - return major >= 3 && minor >= 0; + + if (major > MAJOR_VERSION) return true; + return major == MAJOR_VERSION && minor >= MINOR_VERSION; + } // Ham1255: i am keeping this if some plugin uses this *IF* diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/io/IOUtil.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/io/IOUtil.java deleted file mode 100644 index fa4290e..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/io/IOUtil.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.imaginarycode.minecraft.redisbungee.api.util.io; - -import com.google.common.io.ByteStreams; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - - -public class IOUtil { - public static String readInputStreamAsString(InputStream is) { - String string; - try { - string = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new AssertionError(e); - } - return string; - } -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/payload/PayloadUtils.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/payload/PayloadUtils.java deleted file mode 100644 index 36e9b78..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/payload/PayloadUtils.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.imaginarycode.minecraft.redisbungee.api.util.payload; - -import com.google.gson.Gson; -import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; -import com.imaginarycode.minecraft.redisbungee.api.AbstractDataManager; -import redis.clients.jedis.UnifiedJedis; - -import java.net.InetAddress; -import java.util.UUID; - -public class PayloadUtils { - private static final Gson gson = new Gson(); - - public static void playerJoinPayload(UUID uuid, UnifiedJedis unifiedJedis, InetAddress inetAddress) { - unifiedJedis.publish("redisbungee-data", gson.toJson(new AbstractDataManager.DataManagerMessage<>( - uuid, AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId(), AbstractDataManager.DataManagerMessage.Action.JOIN, - new AbstractDataManager.LoginPayload(inetAddress)))); - } - - - public static void playerQuitPayload(String uuid, UnifiedJedis unifiedJedis, long timestamp) { - unifiedJedis.publish("redisbungee-data", gson.toJson(new AbstractDataManager.DataManagerMessage<>( - UUID.fromString(uuid), AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId(), AbstractDataManager.DataManagerMessage.Action.LEAVE, - new AbstractDataManager.LogoutPayload(timestamp)))); - } - - - - public static void playerServerChangePayload(UUID uuid, UnifiedJedis unifiedJedis, String newServer, String oldServer) { - unifiedJedis.publish("redisbungee-data", gson.toJson(new AbstractDataManager.DataManagerMessage<>( - uuid, AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId(), AbstractDataManager.DataManagerMessage.Action.SERVER_CHANGE, - new AbstractDataManager.ServerChangePayload(newServer, oldServer)))); - } - - - public static void kickPlayerPayload(UUID uuid, String message, UnifiedJedis unifiedJedis) { - unifiedJedis.publish("redisbungee-data", gson.toJson(new AbstractDataManager.DataManagerMessage<>( - uuid, AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId(), AbstractDataManager.DataManagerMessage.Action.KICK, - new AbstractDataManager.KickPayload(message)))); - } -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/player/PlayerUtils.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/player/PlayerUtils.java deleted file mode 100644 index 820ad34..0000000 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/player/PlayerUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.imaginarycode.minecraft.redisbungee.api.util.player; - -import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; -import redis.clients.jedis.UnifiedJedis; - -import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -import static com.imaginarycode.minecraft.redisbungee.api.util.payload.PayloadUtils.playerJoinPayload; -import static com.imaginarycode.minecraft.redisbungee.api.util.payload.PayloadUtils.playerQuitPayload; - -public class PlayerUtils { - - public static void cleanUpPlayer(String uuid, UnifiedJedis rsc, boolean firePayload) { - final long timestamp = System.currentTimeMillis(); - final boolean isKickedFromOtherLocation = isKickedOtherLocation(uuid, rsc); - rsc.srem("proxy:" + AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId() + ":usersOnline", uuid); - if (!isKickedFromOtherLocation) { - rsc.hdel("player:" + uuid, "server", "ip", "proxy"); - rsc.hset("player:" + uuid, "online", String.valueOf(timestamp)); - } - if (firePayload && !isKickedFromOtherLocation) { - playerQuitPayload(uuid, rsc, timestamp); - } - } - - public static void setKickedOtherLocation(String uuid, UnifiedJedis unifiedJedis) { - // set anything for sake of exists check. then expire it after 2 seconds. should be great? - unifiedJedis.set("kicked-other-location::" + uuid, "0"); - unifiedJedis.expire("kicked-other-location::" + uuid, 2); - } - - public static boolean isKickedOtherLocation(String uuid, UnifiedJedis unifiedJedis) { - return unifiedJedis.exists("kicked-other-location::" + uuid); - } - - - public static void createPlayer(UUID uuid, UnifiedJedis unifiedJedis, String currentServer, InetAddress hostname, boolean fireEvent) { - final boolean isKickedFromOtherLocation = isKickedOtherLocation(uuid.toString(), unifiedJedis); - Map playerData = new HashMap<>(4); - playerData.put("online", "0"); - playerData.put("ip", hostname.getHostName()); - playerData.put("proxy", AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId()); - if (currentServer != null) { - playerData.put("server", currentServer); - } - unifiedJedis.sadd("proxy:" + AbstractRedisBungeeAPI.getAbstractRedisBungeeAPI().getProxyId() + ":usersOnline", uuid.toString()); - unifiedJedis.hset("player:" + uuid, playerData); - if (fireEvent && !isKickedFromOtherLocation) { - playerJoinPayload(uuid, unifiedJedis, hostname); - } - } - - -} diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/Serializations.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/MultiMapSerialization.java similarity index 97% rename from RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/Serializations.java rename to RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/MultiMapSerialization.java index 7ee9cc5..71df20b 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/Serializations.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/serialize/MultiMapSerialization.java @@ -7,7 +7,7 @@ import com.google.common.io.ByteArrayDataOutput; import java.util.Collection; import java.util.Map; -public class Serializations { +public class MultiMapSerialization { public static void serializeMultiset(Multiset collection, ByteArrayDataOutput output) { output.writeInt(collection.elementSet().size()); @@ -36,4 +36,5 @@ public class Serializations { output.writeUTF(o.toString()); } } + } diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/NameFetcher.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/NameFetcher.java index 69eb689..3bcd38c 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/NameFetcher.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/NameFetcher.java @@ -22,38 +22,38 @@ import java.util.List; import java.util.UUID; public class NameFetcher { - private static OkHttpClient httpClient; - private static final Gson gson = new Gson(); + private static OkHttpClient httpClient; + private static final Gson gson = new Gson(); - public static void setHttpClient(OkHttpClient httpClient) { - NameFetcher.httpClient = httpClient; - } + public static void setHttpClient(OkHttpClient httpClient) { + NameFetcher.httpClient = httpClient; + } - public static List nameHistoryFromUuid(UUID uuid) throws IOException { - String name = getName(uuid); - if (name == null) return Collections.emptyList(); - return Collections.singletonList(name); - } + public static List nameHistoryFromUuid(UUID uuid) throws IOException { + String name = getName(uuid); + if (name == null) return Collections.emptyList(); + return Collections.singletonList(name); + } - public static String getName(UUID uuid) throws IOException { - String url = "https://playerdb.co/api/player/minecraft/" + uuid.toString(); - Request request = new Request.Builder() - .addHeader("User-Agent", "RedisBungee-ProxioDev") - .url(url) - .get() - .build(); - ResponseBody body = httpClient.newCall(request).execute().body(); - String response = body.string(); - body.close(); + public static String getName(UUID uuid) throws IOException { + String url = "https://playerdb.co/api/player/minecraft/" + uuid.toString(); + Request request = new Request.Builder() + .addHeader("User-Agent", "RedisBungee-ProxioDev") + .url(url) + .get() + .build(); + ResponseBody body = httpClient.newCall(request).execute().body(); + String response = body.string(); + body.close(); - JsonObject json = gson.fromJson(response, JsonObject.class); - if (!json.has("success") || !json.get("success").getAsBoolean()) return null; - if (!json.has("data")) return null; - JsonObject data = json.getAsJsonObject("data"); - if (!data.has("player")) return null; - JsonObject player = data.getAsJsonObject("player"); - if (!player.has("username")) return null; + JsonObject json = gson.fromJson(response, JsonObject.class); + if (!json.has("success") || !json.get("success").getAsBoolean()) return null; + if (!json.has("data")) return null; + JsonObject data = json.getAsJsonObject("data"); + if (!data.has("player")) return null; + JsonObject player = data.getAsJsonObject("player"); + if (!player.has("username")) return null; - return player.get("username").getAsString(); - } + return player.get("username").getAsString(); + } } \ No newline at end of file diff --git a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/UUIDTranslator.java b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/UUIDTranslator.java index 7453074..acccf40 100644 --- a/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/UUIDTranslator.java +++ b/RedisBungee-API/src/main/java/com/imaginarycode/minecraft/redisbungee/api/util/uuid/UUIDTranslator.java @@ -14,13 +14,15 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; - import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; import org.checkerframework.checker.nullness.qual.NonNull; import redis.clients.jedis.UnifiedJedis; import redis.clients.jedis.exceptions.JedisException; -import java.util.*; +import java.util.Calendar; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; diff --git a/RedisBungee-API/src/main/resources/config.yml b/RedisBungee-API/src/main/resources/config.yml index 85ef226..786efd9 100644 --- a/RedisBungee-API/src/main/resources/config.yml +++ b/RedisBungee-API/src/main/resources/config.yml @@ -1,7 +1,14 @@ # RedisBungee configuration file. -# Get Redis from http://redis.io/ +# Notice: +# Redis 7.2.4 is last free and open source Redis version after license change +# https://download.redis.io/releases/redis-7.2.4.tar.gz which you have to compile yourself, +# unless your package manager still provide it. +# Here is The alternatives +# - 'ValKey' By linux foundation https://valkey.io/download/ +# - 'KeyDB' by Snapchat inc https://docs.keydb.dev/docs/download/ -# The Redis server you use. + +# The 'Redis', 'ValKey', 'KeyDB' server you will use. # these settings are ignored when cluster mode is enabled. redis-server: 127.0.0.1 redis-port: 6379 @@ -12,7 +19,7 @@ cluster-mode-enabled: false # FORMAT: # redis-cluster-servers: -# - host: 127.0.0.1 +# - host: 127.0.0.1` # port: 2020 # - host: 127.0.0.1 # port: 2021 @@ -25,11 +32,10 @@ redis-cluster-servers: - host: 127.0.0.1 port: 6379 -# THIS FEATURE IS REDIS V6+ # OPTIONAL: if your redis uses acl usernames set the username here. leave empty for no username. redis-username: "" -# OPTIONAL but recommended: If your Redis server uses AUTH, set the password required. +# OPTIONAL but recommended: If your Redis server uses AUTH, set the required password. redis-password: "" # Maximum connections that will be maintained to the Redis server. @@ -37,44 +43,100 @@ redis-password: "" # inefficient plugins or a lot of players. max-redis-connections: 10 -# since redis can support ssl by version 6 you can use ssl / tls in redis bungee too! +# since redis can support ssl by version 6 you can use SSL/TLS in redis bungee too! # but there is more configuration needed to work see https://github.com/ProxioDev/RedisBungee/issues/18 # Keep note that SSL/TLS connections will decrease redis performance so use it when needed. useSSL: false -# An identifier for this BungeeCord / Velocity instance. Will randomly generate if leaving it blank. -proxy-id: "test-1" +# An identifier for this network, which helps to separate redisbungee instances on same redis instance. +# You can use environment variable 'REDISBUNGEE_NETWORK_ID' to override +network-id: "main" -# since version 0.8.0 Internally now uses JedisPooled instead of Jedis, JedisPool. +# An identifier for this BungeeCord / Velocity instance. Will randomly generate if leaving it blank. +# You can set Environment variable 'REDISBUNGEE_PROXY_ID' to override +proxy-id: "proxy-1" + +# since RedisBungee Internally now uses UnifiedJedis instead of Jedis, JedisPool. # which will break compatibility with old plugins that uses RedisBungee JedisPool -# so to mitigate this issue, we will instruct RedisBungee to init an JedisPool for compatibility reasons. -# enabled by default -# ignored when cluster mode is enabled -enable-jedis-pool-compatibility: true +# so to mitigate this issue, RedisBungee will create an JedisPool for compatibility reasons. +# disabled by default +# Automatically disabled when cluster mode is enabled +enable-jedis-pool-compatibility: false + # max connections for the compatibility pool compatibility-max-connections: 3 -# Register redis bungee legacy commands -# if this disabled override-bungee-commands will be ignored -register-legacy-commands: false +# restore old login behavior before 0.9.0 update +# enabled by default +# when true: when player login and there is old player with same uuid it will get disconnected as result and new player will log in +# when false: when a player login but login will fail because old player is still connected. +kick-when-online: true -# Whether or not RedisBungee should install its version of regular BungeeCord commands. -# Often, the RedisBungee commands are desired, but in some cases someone may wish to -# override the commands using another plugin. -# -# If you are just denying access to the commands, RedisBungee uses the default BungeeCord -# permissions - just deny them and access will be denied. -# -# Please note that with build 787+, most commands overridden by RedisBungee were moved to -# modules, and these must be disabled or overridden yourself. -override-bungee-commands: false +# enabled by default +# this option tells RedisBungee handle motd and set online count, when motd is requested +# you can disable this when you want to handle motd yourself, use RedisBungee api to get total players when needed :) +handle-motd: true # A list of IP addresses for which RedisBungee will not modify the response for, useful for automatic # restart scripts. +# Automatically disabled if handle-motd is disabled. exempt-ip-addresses: [] -# restore old login when online behavior before 0.9.0 update -disable-kick-when-online: false +# disabled by default +# RedisBungee will attempt to connect player to last server that was stored. +reconnect-to-last-server: false + +# For redis bungee legacy commands +# either can be run using '/rbl glist' for example +# or if 'install' is set to true '/glist' can be used. +# 'install' also overrides the proxy installed commands +# +# In legacy commands each command got it own permissions since they had it own permission pre new command system, +# so it's also applied to subcommands in '/rbl'. +commands: + # Permission redisbungee.legacy.use + redisbungee-legacy: + enabled: false + subcommands: + # Permission redisbungee.command.glist + glist: + enabled: false + install: false + # Permission redisbungee.command.find + find: + enabled: false + install: false + # Permission redisbungee.command.lastseen + lastseen: + enabled: false + install: false + # Permission redisbungee.command.ip + ip: + enabled: false + install: false + # Permission redisbungee.command.pproxy + pproxy: + enabled: false + install: false + # Permission redisbungee.command.sendtoall + sendtoall: + enabled: false + install: false + # Permission redisbungee.command.serverid + serverid: + enabled: false + install: false + # Permission redisbungee.command.serverids + serverids: + enabled: false + install: false + # Permission redisbungee.command.plist + plist: + enabled: false + install: false + # Permission redisbungee.command.use + redisbungee: + enabled: true # Config version DO NOT CHANGE!!!! -config-version: 1 +config-version: 2 diff --git a/RedisBungee-API/src/main/resources/lang.yml b/RedisBungee-API/src/main/resources/lang.yml new file mode 100644 index 0000000..817a053 --- /dev/null +++ b/RedisBungee-API/src/main/resources/lang.yml @@ -0,0 +1,55 @@ +# this config file is for messages / Languages +# use MiniMessage format https://docs.advntr.dev/minimessage/format.html +# for colors etc... Legacy chat color is not supported. + +# Language codes used in minecraft from the minecraft wiki +# example: en-us for american english and ar-sa for arabic + +# all codes can be obtained from link below +# from the colum Locale Code -> In-game +# NOTE: minecraft wiki shows languages like this `en_us` in config it should be `en-us` +# https://minecraft.wiki/w/Language + +# example: +# lets assume we want to add arabic language. +# messages: +# logged-in-other-location: +# en-us: "You logged in from another location!" +# ar-sa: "لقد اتصلت من مكان اخر" + + +# RedisBungee Prefix if ever used. +prefix: "[RedisBungee]" + +# en-us is american English, Which is the default language used when a language for a message isn't defined. +# Warning: IF THE set default locale wasn't defined in the config for all messages, plugin will not load. +# set the Default locale +default-locale: en-us + +# send language based on client sent settings +# if you don't have languages configured For client Language +# it will default to language that has been set above +# NOTE: due minecraft protocol not sending player settings during login, +# some of the messages like logged-in-other-location will +# skip translation and use default locale that has been set in default-locale. +use-client-locale: true + +# messages that are used during login, and connecting to Last server +messages: + logged-in-other-location: + en-us: "You logged in from another location!" + pt-br: "Você está logado em outra localização!" + already-logged-in: + en-us: "You are already logged in!" + pt-br: "Você já está logado!" + server-not-found: + # placeholder displays server name in the message. + en-us: "unable to connect you to the last server, because server was not found." + pt-br: "falha ao conectar você ao último servidor, porque o servidor não foi encontrado." + server-connecting: + # placeholder displays server name in the message. + en-us: "Connecting you to ..." + pt-br: "Conectando você a ..." + +# DO NOT CHANGE!!!!! +config-version: 1 diff --git a/RedisBungee-API/src/main/resources/messages.yml b/RedisBungee-API/src/main/resources/messages.yml deleted file mode 100644 index a1b1853..0000000 --- a/RedisBungee-API/src/main/resources/messages.yml +++ /dev/null @@ -1,2 +0,0 @@ -logged-in-other-location: "§cYou logged in from another location!" -already-logged-in: "§cYou are already logged in!" \ No newline at end of file diff --git a/RedisBungee-Bungee/build.gradle.kts b/RedisBungee-Bungee/build.gradle.kts index 78cf66f..217be17 100644 --- a/RedisBungee-Bungee/build.gradle.kts +++ b/RedisBungee-Bungee/build.gradle.kts @@ -5,18 +5,17 @@ plugins { id("xyz.jpenilla.run-waterfall") version "2.0.0" } - -repositories { - mavenCentral() - maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } // bungeecord -} -val bungeecordApiVersion = "1.19-R0.1-SNAPSHOT" dependencies { api(project(":RedisBungee-API")) - compileOnly("net.md-5:bungeecord-api:$bungeecordApiVersion") { + compileOnly(libs.platform.bungeecord) { exclude("com.google.guava", "guava") exclude("com.google.code.gson", "gson") + exclude("net.kyori","adventure-api") } + implementation(libs.adventure.platforms.bungeecord) + implementation(libs.adventure.gson) + implementation(libs.acf.bungeecord) + implementation(project(":RedisBungee-Commands")) } description = "RedisBungee Bungeecord implementation" @@ -40,11 +39,13 @@ tasks { options.linksOffline("https://ci.limework.net/RedisBungee/RedisBungee-API/build/docs/javadoc", apiDocs.path) } runWaterfall { - waterfallVersion("1.19") + waterfallVersion("1.20") + environment["REDISBUNGEE_PROXY_ID"] = "bungeecord-1" + environment["REDISBUNGEE_NETWORK_ID"] = "dev" } compileJava { options.encoding = Charsets.UTF_8.name() - options.release.set(8) + options.release.set(17) } javadoc { options.encoding = Charsets.UTF_8.name() @@ -73,6 +74,10 @@ tasks { relocate("com.google.gson", "com.imaginarycode.minecraft.redisbungee.internal.com.google.gson") relocate("com.google.j2objc", "com.imaginarycode.minecraft.redisbungee.internal.com.google.j2objc") relocate("com.google.thirdparty", "com.imaginarycode.minecraft.redisbungee.internal.com.google.thirdparty") + relocate("com.github.benmanes.caffeine", "com.imaginarycode.minecraft.redisbungee.internal.caffeine") + // acf shade + relocate("co.aikar.commands", "com.imaginarycode.minecraft.redisbungee.internal.acf.commands") + } } diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeCommandPlatformHelper.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeCommandPlatformHelper.java new file mode 100644 index 0000000..019d06f --- /dev/null +++ b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeCommandPlatformHelper.java @@ -0,0 +1,17 @@ +package com.imaginarycode.minecraft.redisbungee; + +import co.aikar.commands.BungeeCommandIssuer; +import co.aikar.commands.CommandIssuer; +import com.imaginarycode.minecraft.redisbungee.commands.utils.CommandPlatformHelper; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; + +public class BungeeCommandPlatformHelper extends CommandPlatformHelper { + + @Override + public void sendMessage(CommandIssuer issuer, Component component) { + BungeeCommandIssuer bIssuer = (BungeeCommandIssuer) issuer; + bIssuer.getIssuer().sendMessage(BungeeComponentSerializer.get().serialize(component)); + } + +} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java deleted file mode 100644 index dea9185..0000000 --- a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeeDataManager.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee; - -import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; -import com.imaginarycode.minecraft.redisbungee.api.AbstractDataManager; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.TextComponent; -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; - -import java.util.UUID; - -public class BungeeDataManager extends AbstractDataManager implements Listener { - - public BungeeDataManager(RedisBungeePlugin plugin) { - super(plugin); - } - - @Override - @EventHandler - public void onPostLogin(PostLoginEvent event) { - invalidate(event.getPlayer().getUniqueId()); - } - - @Override - @EventHandler - public void onPlayerDisconnect(PlayerDisconnectEvent event) { - invalidate(event.getPlayer().getUniqueId()); - } - - @Override - @EventHandler - public void onPubSubMessage(PubSubMessageEvent event) { - handlePubSubMessage(event.getChannel(), event.getMessage()); - } - - @Override - public boolean handleKick(UUID target, String message) { - // check if the player is online on this proxy - ProxiedPlayer player = plugin.getPlayer(target); - if (player == null) return false; - player.disconnect(TextComponent.fromLegacyText(message)); - return true; - } -} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerDataManager.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerDataManager.java new file mode 100644 index 0000000..8540005 --- /dev/null +++ b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerDataManager.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.api.PlayerDataManager; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PlayerLeftNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.LoginEvent; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.ServerConnectedEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventHandler; + +import java.util.concurrent.TimeUnit; + + +public class BungeePlayerDataManager extends PlayerDataManager implements Listener { + + public BungeePlayerDataManager(RedisBungeePlugin plugin) { + super(plugin); + } + + @Override + @EventHandler + public void onPlayerChangedServerNetworkEvent(PlayerChangedServerNetworkEvent event) { + super.handleNetworkPlayerServerChange(event); + } + + @Override + @EventHandler + public void onNetworkPlayerQuit(PlayerLeftNetworkEvent event) { + super.handleNetworkPlayerQuit(event); + } + + @Override + @EventHandler + public void onPubSubMessageEvent(PubSubMessageEvent event) { + super.handlePubSubMessageEvent(event); + } + + @Override + @EventHandler + public void onServerConnectedEvent(ServerConnectedEvent event) { + final String currentServer = event.getServer().getInfo().getName(); + final String oldServer = event.getPlayer().getServer() == null ? null : event.getPlayer().getServer().getInfo().getName(); + super.playerChangedServer(event.getPlayer().getUniqueId(), oldServer, currentServer); + } + + @EventHandler + public void onLoginEvent(LoginEvent event) { + event.registerIntent((Plugin) plugin); + // check if online + if (getLastOnline(event.getConnection().getUniqueId()) == 0) { + if (plugin.configuration().kickWhenOnline()) { + kickPlayer(event.getConnection().getUniqueId(), plugin.langConfiguration().messages().loggedInFromOtherLocation()); + // wait 3 seconds before releasing the event + plugin.executeAsyncAfter(() -> event.completeIntent((Plugin) plugin), TimeUnit.SECONDS, 3); + } else { + event.setCancelled(true); + event.setCancelReason(BungeeComponentSerializer.get().serialize(plugin.langConfiguration().messages().alreadyLoggedIn())); + event.completeIntent((Plugin) plugin); + } + } else { + event.completeIntent((Plugin) plugin); + } + + } + + @Override + @EventHandler + public void onLoginEvent(PostLoginEvent event) { + super.addPlayer(event.getPlayer().getUniqueId(), event.getPlayer().getAddress().getAddress()); + } + + @Override + @EventHandler + public void onDisconnectEvent(PlayerDisconnectEvent event) { + super.removePlayer(event.getPlayer().getUniqueId()); + } + + +} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerUtils.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerUtils.java deleted file mode 100644 index f1a46c6..0000000 --- a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/BungeePlayerUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee; - -import com.imaginarycode.minecraft.redisbungee.api.util.player.PlayerUtils; -import net.md_5.bungee.api.connection.PendingConnection; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import redis.clients.jedis.UnifiedJedis; -public class BungeePlayerUtils { - - public static void createBungeePlayer(ProxiedPlayer player, UnifiedJedis unifiedJedis, boolean fireEvent) { - String serverName = null; - if (player.getServer() != null) { - serverName = player.getServer().getInfo().getName(); - } - PendingConnection pendingConnection = player.getPendingConnection(); - PlayerUtils.createPlayer(player.getUniqueId(), unifiedJedis, serverName, pendingConnection.getAddress().getAddress(), fireEvent); - } - -} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java index f540ab6..d5ae22b 100644 --- a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java +++ b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungee.java @@ -10,131 +10,104 @@ package com.imaginarycode.minecraft.redisbungee; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimap; -import com.imaginarycode.minecraft.redisbungee.api.config.ConfigLoader; +import co.aikar.commands.BungeeCommandManager; +import com.imaginarycode.minecraft.redisbungee.api.PlayerDataManager; +import com.imaginarycode.minecraft.redisbungee.api.ProxyDataManager; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration; +import com.imaginarycode.minecraft.redisbungee.api.config.loaders.ConfigLoader; import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; +import com.imaginarycode.minecraft.redisbungee.api.config.loaders.LangConfigLoader; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerChangedServerNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerJoinedNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerLeftNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPubSubMessageEvent; -import com.imaginarycode.minecraft.redisbungee.api.tasks.*; -import com.imaginarycode.minecraft.redisbungee.commands.RedisBungeeCommands; +import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; +import com.imaginarycode.minecraft.redisbungee.api.util.InitialUtils; +import com.imaginarycode.minecraft.redisbungee.api.util.uuid.NameFetcher; +import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDFetcher; +import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDTranslator; +import com.imaginarycode.minecraft.redisbungee.commands.CommandLoader; +import com.imaginarycode.minecraft.redisbungee.commands.utils.CommandPlatformHelper; import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetworkEvent; import com.imaginarycode.minecraft.redisbungee.events.PlayerJoinedNetworkEvent; import com.imaginarycode.minecraft.redisbungee.events.PlayerLeftNetworkEvent; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; -import com.imaginarycode.minecraft.redisbungee.api.*; -import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; -import com.imaginarycode.minecraft.redisbungee.api.util.uuid.NameFetcher; -import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDFetcher; -import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDTranslator; import com.squareup.okhttp.Dispatcher; import com.squareup.okhttp.OkHttpClient; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; 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 redis.clients.jedis.*; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.JedisPool; -import java.io.*; +import java.io.IOException; import java.lang.reflect.Field; import java.net.InetAddress; -import java.util.*; +import java.sql.Date; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -public class RedisBungee extends Plugin implements RedisBungeePlugin, ConfigLoader { +public class RedisBungee extends Plugin implements RedisBungeePlugin, ConfigLoader, LangConfigLoader { private static RedisBungeeAPI apiStatic; - private AbstractRedisBungeeAPI api; private RedisBungeeMode redisBungeeMode; - private PubSubListener psl = null; + private ProxyDataManager proxyDataManager; + private BungeePlayerDataManager playerDataManager; + private ScheduledTask heartbeatTask; + private ScheduledTask cleanupTask; private Summoner summoner; private UUIDTranslator uuidTranslator; private RedisBungeeConfiguration configuration; - private BungeeDataManager dataManager; + private LangConfiguration langConfiguration; private OkHttpClient httpClient; - private volatile List proxiesIds; - private final AtomicInteger globalPlayerCount = new AtomicInteger(); - private Future integrityCheck; - private Future heartbeatTask; - private static final Object SERVER_TO_PLAYERS_KEY = new Object(); - private final Cache> serverToPlayersCache = CacheBuilder.newBuilder() - .expireAfterWrite(5, TimeUnit.SECONDS) - .build(); + private BungeeCommandManager commandManager; + + private final Logger logger = LoggerFactory.getLogger("RedisBungee"); @Override - public RedisBungeeConfiguration getConfiguration() { + public RedisBungeeConfiguration configuration() { return this.configuration; } @Override - public int getCount() { - return this.globalPlayerCount.get(); + public LangConfiguration langConfiguration() { + return this.langConfiguration; } - @Override - public Set getLocalPlayersAsUuidStrings() { - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (ProxiedPlayer player : getProxy().getPlayers()) { - builder.add(player.getUniqueId().toString()); - } - return builder.build(); - } - - @Override - public AbstractDataManager getDataManager() { - return this.dataManager; - } - - @Override public AbstractRedisBungeeAPI getAbstractRedisBungeeApi() { return this.api; } + @Override + public ProxyDataManager proxyDataManager() { + return this.proxyDataManager; + } + + @Override + public PlayerDataManager playerDataManager() { + return this.playerDataManager; + } + @Override public UUIDTranslator getUuidTranslator() { return this.uuidTranslator; } - @Override - public Multimap serverToPlayersCache() { - try { - return this.serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, this::serversToPlayers); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - - @Override - public List getProxiesIds() { - return proxiesIds; - } - - @Override - public PubSubListener getPubSubListener() { - return this.psl; - } - - @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 fireEvent(Object event) { this.getProxy().getPluginManager().callEvent((Event) event); @@ -147,17 +120,32 @@ public class RedisBungee extends Plugin implements RedisBungeePlugin getLocalOnlineUUIDs() { + HashSet uuids = new HashSet<>(); + ProxyServer.getInstance().getPlayers().forEach((proxiedPlayer) -> uuids.add(proxiedPlayer.getUniqueId())); + return uuids; + } + + @Override + protected void handlePlatformCommandExecution(String command) { + logInfo("Dispatching {}", command); + ProxyServer.getInstance().getPluginManager().dispatchCommand(RedisBungeeCommandSender.getSingleton(), command); + } + }; + this.playerDataManager = new BungeePlayerDataManager(this); + + getProxy().getPluginManager().registerListener(this, this.playerDataManager); + getProxy().getPluginManager().registerListener(this, new RedisBungeeListener(this)); + // start listening + getProxy().getScheduler().runAsync(this, proxyDataManager); + // heartbeat + this.heartbeatTask = getProxy().getScheduler().schedule(this, () -> this.proxyDataManager.publishHeartbeat(), 0, 1, TimeUnit.SECONDS); + // cleanup + this.cleanupTask = getProxy().getScheduler().schedule(this, () -> this.proxyDataManager.correctionTask(), 0, 60, TimeUnit.SECONDS); // init the http lib httpClient = new OkHttpClient(); Dispatcher dispatcher = new Dispatcher(getExecutorService()); @@ -226,71 +247,49 @@ public class RedisBungee extends Plugin implements RedisBungeePlugin implements Listener { - - - public RedisBungeeBungeeListener(RedisBungeePlugin plugin, List exemptAddresses) { - super(plugin, exemptAddresses); - } - - @Override - @EventHandler(priority = HIGHEST) - public void onLogin(LoginEvent event) { - event.registerIntent((Plugin) plugin); - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - if (event.isCancelled()) { - return null; - } - if (plugin.getConfiguration().restoreOldKickBehavior()) { - for (String s : plugin.getProxiesIds()) { - if (unifiedJedis.sismember("proxy:" + s + ":usersOnline", event.getConnection().getUniqueId().toString())) { - event.setCancelled(true); - event.setCancelReason(plugin.getConfiguration().getMessages().get(RedisBungeeConfiguration.MessageType.ALREADY_LOGGED_IN)); - return null; - } - } - } else if (api.isPlayerOnline(event.getConnection().getUniqueId())) { - PlayerUtils.setKickedOtherLocation(event.getConnection().getUniqueId().toString(), unifiedJedis); - api.kickPlayer(event.getConnection().getUniqueId(), plugin.getConfiguration().getMessages().get(RedisBungeeConfiguration.MessageType.LOGGED_IN_OTHER_LOCATION)); - } - return null; - } finally { - event.completeIntent((Plugin) plugin); - } - } - }); - } - - @Override - @EventHandler - public void onPostLogin(PostLoginEvent event) { - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - plugin.getUuidTranslator().persistInfo(event.getPlayer().getName(), event.getPlayer().getUniqueId(), unifiedJedis); - BungeePlayerUtils.createBungeePlayer(event.getPlayer(), unifiedJedis, true); - return null; - } - }); - } - - @Override - @EventHandler - public void onPlayerDisconnect(PlayerDisconnectEvent event) { - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - PlayerUtils.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), unifiedJedis, true); - return null; - } - }); - - } - - @Override - @EventHandler - public void onServerChange(ServerConnectedEvent event) { - final String currentServer = event.getServer().getInfo().getName(); - final String oldServer = event.getPlayer().getServer() == null ? null : event.getPlayer().getServer().getInfo().getName(); - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - unifiedJedis.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", event.getServer().getInfo().getName()); - PayloadUtils.playerServerChangePayload(event.getPlayer().getUniqueId(), unifiedJedis, currentServer, oldServer); - return null; - } - }); - } - - @Override - @EventHandler - public void onPing(ProxyPingEvent event) { - if (exemptAddresses.contains(event.getConnection().getAddress().getAddress())) { - return; - } - ServerInfo forced = AbstractReconnectHandler.getForcedHost(event.getConnection()); - - if (forced != null && event.getConnection().getListener().isPingPassthrough()) { - return; - } - event.getResponse().getPlayers().setOnline(plugin.getCount()); - } - - @Override - @SuppressWarnings("UnstableApiUsage") - @EventHandler - public void onPluginMessage(PluginMessageEvent event) { - if ((event.getTag().equals("legacy:redisbungee") || event.getTag().equals("RedisBungee")) && event.getSender() instanceof Server) { - final String currentChannel = event.getTag(); - final byte[] data = Arrays.copyOf(event.getData(), event.getData().length); - plugin.executeAsync(() -> { - ByteArrayDataInput in = ByteStreams.newDataInput(data); - - String subchannel = in.readUTF(); - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - String type; - - switch (subchannel) { - case "PlayerList": - out.writeUTF("PlayerList"); - Set original = Collections.emptySet(); - type = in.readUTF(); - if (type.equals("ALL")) { - out.writeUTF("ALL"); - original = plugin.getPlayers(); - } else { - out.writeUTF(type); - try { - original = plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type); - } catch (IllegalArgumentException ignored) { - } - } - Set players = new HashSet<>(); - for (UUID uuid : original) - players.add(plugin.getUuidTranslator().getNameFromUuid(uuid, false)); - out.writeUTF(Joiner.on(',').join(players)); - break; - case "PlayerCount": - out.writeUTF("PlayerCount"); - type = in.readUTF(); - if (type.equals("ALL")) { - out.writeUTF("ALL"); - out.writeInt(plugin.getCount()); - } else { - out.writeUTF(type); - try { - out.writeInt(plugin.getAbstractRedisBungeeApi().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.getAbstractRedisBungeeApi().getLastOnline(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(user, true)))); - break; - case "ServerPlayers": - String type1 = in.readUTF(); - out.writeUTF("ServerPlayers"); - Multimap multimap = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - - boolean includesUsers; - - switch (type1) { - case "COUNT": - includesUsers = false; - break; - case "PLAYERS": - includesUsers = true; - break; - default: - // TODO: Should I raise an error? - return; - } - - out.writeUTF(type1); - - if (includesUsers) { - Multimap human = HashMultimap.create(); - for (Map.Entry entry : multimap.entries()) { - human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); - } - serializeMultimap(human, true, out); - } else { - serializeMultiset(multimap.keys(), out); - } - break; - case "Proxy": - out.writeUTF("Proxy"); - out.writeUTF(plugin.getConfiguration().getProxyId()); - break; - case "PlayerProxy": - String username = in.readUTF(); - out.writeUTF("PlayerProxy"); - out.writeUTF(username); - out.writeUTF(plugin.getAbstractRedisBungeeApi().getProxy(Objects.requireNonNull(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.getAbstractRedisBungeeApi().getProxyId())) { - String message = event.getMessage(); - if (message.startsWith("/")) - message = message.substring(1); - plugin.logInfo("Invoking command via PubSub: /" + message); - ((Plugin) plugin).getProxy().getPluginManager().dispatchCommand(RedisBungeeCommandSender.getSingleton(), message); - } - } -} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java new file mode 100644 index 0000000..b5c401f --- /dev/null +++ b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee; + +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +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.PluginMessageEvent; +import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.api.event.ServerConnectEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; + +import java.util.*; + +import static com.imaginarycode.minecraft.redisbungee.api.util.serialize.MultiMapSerialization.*; + +public class RedisBungeeListener implements Listener { + + private final RedisBungeePlugin plugin; + + public RedisBungeeListener(RedisBungeePlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPing(ProxyPingEvent event) { + if (!plugin.configuration().handleMotd()) return; + if (plugin.configuration().getExemptAddresses().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.proxyDataManager().totalNetworkPlayers()); + } + + @SuppressWarnings("UnstableApiUsage") + @EventHandler + public void onPluginMessage(PluginMessageEvent event) { + if ((event.getTag().equals("legacy:redisbungee") || event.getTag().equals("RedisBungee")) && event.getSender() instanceof Server) { + final String currentChannel = event.getTag(); + final byte[] data = Arrays.copyOf(event.getData(), event.getData().length); + plugin.executeAsync(() -> { + ByteArrayDataInput in = ByteStreams.newDataInput(data); + + String subchannel = in.readUTF(); + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + String type; + + switch (subchannel) { + case "PlayerList" -> { + out.writeUTF("PlayerList"); + Set original = Collections.emptySet(); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + original = plugin.proxyDataManager().networkPlayers(); + } else { + out.writeUTF(type); + try { + original = plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type); + } catch (IllegalArgumentException ignored) { + } + } + Set players = new HashSet<>(); + for (UUID uuid : original) + players.add(plugin.getUuidTranslator().getNameFromUuid(uuid, false)); + out.writeUTF(Joiner.on(',').join(players)); + } + case "PlayerCount" -> { + out.writeUTF("PlayerCount"); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + out.writeInt(plugin.proxyDataManager().totalNetworkPlayers()); + } else { + out.writeUTF(type); + try { + out.writeInt(plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type).size()); + } catch (IllegalArgumentException e) { + out.writeInt(0); + } + } + } + case "LastOnline" -> { + String user = in.readUTF(); + out.writeUTF("LastOnline"); + out.writeUTF(user); + out.writeLong(plugin.getAbstractRedisBungeeApi().getLastOnline(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(user, true)))); + } + case "ServerPlayers" -> { + String type1 = in.readUTF(); + out.writeUTF("ServerPlayers"); + Multimap multimap = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); + boolean includesUsers; + switch (type1) { + case "COUNT" -> includesUsers = false; + case "PLAYERS" -> includesUsers = true; + default -> { + // TODO: Should I raise an error? + return; + } + } + out.writeUTF(type1); + if (includesUsers) { + Multimap human = HashMultimap.create(); + for (Map.Entry entry : multimap.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + serializeMultimap(human, true, out); + } else { + serializeMultiset(multimap.keys(), out); + } + } + case "Proxy" -> { + out.writeUTF("Proxy"); + out.writeUTF(plugin.configuration().getProxyId()); + } + case "PlayerProxy" -> { + String username = in.readUTF(); + out.writeUTF("PlayerProxy"); + out.writeUTF(username); + out.writeUTF(plugin.getAbstractRedisBungeeApi().getProxy(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(username, true)))); + } + default -> { + return; + } + } + + ((Server) event.getSender()).sendData(currentChannel, out.toByteArray()); + }); + } + } + + @EventHandler + public void onServerConnectEvent(ServerConnectEvent event) { + if (event.getReason() == ServerConnectEvent.Reason.JOIN_PROXY && plugin.configuration().handleReconnectToLastServer()) { + ProxiedPlayer player = event.getPlayer(); + String lastServer = plugin.playerDataManager().getLastServerFor(event.getPlayer().getUniqueId()); + if (lastServer == null) return; + player.sendMessage(BungeeComponentSerializer.get().serialize(plugin.langConfiguration().messages().serverConnecting(player.getLocale(), lastServer))); + ServerInfo serverInfo = ProxyServer.getInstance().getServerInfo(lastServer); + if (serverInfo == null) { + player.sendMessage(BungeeComponentSerializer.get().serialize(plugin.langConfiguration().messages().serverNotFound(player.getLocale(), lastServer))); + return; + } + event.setTarget(serverInfo); + } + } +} diff --git a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java b/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java deleted file mode 100644 index e5a9ea3..0000000 --- a/RedisBungee-Bungee/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -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.RedisBungee; -import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; -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.plugin.Command; - -import java.net.InetAddress; -import java.text.SimpleDateFormat; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; - -/** - * This class contains subclasses that are used for the commands RedisBungee overrides or includes: /glist, /find and /lastseen. - *

- * All classes use the {@link AbstractRedisBungeeAPI}. - * - * @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 RedisBungee plugin; - - public GlistCommand(RedisBungee 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.getAbstractRedisBungeeApi().getPlayerCount(); - BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) - .append(playerPlural(count) + " currently online.").create(); - if (args.length > 0 && args[0].equals("showall")) { - Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - Multimap human = HashMultimap.create(); - for (Map.Entry entry : serverToPlayers.entries()) { - // if for any reason UUID translation fails just return the uuid as name, to make command finish executing. - String playerName = plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false); - human.put(entry.getKey(), playerName != null ? playerName : entry.getValue().toString()); - } - 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 RedisBungee plugin; - - public FindCommand(RedisBungee 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.getAbstractRedisBungeeApi().getServerNameFor(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 RedisBungee plugin; - - public LastSeenCommand(RedisBungee 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.getAbstractRedisBungeeApi().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 RedisBungee plugin; - - public IpCommand(RedisBungee 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.getAbstractRedisBungeeApi().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 RedisBungee plugin; - - public PlayerProxyCommand(RedisBungee 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.getAbstractRedisBungeeApi().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 RedisBungee plugin; - - public SendToAll(RedisBungee 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.getAbstractRedisBungeeApi().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 RedisBungee plugin; - - public ServerId(RedisBungee 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.getAbstractRedisBungeeApi().getProxyId() + "."); - textComponent.setColor(ChatColor.YELLOW); - sender.sendMessage(textComponent); - } - } - - public static class ServerIds extends Command { - private final RedisBungee plugin; - public ServerIds(RedisBungee 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.getAbstractRedisBungeeApi().getAllProxies())); - textComponent.setColor(ChatColor.YELLOW); - sender.sendMessage(textComponent); - } - } - - public static class PlistCommand extends Command { - private final RedisBungee plugin; - - public PlistCommand(RedisBungee 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().getProxyId(); - if (!plugin.getProxiesIds().contains(proxy)) { - sender.sendMessage(new ComponentBuilder(proxy + " is not a valid proxy. See /serverids for valid proxies.").color(ChatColor.RED).create()); - return; - } - Set players = plugin.getAbstractRedisBungeeApi().getPlayersOnProxy(proxy); - BaseComponent[] playersOnline = new ComponentBuilder("").color(ChatColor.YELLOW) - .append(playerPlural(players.size()) + " currently on proxy " + proxy + ".").create(); - if (args.length >= 2 && args[1].equals("showall")) { - Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - Multimap human = HashMultimap.create(); - for (Map.Entry entry : serverToPlayers.entries()) { - if (players.contains(entry.getValue())) { - human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); - } - } - for (String server : new TreeSet<>(human.keySet())) { - TextComponent serverName = new TextComponent(); - serverName.setColor(ChatColor.RED); - serverName.setText("[" + server + "] "); - TextComponent serverCount = new TextComponent(); - serverCount.setColor(ChatColor.YELLOW); - serverCount.setText("(" + human.get(server).size() + "): "); - TextComponent serverPlayers = new TextComponent(); - serverPlayers.setColor(ChatColor.WHITE); - serverPlayers.setText(Joiner.on(", ").join(human.get(server))); - sender.sendMessage(serverName, serverCount, serverPlayers); - } - sender.sendMessage(playersOnline); - } else { - sender.sendMessage(playersOnline); - sender.sendMessage(new ComponentBuilder("To see all players online, use /plist " + proxy + " showall.").color(ChatColor.YELLOW).create()); - } - } - }); - } - } -} \ No newline at end of file diff --git a/RedisBungee-Commands/build.gradle.kts b/RedisBungee-Commands/build.gradle.kts new file mode 100644 index 0000000..0c5944e --- /dev/null +++ b/RedisBungee-Commands/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + `java-library` +} + +dependencies { + implementation(project(":RedisBungee-API")) + implementation(libs.acf.core) +} + +description = "RedisBungee common commands" + + +tasks { + compileJava { + options.encoding = Charsets.UTF_8.name() + options.release.set(17) + } + javadoc { + options.encoding = Charsets.UTF_8.name() + } + processResources { + filteringCharset = Charsets.UTF_8.name() + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandLoader.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandLoader.java new file mode 100644 index 0000000..5346be9 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandLoader.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands; + +import co.aikar.commands.CommandManager; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; + +import com.imaginarycode.minecraft.redisbungee.commands.legacy.LegacyRedisBungeeCommands; + +public class CommandLoader { + + public static void initCommands(CommandManager commandManager, RedisBungeePlugin plugin) { + var commandsConfiguration = plugin.configuration().commandsConfiguration(); + if (commandsConfiguration.redisbungeeEnabled()) { + commandManager.registerCommand(new CommandRedisBungee(plugin)); + } + if (commandsConfiguration.redisbungeeLegacyEnabled()) { + commandManager.registerCommand(new LegacyRedisBungeeCommands(commandManager,plugin)); + } + + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandRedisBungee.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandRedisBungee.java new file mode 100644 index 0000000..c67b7a0 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/CommandRedisBungee.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.RegisteredCommand; +import co.aikar.commands.annotation.*; +import com.google.common.primitives.Ints; +import com.imaginarycode.minecraft.redisbungee.Constants; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; +import com.imaginarycode.minecraft.redisbungee.commands.utils.StopperUUIDCleanupTask; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@CommandAlias("rb|redisbungee") +@CommandPermission("redisbungee.command.use") +@Description("Main command") +public class CommandRedisBungee extends AdventureBaseCommand { + + private final RedisBungeePlugin plugin; + + public CommandRedisBungee(RedisBungeePlugin plugin) { + this.plugin = plugin; + } + + @Default + @Subcommand("info|version|git") + @Description("information about current redisbungee build") + public void info(CommandIssuer issuer) { + final String message = """ + This proxy is running RedisBungee Limework's fork + ======================================== + RedisBungee version: + Commit: + ======================================== + run /rb help for more commands"""; + sendMessage( + issuer, + MiniMessage.miniMessage() + .deserialize( + message, + Placeholder.component("version", Component.text(Constants.VERSION)), + Placeholder.component( + "commit", + Component.text(Constants.GIT_COMMIT.substring(0, 8)) + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, Constants.getGithubCommitLink())) + .hoverEvent(HoverEvent.showText(Component.text("Click me to open: " + Constants.getGithubCommitLink()))) + ))); + } + // ......: ...... + @HelpCommand + @Description("shows the help page") + public void help(CommandIssuer issuer) { + final String barFormat = "========================================"; + final String commandFormat = "/rb : "; + + TextComponent.Builder message = Component.text(); + message.append(MiniMessage.miniMessage().deserialize(barFormat)); + + getSubCommands().forEach((subCommand, registeredCommand) -> { + String[] split = registeredCommand.getCommand().split(" "); + if (split.length > 1 && subCommand.equalsIgnoreCase(split[1])) { + message.appendNewline().append(MiniMessage.miniMessage().deserialize(commandFormat, Placeholder.component("sub-command", Component.text(subCommand)), + Placeholder.component("description", MiniMessage.miniMessage().deserialize(registeredCommand.getHelpText())) + )); + } + }); + + message.appendNewline().append(MiniMessage.miniMessage().deserialize(barFormat)); + + sendMessage(issuer, message.build()); + } + @Subcommand("clean") + @Description("cleans up the uuid cache WARNING... command above could cause performance issues") + @Private + public void cleanUp(CommandIssuer issuer) { + if (StopperUUIDCleanupTask.isRunning) { + sendMessage(issuer, + Component.text("cleanup is currently running!").color(NamedTextColor.RED)); + return; + } + sendMessage(issuer, + Component.text("cleanup is Starting, you should see the output status in the proxy console").color(NamedTextColor.GOLD)); + plugin.executeAsync(new StopperUUIDCleanupTask(plugin)); + } + + + + private List> subListProxies(List> data, final int currentPage, final int pageSize) { + return data.subList(((currentPage * pageSize) - pageSize), Ints.constrainToRange(currentPage * pageSize, 0, data.size())); + + } + @Subcommand("show") + @Description("Shows proxies in this network") + public void showProxies(CommandIssuer issuer, String[] args) { + final String closer = "========================================"; + final String pageTop = "Page: / Network ID: Proxies online: "; + final String proxy = " : online"; + final String proxyHere = " (#) "; + final String nextPage = ">>>>>"; + final String previousPage = "<<<<< "; + final String pageInvalid = "invalid page"; + final String noProxies = "No proxies were found :("; + + final int pageSize = 16; + + int currentPage; + if (args.length > 0) { + try { + currentPage = Integer.parseInt(args[0]); + if (currentPage < 1) currentPage = 1; + } catch (NumberFormatException e) { + sendMessage(issuer, MiniMessage.miniMessage().deserialize(pageInvalid)); + return; + } + } else currentPage = 1; + + var data = new ArrayList<>(plugin.proxyDataManager().eachProxyCount().entrySet()); + // there is no way this runs because there is always an heartbeat. + // if not could be some shenanigans done by devs :P + if (data.isEmpty()) { + sendMessage(issuer, MiniMessage.miniMessage().deserialize(noProxies)); + return; + } + // compute the total pages + int maxPages = (int) Math.ceil(data.size() / (double) pageSize); + if (currentPage > maxPages) currentPage = maxPages; + var subList = subListProxies(data, currentPage, pageSize); + TextComponent.Builder builder = Component.text(); + builder.append(MiniMessage.miniMessage().deserialize(closer)).appendNewline(); + builder.append(MiniMessage.miniMessage().deserialize(pageTop, + Placeholder.component("current", Component.text(currentPage)), + Placeholder.component("max", Component.text(maxPages)), + Placeholder.component("network", Component.text(plugin.proxyDataManager().networkId())), + Placeholder.component("proxies", Component.text(data.size())) + + + )).appendNewline(); + int left = pageSize; + for (Map.Entry entrySet : subList) { + builder.append(MiniMessage.miniMessage().deserialize(proxy, + + Placeholder.component("proxy", Component.text(entrySet.getKey())), + Placeholder.component("here", Component.text(plugin.proxyDataManager().proxyId().equals(entrySet.getKey()) ? proxyHere : "")), + Placeholder.component("players", Component.text(entrySet.getValue())) + + )).appendNewline(); + left--; + } + while(left > 0) { + builder.appendNewline(); + left--; + } + if (currentPage > 1) { + builder.append(MiniMessage.miniMessage().deserialize(previousPage) + .color(NamedTextColor.WHITE).clickEvent(ClickEvent.runCommand("/rb show " + (currentPage - 1)))); + } else { + builder.append(MiniMessage.miniMessage().deserialize(previousPage).color(NamedTextColor.GRAY)); + } + if (subList.size() == pageSize && !subListProxies(data, currentPage + 1, pageSize).isEmpty()) { + builder.append(MiniMessage.miniMessage().deserialize(nextPage) + .color(NamedTextColor.WHITE).clickEvent(ClickEvent.runCommand("/rb show " + (currentPage + 1)))); + } else { + builder.append(MiniMessage.miniMessage().deserialize(nextPage).color(NamedTextColor.GRAY)); + } + builder.appendNewline(); + builder.append(MiniMessage.miniMessage().deserialize(closer)); + sendMessage(issuer, builder.build()); + + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandFind.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandFind.java new file mode 100644 index 0000000..0a73d36 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandFind.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("find|rfind") +@CommandPermission("redisbungee.command.find") +public class CommandFind extends AdventureBaseCommand { + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandFind(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void find(CommandIssuer issuer, String[] args) { + rootCommand.find(issuer, args); + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandGList.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandGList.java new file mode 100644 index 0000000..54cc985 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandGList.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("glist|rglist") +@CommandPermission("redisbungee.command.glist") +public class CommandGList extends AdventureBaseCommand { + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandGList(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void gList(CommandIssuer issuer, String[] args) { + rootCommand.gList(issuer, args); + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandIp.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandIp.java new file mode 100644 index 0000000..410ae91 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandIp.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("ip|playerip|rip|rplayerip") +@CommandPermission("redisbungee.command.ip") +public class CommandIp extends AdventureBaseCommand { + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandIp(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + + @Default + public void ip(CommandIssuer issuer, String[] args) { + this.rootCommand.ip(issuer, args); + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandLastSeen.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandLastSeen.java new file mode 100644 index 0000000..f44ea8e --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandLastSeen.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("lastseen|rlastseend") +@CommandPermission("redisbungee.command.lastseen") +public class CommandLastSeen extends AdventureBaseCommand { + + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandLastSeen(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void lastSeen(CommandIssuer issuer, String[] args) { + this.rootCommand.lastSeen(issuer,args); + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPProxy.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPProxy.java new file mode 100644 index 0000000..70a949c --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPProxy.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("pproxy") +@CommandPermission("redisbungee.command.pproxy") +public class CommandPProxy extends AdventureBaseCommand { + private final LegacyRedisBungeeCommands rootCommand; + + public CommandPProxy(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void playerProxy(CommandIssuer issuer, String[] args) { + this.rootCommand.playerProxy(issuer,args); + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPlist.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPlist.java new file mode 100644 index 0000000..04e1609 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandPlist.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("plist|rplist") +@CommandPermission("redisbungee.command.plist") +public class CommandPlist extends AdventureBaseCommand { + + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandPlist(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void playerList(CommandIssuer issuer, String[] args) { + this.rootCommand.playerList(issuer, args); + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandSendToAll.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandSendToAll.java new file mode 100644 index 0000000..ad9e1da --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandSendToAll.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("sendtoall|rsendtoall") +@CommandPermission("redisbungee.command.sendtoall") +public class CommandSendToAll extends AdventureBaseCommand { + + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandSendToAll(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + @Default + public void sendToAll(CommandIssuer issuer, String[] args) { + this.rootCommand.sendToAll(issuer, args); + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerId.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerId.java new file mode 100644 index 0000000..c62c2f5 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerId.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("serverid|rserverid") +@CommandPermission("redisbungee.command.serverid") +public class CommandServerId extends AdventureBaseCommand { + + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandServerId(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + @Default + public void serverId(CommandIssuer issuer) { + this.rootCommand.serverId(issuer); + } +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerIds.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerIds.java new file mode 100644 index 0000000..85b53b7 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/CommandServerIds.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; + +@CommandAlias("serverids|rserverids") +@CommandPermission("redisbungee.command.serverids") +public class CommandServerIds extends AdventureBaseCommand { + + + private final LegacyRedisBungeeCommands rootCommand; + + public CommandServerIds(LegacyRedisBungeeCommands rootCommand) { + this.rootCommand = rootCommand; + } + + @Default + public void serverIds(CommandIssuer issuer) { + this.rootCommand.serverIds(issuer); + } + + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/LegacyRedisBungeeCommands.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/LegacyRedisBungeeCommands.java new file mode 100644 index 0000000..6253e96 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/legacy/LegacyRedisBungeeCommands.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.legacy; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.CommandManager; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Subcommand; +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.commands.utils.AdventureBaseCommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.net.InetAddress; +import java.text.SimpleDateFormat; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +@CommandAlias("rbl|redisbungeelegacy") +@CommandPermission("redisbungee.legacy.use") +public class LegacyRedisBungeeCommands extends AdventureBaseCommand { + + private final RedisBungeePlugin plugin; + + public LegacyRedisBungeeCommands(CommandManager commandManager, RedisBungeePlugin plugin) { + this.plugin = plugin; + var commands = plugin.configuration().commandsConfiguration().legacySubCommandsConfiguration(); + if (!plugin.configuration().commandsConfiguration().redisbungeeLegacyEnabled()) throw new IllegalStateException("someone tried to init me while disabled!"); + if (commands == null) throw new NullPointerException("commands config is null!!"); + + if (commands.installGlist()) commandManager.registerCommand(new CommandGList(this)); + if (commands.installFind()) commandManager.registerCommand(new CommandFind(this)); + if (commands.installIp()) commandManager.registerCommand(new CommandIp(this)); + if (commands.installLastseen()) commandManager.registerCommand(new CommandLastSeen(this)); + if (commands.installPlist()) commandManager.registerCommand(new CommandPlist(this)); + if (commands.installPproxy()) commandManager.registerCommand(new CommandPProxy(this)); + if (commands.installSendtoall()) commandManager.registerCommand(new CommandSendToAll(this)); + if (commands.installServerid()) commandManager.registerCommand(new CommandServerId(this)); + if (commands.installServerids()) commandManager.registerCommand(new CommandServerIds(this)); + } + + private static final Component NO_PLAYER_SPECIFIED = + Component.text("You must specify a player name.", NamedTextColor.RED); + private static final Component PLAYER_NOT_FOUND = + Component.text("No such player found.", NamedTextColor.RED); + private static final Component NO_COMMAND_SPECIFIED = + Component.text("You must specify a command to be run.", NamedTextColor.RED); + + private static String playerPlural(int num) { + return num == 1 ? num + " player is" : num + " players are"; + } + + @Subcommand("glist") + @CommandPermission("redisbungee.command.glist") + public void gList(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + int count = plugin.getAbstractRedisBungeeApi().getPlayerCount(); + Component playersOnline = Component.text(playerPlural(count) + " currently online.", NamedTextColor.YELLOW); + if (args.length > 0 && args[0].equals("showall")) { + Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + serverToPlayers.forEach((key, value) -> { + // if for any reason UUID translation fails just return the uuid as name, to make command finish executing. + String playerName = plugin.getUuidTranslator().getNameFromUuid(value, false); + human.put(key, playerName != null ? playerName : value.toString()); + }); + for (String server : new TreeSet<>(serverToPlayers.keySet())) { + Component serverName = Component.text("[" + server + "] ", NamedTextColor.GREEN); + Component serverCount = Component.text("(" + serverToPlayers.get(server).size() + "): ", NamedTextColor.YELLOW); + Component serverPlayers = Component.text(Joiner.on(", ").join(human.get(server)), NamedTextColor.WHITE); + sendMessage(issuer, Component.textOfChildren(serverName, serverCount, serverPlayers)); + } + sendMessage(issuer, playersOnline); + } else { + sendMessage(issuer, playersOnline); + sendMessage(issuer, Component.text("To see all players online, use /glist showall.", NamedTextColor.YELLOW)); + } + + }); + } + + @Subcommand("find") + @CommandPermission("redisbungee.command.find") + public void find(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sendMessage(issuer, PLAYER_NOT_FOUND); + return; + } + String proxyId = plugin.playerDataManager().getProxyFor(uuid); + if (proxyId != null) { + String serverId = plugin.playerDataManager().getServerFor(uuid); + Component message = Component.text(args[0] + " is on proxy " + proxyId + " on server " + serverId +".", NamedTextColor.BLUE); + sendMessage(issuer, message); + } else { + sendMessage(issuer, PLAYER_NOT_FOUND); + } + } else { + sendMessage(issuer, NO_PLAYER_SPECIFIED); + } + }); + + } + + @Subcommand("lastseen") + @CommandPermission("redisbungee.command.lastseen") + public void lastSeen(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sendMessage(issuer, PLAYER_NOT_FOUND); + return; + } + long secs = plugin.getAbstractRedisBungeeApi().getLastOnline(uuid); + TextComponent.Builder message = Component.text(); + if (secs == 0) { + message.color(NamedTextColor.GREEN); + message.content(args[0] + " is currently online."); + } else if (secs != -1) { + message.color(NamedTextColor.BLUE); + message.content(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); + } else { + message.color(NamedTextColor.RED); + message.content(args[0] + " has never been online."); + } + sendMessage(issuer, message.build()); + } else { + sendMessage(issuer, NO_PLAYER_SPECIFIED); + } + + + }); + } + + @Subcommand("ip") + @CommandPermission("redisbungee.command.ip") + public void ip(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sendMessage(issuer, PLAYER_NOT_FOUND); + return; + } + InetAddress ia = plugin.getAbstractRedisBungeeApi().getPlayerIp(uuid); + if (ia != null) { + TextComponent message = Component.text(args[0] + " is connected from " + ia.toString() + ".", NamedTextColor.GREEN); + sendMessage(issuer, message); + } else { + sendMessage(issuer, PLAYER_NOT_FOUND); + } + } else { + sendMessage(issuer, NO_PLAYER_SPECIFIED); + } + }); + } + + @Subcommand("pproxy") + @CommandPermission("redisbungee.command.pproxy") + public void playerProxy(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + if (args.length > 0) { + UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); + if (uuid == null) { + sendMessage(issuer, PLAYER_NOT_FOUND); + return; + } + String proxy = plugin.getAbstractRedisBungeeApi().getProxy(uuid); + if (proxy != null) { + TextComponent message = Component.text(args[0] + " is connected to " + proxy + ".", NamedTextColor.GREEN); + sendMessage(issuer, message); + } else { + sendMessage(issuer, PLAYER_NOT_FOUND); + } + } else { + sendMessage(issuer, NO_PLAYER_SPECIFIED); + } + }); + + } + + @Subcommand("sendtoall") + @CommandPermission("redisbungee.command.sendtoall") + public void sendToAll(CommandIssuer issuer, String[] args) { + if (args.length > 0) { + String command = Joiner.on(" ").skipNulls().join(args); + plugin.getAbstractRedisBungeeApi().sendProxyCommand(command); + TextComponent message = Component.text("Sent the command /" + command + " to all proxies.", NamedTextColor.GREEN); + sendMessage(issuer, message); + } else { + sendMessage(issuer, NO_COMMAND_SPECIFIED); + } + + } + + @Subcommand("serverid") + @CommandPermission("redisbungee.command.serverid") + public void serverId(CommandIssuer issuer) { + sendMessage(issuer, Component.text("You are on " + plugin.getAbstractRedisBungeeApi().getProxyId() + ".", NamedTextColor.YELLOW)); + } + + @Subcommand("serverids") + @CommandPermission("redisbungee.command.serverids") + public void serverIds(CommandIssuer issuer) { + sendMessage(issuer, Component.text("All Proxies IDs: " + Joiner.on(", ").join(plugin.getAbstractRedisBungeeApi().getAllProxies()), NamedTextColor.YELLOW)); + } + + + @Subcommand("plist") + @CommandPermission("redisbungee.command.plist") + public void playerList(CommandIssuer issuer, String[] args) { + plugin.executeAsync(() -> { + String proxy = args.length >= 1 ? args[0] : plugin.configuration().getProxyId(); + if (!plugin.proxyDataManager().proxiesIds().contains(proxy)) { + sendMessage(issuer, Component.text(proxy + " is not a valid proxy. See /serverids for valid proxies.", NamedTextColor.RED)); + return; + } + Set players = plugin.getAbstractRedisBungeeApi().getPlayersOnProxy(proxy); + Component playersOnline = Component.text(playerPlural(players.size()) + " currently on proxy " + proxy + ".", NamedTextColor.YELLOW); + if (args.length >= 2 && args[1].equals("showall")) { + Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); + Multimap human = HashMultimap.create(); + serverToPlayers.forEach((key, value) -> { + if (players.contains(value)) { + human.put(key, plugin.getUuidTranslator().getNameFromUuid(value, false)); + } + }); + for (String server : new TreeSet<>(human.keySet())) { + TextComponent serverName = Component.text("[" + server + "] ", NamedTextColor.RED); + TextComponent serverCount = Component.text("(" + human.get(server).size() + "): ", NamedTextColor.YELLOW); + TextComponent serverPlayers = Component.text(Joiner.on(", ").join(human.get(server)), NamedTextColor.WHITE); + sendMessage(issuer, Component.textOfChildren(serverName, serverCount, serverPlayers)); + } + sendMessage(issuer, playersOnline); + } else { + sendMessage(issuer, playersOnline); + sendMessage(issuer, Component.text("To see all players online, use /plist " + proxy + " showall.", NamedTextColor.YELLOW)); + } + }); + + } + +} \ No newline at end of file diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/AdventureBaseCommand.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/AdventureBaseCommand.java new file mode 100644 index 0000000..873e8ee --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/AdventureBaseCommand.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.utils; + +import co.aikar.commands.BaseCommand; +import co.aikar.commands.CommandIssuer; +import net.kyori.adventure.text.Component; + +/** + * this just dumb class that wraps the adventure stuff into base command + */ +public abstract class AdventureBaseCommand extends BaseCommand { + + public static void sendMessage(CommandIssuer issuer, Component component) { + CommandPlatformHelper.getPlatformHelper().sendMessage(issuer, component); + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/CommandPlatformHelper.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/CommandPlatformHelper.java new file mode 100644 index 0000000..a951e9d --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/CommandPlatformHelper.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee.commands.utils; + +import co.aikar.commands.CommandIssuer; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import net.kyori.adventure.text.Component; + + +public abstract class CommandPlatformHelper { + + private static CommandPlatformHelper SINGLETON; + + public abstract void sendMessage(CommandIssuer issuer, Component component); + + public static void init(CommandPlatformHelper platformHelper) { + if (SINGLETON != null) { + throw new IllegalStateException("tried to re init Platform Helper"); + } + SINGLETON = platformHelper; + } + + + public static CommandPlatformHelper getPlatformHelper() { + return SINGLETON; + } + +} diff --git a/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/StopperUUIDCleanupTask.java b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/StopperUUIDCleanupTask.java new file mode 100644 index 0000000..06dbe85 --- /dev/null +++ b/RedisBungee-Commands/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/utils/StopperUUIDCleanupTask.java @@ -0,0 +1,25 @@ +package com.imaginarycode.minecraft.redisbungee.commands.utils; + +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.tasks.UUIDCleanupTask; +import redis.clients.jedis.UnifiedJedis; + +public class StopperUUIDCleanupTask extends UUIDCleanupTask { + + public static boolean isRunning = false; + + public StopperUUIDCleanupTask(RedisBungeePlugin plugin) { + super(plugin); + } + + + @Override + public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { + isRunning = true; + try { + super.unifiedJedisTask(unifiedJedis); + } catch (Exception ignored) {} + isRunning = false; + return null; + } +} diff --git a/RedisBungee-Velocity/build.gradle.kts b/RedisBungee-Velocity/build.gradle.kts index 4c2bbb4..fb9bc22 100644 --- a/RedisBungee-Velocity/build.gradle.kts +++ b/RedisBungee-Velocity/build.gradle.kts @@ -6,10 +6,6 @@ plugins { } -repositories { - mavenCentral() - maven { url = uri("https://repo.papermc.io/repository/maven-public/") } -} dependencies { api(project(":RedisBungee-API")) { @@ -17,9 +13,18 @@ dependencies { exclude("com.google.guava", "guava") exclude("com.google.code.gson", "gson") exclude("org.spongepowered", "configurate-yaml") + // exclude also adventure api + exclude("net.kyori", "adventure-api") + exclude("net.kyori", "adventure-text-serializer-gson") + exclude("net.kyori", "adventure-text-serializer-legacy") + exclude("net.kyori", "adventure-text-serializer-plain") + exclude("net.kyori", "adventure-text-minimessage") } - compileOnly("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") - annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") + compileOnly(libs.platform.velocity) + annotationProcessor(libs.platform.velocity) + implementation(project(":RedisBungee-Commands")) + implementation(libs.acf.velocity) + } description = "RedisBungee Velocity implementation" @@ -39,10 +44,12 @@ tasks { "https://jd.papermc.io/velocity/3.0.0/", // velocity api ) val apiDocs = File(rootProject.projectDir, "RedisBungee-API/build/docs/javadoc") - options.linksOffline("https://ci.limework.net/RedisBungee/RedisBungee-API/build/docs/javadoc", apiDocs.path) + options.linksOffline("https://ci.limework.net/RedisBungee/RedisBungee-API/build/docs/javadoc", apiDocs.path) } runVelocity { velocityVersion("3.3.0-SNAPSHOT") + environment["REDISBUNGEE_PROXY_ID"] = "velocity-1" + environment["REDISBUNGEE_NETWORK_ID"] = "dev" } compileJava { options.encoding = Charsets.UTF_8.name() @@ -61,6 +68,9 @@ tasks { relocate("com.squareup.okhttp", "com.imaginarycode.minecraft.redisbungee.internal.okhttp") relocate("okio", "com.imaginarycode.minecraft.redisbungee.internal.okio") relocate("org.json", "com.imaginarycode.minecraft.redisbungee.internal.json") + relocate("com.github.benmanes.caffeine", "com.imaginarycode.minecraft.redisbungee.internal.caffeine") + // acf shade + relocate("co.aikar.commands", "com.imaginarycode.minecraft.redisbungee.internal.acf.commands") } } diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java index 712ebba..2345f25 100644 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeAPI.java @@ -23,7 +23,7 @@ import java.util.UUID; * or somehow you got the Plugin instance by you can call the api using {@link RedisBungeePlugin#getAbstractRedisBungeeApi()}. * * @author tuxed - * @since 0.2.3 | updated 0.8.0 + * @since 0.2.3 */ public class RedisBungeeAPI extends AbstractRedisBungeeAPI { diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java new file mode 100644 index 0000000..05e9264 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeListener.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee; + +import com.google.common.base.Joiner; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +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.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerPing; +import net.kyori.adventure.text.Component; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.imaginarycode.minecraft.redisbungee.api.util.serialize.MultiMapSerialization.serializeMultimap; +import static com.imaginarycode.minecraft.redisbungee.api.util.serialize.MultiMapSerialization.serializeMultiset; + +public class RedisBungeeListener { + + private final RedisBungeePlugin plugin; + + public RedisBungeeListener(RedisBungeePlugin plugin) { + this.plugin = plugin; + } + + @Subscribe(order = PostOrder.LAST) // some plugins changes it online players so we need to be executed as last + public void onPing(ProxyPingEvent event) { + if (!plugin.configuration().handleMotd()) return; + if (plugin.configuration().getExemptAddresses().contains(event.getConnection().getRemoteAddress().getAddress())) return; + + ServerPing.Builder ping = event.getPing().asBuilder(); + ping.onlinePlayers(plugin.proxyDataManager().totalNetworkPlayers()); + event.setPing(ping.build()); + } + + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + if (!(event.getSource() instanceof ServerConnection) || !RedisBungeeVelocityPlugin.IDENTIFIERS.contains(event.getIdentifier())) { + return; + } + + event.setResult(PluginMessageEvent.ForwardResult.handled()); + + plugin.executeAsync(() -> { + ByteArrayDataInput in = event.dataAsDataStream(); + + String subchannel = in.readUTF(); + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + String type; + + switch (subchannel) { + case "PlayerList" -> { + out.writeUTF("PlayerList"); + Set original = Collections.emptySet(); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + original = plugin.proxyDataManager().networkPlayers(); + } else { + out.writeUTF(type); + try { + original = plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type); + } catch (IllegalArgumentException ignored) { + } + } + Set players = original.stream() + .map(uuid -> plugin.getUuidTranslator().getNameFromUuid(uuid, false)) + .collect(Collectors.toSet()); + out.writeUTF(Joiner.on(',').join(players)); + } + case "PlayerCount" -> { + out.writeUTF("PlayerCount"); + type = in.readUTF(); + if (type.equals("ALL")) { + out.writeUTF("ALL"); + out.writeInt(plugin.proxyDataManager().totalNetworkPlayers()); + } else { + out.writeUTF(type); + try { + out.writeInt(plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type).size()); + } catch (IllegalArgumentException e) { + out.writeInt(0); + } + } + } + case "LastOnline" -> { + String user = in.readUTF(); + out.writeUTF("LastOnline"); + out.writeUTF(user); + out.writeLong(plugin.getAbstractRedisBungeeApi().getLastOnline(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(user, true)))); + } + case "ServerPlayers" -> { + String type1 = in.readUTF(); + out.writeUTF("ServerPlayers"); + Multimap multimap = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); + boolean includesUsers; + switch (type1) { + case "COUNT" -> includesUsers = false; + case "PLAYERS" -> includesUsers = true; + default -> { + // TODO: Should I raise an error? + return; + } + } + out.writeUTF(type1); + if (includesUsers) { + Multimap human = HashMultimap.create(); + for (Map.Entry entry : multimap.entries()) { + human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); + } + serializeMultimap(human, true, out); + } else { + serializeMultiset(multimap.keys(), out); + } + } + case "Proxy" -> { + out.writeUTF("Proxy"); + out.writeUTF(plugin.configuration().getProxyId()); + } + case "PlayerProxy" -> { + String username = in.readUTF(); + out.writeUTF("PlayerProxy"); + out.writeUTF(username); + out.writeUTF(plugin.getAbstractRedisBungeeApi().getProxy(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(username, true)))); + } + default -> { + return; + } + } + + ((ServerConnection) event.getSource()).sendPluginMessage(event.getIdentifier(), out.toByteArray()); + }); + + } + + @Subscribe + public void onPlayerChooseInitialServerEvent(PlayerChooseInitialServerEvent event) { + if (plugin.configuration().handleReconnectToLastServer()) { + Player player = event.getPlayer(); + String lastServer = plugin.playerDataManager().getLastServerFor(player.getUniqueId()); + if (lastServer == null) return; + player.sendMessage(plugin.langConfiguration().messages().serverConnecting(player.getPlayerSettings().getLocale(), lastServer)); + Optional optionalRegisteredServer = ((RedisBungeeVelocityPlugin) plugin).getProxy().getServer(lastServer); + if (optionalRegisteredServer.isEmpty()) { + player.sendMessage(plugin.langConfiguration().messages().serverNotFound(player.getPlayerSettings().getLocale(), lastServer)); + return; + } + RegisteredServer server = optionalRegisteredServer.get(); + event.setInitialServer(server); + } + } + + +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityListener.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityListener.java deleted file mode 100644 index f73bf88..0000000 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityListener.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee; - -import com.google.common.base.Joiner; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import com.imaginarycode.minecraft.redisbungee.api.AbstractRedisBungeeListener; -import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; -import com.imaginarycode.minecraft.redisbungee.api.util.player.PlayerUtils; -import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; -import com.imaginarycode.minecraft.redisbungee.api.tasks.RedisTask; -import com.imaginarycode.minecraft.redisbungee.api.util.payload.PayloadUtils; -import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; -import com.velocitypowered.api.event.Continuation; -import com.velocitypowered.api.event.PostOrder; -import com.velocitypowered.api.event.ResultedEvent; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.event.connection.LoginEvent; -import com.velocitypowered.api.event.connection.PluginMessageEvent; -import com.velocitypowered.api.event.connection.PostLoginEvent; -import com.velocitypowered.api.event.connection.PluginMessageEvent.ForwardResult; -import com.velocitypowered.api.event.player.ServerConnectedEvent; -import com.velocitypowered.api.event.proxy.ProxyPingEvent; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import com.velocitypowered.api.proxy.server.ServerPing; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import redis.clients.jedis.UnifiedJedis; - -import java.net.InetAddress; -import java.util.*; -import java.util.stream.Collectors; - -import static com.imaginarycode.minecraft.redisbungee.api.util.serialize.Serializations.serializeMultimap; -import static com.imaginarycode.minecraft.redisbungee.api.util.serialize.Serializations.serializeMultiset; - -public class RedisBungeeVelocityListener extends AbstractRedisBungeeListener { - // Some messages are using legacy characters - private final LegacyComponentSerializer serializer = LegacyComponentSerializer.legacySection(); - - public RedisBungeeVelocityListener(RedisBungeePlugin plugin, List exemptAddresses) { - super(plugin, exemptAddresses); - } - - @Subscribe(order = PostOrder.LAST) - public void onLogin(LoginEvent event, Continuation continuation) { - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - try { - if (!event.getResult().isAllowed()) { - return null; - } - if (plugin.getConfiguration().restoreOldKickBehavior()) { - - for (String s : plugin.getProxiesIds()) { - if (unifiedJedis.sismember("proxy:" + s + ":usersOnline", event.getPlayer().getUniqueId().toString())) { - event.setResult(ResultedEvent.ComponentResult.denied(serializer.deserialize(plugin.getConfiguration().getMessages().get(RedisBungeeConfiguration.MessageType.ALREADY_LOGGED_IN)))); - return null; - } - } - - } else if (api.isPlayerOnline(event.getPlayer().getUniqueId())) { - PlayerUtils.setKickedOtherLocation(event.getPlayer().getUniqueId().toString(), unifiedJedis); - api.kickPlayer(event.getPlayer().getUniqueId(), plugin.getConfiguration().getMessages().get(RedisBungeeConfiguration.MessageType.LOGGED_IN_OTHER_LOCATION)); - } - return null; - } finally { - continuation.resume(); - } - } - - }); - } - - @Override - @Subscribe - public void onPostLogin(PostLoginEvent event) { - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - plugin.getUuidTranslator().persistInfo(event.getPlayer().getUsername(), event.getPlayer().getUniqueId(), unifiedJedis); - VelocityPlayerUtils.createVelocityPlayer(event.getPlayer(), unifiedJedis, true); - return null; - } - }); - } - - @Override - @Subscribe - public void onPlayerDisconnect(DisconnectEvent event) { - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - PlayerUtils.cleanUpPlayer(event.getPlayer().getUniqueId().toString(), unifiedJedis, true); - return null; - } - - }); - - } - - @Override - @Subscribe - public void onServerChange(ServerConnectedEvent event) { - final String currentServer = event.getServer().getServerInfo().getName(); - final String oldServer = event.getPreviousServer().map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null); - plugin.executeAsync(new RedisTask(plugin) { - @Override - public Void unifiedJedisTask(UnifiedJedis unifiedJedis) { - unifiedJedis.hset("player:" + event.getPlayer().getUniqueId().toString(), "server", currentServer); - PayloadUtils.playerServerChangePayload(event.getPlayer().getUniqueId(), unifiedJedis, currentServer, oldServer); - return null; - } - }); - } - - @Override - @Subscribe(order = PostOrder.LAST) // some plugins changes it online players so we need to be executed as last - public void onPing(ProxyPingEvent event) { - if (exemptAddresses.contains(event.getConnection().getRemoteAddress().getAddress())) { - return; - } - ServerPing.Builder ping = event.getPing().asBuilder(); - ping.onlinePlayers(plugin.getCount()); - event.setPing(ping.build()); - } - - @Override - @Subscribe - public void onPluginMessage(PluginMessageEvent event) { - if (!(event.getSource() instanceof ServerConnection) || !RedisBungeeVelocityPlugin.IDENTIFIERS.contains(event.getIdentifier())) { - return; - } - - event.setResult(ForwardResult.handled()); - - plugin.executeAsync(() -> { - ByteArrayDataInput in = event.dataAsDataStream(); - - String subchannel = in.readUTF(); - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - String type; - - switch (subchannel) { - case "PlayerList": - out.writeUTF("PlayerList"); - Set original = Collections.emptySet(); - type = in.readUTF(); - if (type.equals("ALL")) { - out.writeUTF("ALL"); - original = plugin.getPlayers(); - } else { - out.writeUTF(type); - try { - original = plugin.getAbstractRedisBungeeApi().getPlayersOnServer(type); - } catch (IllegalArgumentException ignored) { - } - } - Set players = original.stream() - .map(uuid -> plugin.getUuidTranslator().getNameFromUuid(uuid, false)) - .collect(Collectors.toSet()); - out.writeUTF(Joiner.on(',').join(players)); - break; - case "PlayerCount": - out.writeUTF("PlayerCount"); - type = in.readUTF(); - if (type.equals("ALL")) { - out.writeUTF("ALL"); - out.writeInt(plugin.getCount()); - } else { - out.writeUTF(type); - try { - out.writeInt(plugin.getAbstractRedisBungeeApi().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.getAbstractRedisBungeeApi().getLastOnline(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(user, true)))); - break; - case "ServerPlayers": - String type1 = in.readUTF(); - out.writeUTF("ServerPlayers"); - Multimap multimap = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - - boolean includesUsers; - - switch (type1) { - case "COUNT": - includesUsers = false; - break; - case "PLAYERS": - includesUsers = true; - break; - default: - // TODO: Should I raise an error? - return; - } - - out.writeUTF(type1); - - if (includesUsers) { - Multimap human = HashMultimap.create(); - for (Map.Entry entry : multimap.entries()) { - human.put(entry.getKey(), plugin.getUuidTranslator().getNameFromUuid(entry.getValue(), false)); - } - serializeMultimap(human, true, out); - } else { - serializeMultiset(multimap.keys(), out); - } - break; - case "Proxy": - out.writeUTF("Proxy"); - out.writeUTF(plugin.getConfiguration().getProxyId()); - break; - case "PlayerProxy": - String username = in.readUTF(); - out.writeUTF("PlayerProxy"); - out.writeUTF(username); - out.writeUTF(plugin.getAbstractRedisBungeeApi().getProxy(Objects.requireNonNull(plugin.getUuidTranslator().getTranslatedUuid(username, true)))); - break; - default: - return; - } - - ((ServerConnection) event.getSource()).sendPluginMessage(event.getIdentifier(), out.toByteArray()); - }); - - } - - - @Override - @Subscribe - public void onPubSubMessage(PubSubMessageEvent event) { - if (event.getChannel().equals("redisbungee-allservers") || event.getChannel().equals("redisbungee-" + plugin.getAbstractRedisBungeeApi().getProxyId())) { - String message = event.getMessage(); - if (message.startsWith("/")) - message = message.substring(1); - plugin.logInfo("Invoking command via PubSub: /" + message); - ((RedisBungeeVelocityPlugin) plugin).getProxy().getCommandManager().executeAsync(RedisBungeeCommandSource.getSingleton(), message); - - } - } -} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java index fe0d6c6..51edc96 100644 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/RedisBungeeVelocityPlugin.java @@ -10,24 +10,27 @@ package com.imaginarycode.minecraft.redisbungee; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Multimap; +import co.aikar.commands.VelocityCommandManager; import com.google.inject.Inject; -import com.imaginarycode.minecraft.redisbungee.api.*; -import com.imaginarycode.minecraft.redisbungee.api.config.ConfigLoader; +import com.imaginarycode.minecraft.redisbungee.api.PlayerDataManager; +import com.imaginarycode.minecraft.redisbungee.api.ProxyDataManager; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeeMode; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.commands.CommandLoader; +import com.imaginarycode.minecraft.redisbungee.commands.utils.CommandPlatformHelper; +import com.imaginarycode.minecraft.redisbungee.api.config.LangConfiguration; +import com.imaginarycode.minecraft.redisbungee.api.config.loaders.ConfigLoader; import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; +import com.imaginarycode.minecraft.redisbungee.api.config.loaders.LangConfigLoader; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerChangedServerNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerJoinedNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPlayerLeftNetworkEvent; import com.imaginarycode.minecraft.redisbungee.api.events.IPubSubMessageEvent; import com.imaginarycode.minecraft.redisbungee.api.summoners.Summoner; -import com.imaginarycode.minecraft.redisbungee.api.tasks.*; +import com.imaginarycode.minecraft.redisbungee.api.util.InitialUtils; import com.imaginarycode.minecraft.redisbungee.api.util.uuid.NameFetcher; import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDFetcher; import com.imaginarycode.minecraft.redisbungee.api.util.uuid.UUIDTranslator; -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.PlayerLeftNetworkEvent; @@ -46,55 +49,62 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.scheduler.ScheduledTask; +import net.kyori.adventure.text.Component; import org.slf4j.Logger; -import redis.clients.jedis.*; import redis.clients.jedis.exceptions.JedisConnectionException; - -import java.io.*; +import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; +import java.sql.Date; +import java.time.Duration; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Plugin(id = "redisbungee", name = "RedisBungee", version = Constants.VERSION, url = "https://github.com/ProxioDev/RedisBungee", authors = {"astei", "ProxioDev"}) -public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, ConfigLoader { +public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, ConfigLoader, LangConfigLoader { private final ProxyServer server; private final Logger logger; private final Path dataFolder; private final AbstractRedisBungeeAPI api; - private final PubSubListener psl; private Summoner jedisSummoner; private RedisBungeeMode redisBungeeMode; private final UUIDTranslator uuidTranslator; private RedisBungeeConfiguration configuration; - private final VelocityDataManager dataManager; + private LangConfiguration langConfiguration; private final OkHttpClient httpClient; - private volatile List proxiesIds; - private final AtomicInteger globalPlayerCount = new AtomicInteger(); - private ScheduledTask integrityCheck; + + private final ProxyDataManager proxyDataManager; + + private final VelocityPlayerDataManager playerDataManager; + + private ScheduledTask cleanUpTask; private ScheduledTask heartbeatTask; - private static final Object SERVER_TO_PLAYERS_KEY = new Object(); public static final List IDENTIFIERS = List.of( MinecraftChannelIdentifier.create("legacy", "redisbungee"), new LegacyChannelIdentifier("RedisBungee"), // This is needed for clients before 1.13 new LegacyChannelIdentifier("legacy:redisbungee") ); - private final Cache> serverToPlayersCache = CacheBuilder.newBuilder() - .expireAfterWrite(5, TimeUnit.SECONDS) - .build(); + private VelocityCommandManager commandManager; @Inject public RedisBungeeVelocityPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { this.server = server; this.logger = logger; this.dataFolder = dataDirectory; + logInfo("Version: {}", Constants.VERSION); try { loadConfig(this, dataDirectory); + loadLangConfig(this, dataDirectory); } catch (IOException e) { throw new RuntimeException("Unable to load/save config", e); } catch (JedisConnectionException e) { @@ -102,11 +112,21 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con } this.api = new RedisBungeeAPI(this); InitialUtils.checkRedisVersion(this); - // check if this proxy is recovering from a crash and start heart the beat. - InitialUtils.checkIfRecovering(this, getDataFolder()); + this.proxyDataManager = new ProxyDataManager(this) { + @Override + public Set getLocalOnlineUUIDs() { + HashSet players = new HashSet<>(); + server.getAllPlayers().forEach(player -> players.add(player.getUniqueId())); + return players; + } + + @Override + protected void handlePlatformCommandExecution(String command) { + server.getCommandManager().executeAsync(RedisBungeeCommandSource.getSingleton(), command); + } + }; + this.playerDataManager = new VelocityPlayerDataManager(this); uuidTranslator = new UUIDTranslator(this); - dataManager = new VelocityDataManager(this); - psl = new PubSubListener(this); this.httpClient = new OkHttpClient(); Dispatcher dispatcher = new Dispatcher(Executors.newFixedThreadPool(6)); this.httpClient.setDispatcher(dispatcher); @@ -115,31 +135,6 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con } - @Override - public RedisBungeeConfiguration getConfiguration() { - return this.configuration; - } - - @Override - public int getCount() { - return this.globalPlayerCount.get(); - } - - - @Override - public Set getLocalPlayersAsUuidStrings() { - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (Player player : getProxy().getAllPlayers()) { - builder.add(player.getUniqueId().toString()); - } - return builder.build(); - } - - @Override - public AbstractDataManager getDataManager() { - return this.dataManager; - } - @Override public Summoner getSummoner() { return this.jedisSummoner; @@ -150,29 +145,21 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con return this.api; } + @Override + public ProxyDataManager proxyDataManager() { + return this.proxyDataManager; + } + + @Override + public PlayerDataManager playerDataManager() { + return this.playerDataManager; + } + @Override public UUIDTranslator getUuidTranslator() { return this.uuidTranslator; } - @Override - public Multimap serverToPlayersCache() { - try { - return this.serverToPlayersCache.get(SERVER_TO_PLAYERS_KEY, this::serversToPlayers); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - - @Override - public List getProxiesIds() { - return proxiesIds; - } - - @Override - public PubSubListener getPubSubListener() { - return this.psl; - } @Override public void executeAsync(Runnable runnable) { @@ -199,16 +186,41 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con this.getLogger().info(msg); } + @Override + public void logInfo(String format, Object... object) { + logger.info(format, object); + } + @Override public void logWarn(String msg) { this.getLogger().warn(msg); } + @Override + public void logWarn(String format, Object... object) { + logger.warn(format, object); + } + @Override public void logFatal(String msg) { this.getLogger().error(msg); } + @Override + public void logFatal(String format, Throwable throwable) { + logger.error(format, throwable); + } + + @Override + public RedisBungeeConfiguration configuration() { + return this.configuration; + } + + @Override + public LangConfiguration langConfiguration() { + return this.langConfiguration; + } + @Override public Player getPlayer(UUID uuid) { return this.getProxy().getPlayer(uuid).orElse(null); @@ -229,6 +241,15 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con return this.getProxy().getPlayer(player).map(Player::getUsername).orElse(null); } + + @Override + public boolean handlePlatformKick(UUID uuid, Component message) { + Player player = getPlayer(uuid); + if (player == null) return false; + player.disconnect(message); + return true; + } + @Override public String getPlayerServerName(Player player) { return player.getCurrentServer().map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(null); @@ -247,44 +268,25 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con @Override public void initialize() { logInfo("Initializing RedisBungee....."); - updateProxiesIds(); // start heartbeat task - heartbeatTask = getProxy().getScheduler().buildTask(this, new HeartbeatTask(this, this.globalPlayerCount)).repeat(HeartbeatTask.INTERVAL, HeartbeatTask.REPEAT_INTERVAL_TIME_UNIT).schedule(); + // heartbeat and clean up + this.heartbeatTask = server.getScheduler().buildTask(this, this.proxyDataManager::publishHeartbeat).repeat(Duration.ofSeconds(1)).schedule(); + this.cleanUpTask = server.getScheduler().buildTask(this, this.proxyDataManager::correctionTask).repeat(Duration.ofSeconds(60)).schedule(); - getProxy().getEventManager().register(this, new RedisBungeeVelocityListener(this, configuration.getExemptAddresses())); - getProxy().getEventManager().register(this, dataManager); - getProxy().getScheduler().buildTask(this, psl).schedule(); - - IntegrityCheckTask integrityCheckTask = new IntegrityCheckTask(this) { - @Override - public void handlePlatformPlayer(String player, UnifiedJedis unifiedJedis) { - Player playerProxied = getProxy().getPlayer(UUID.fromString(player)).orElse(null); - if (playerProxied == null) - return; // We'll deal with it later. - VelocityPlayerUtils.createVelocityPlayer(playerProxied, unifiedJedis, false); - } - }; - integrityCheck = getProxy().getScheduler().buildTask(this, integrityCheckTask::execute).repeat(30, TimeUnit.SECONDS).schedule(); + server.getEventManager().register(this, this.playerDataManager); + server.getEventManager().register(this, new RedisBungeeListener(this)); + // subscribe + server.getScheduler().buildTask(this, this.proxyDataManager).schedule(); // register plugin messages IDENTIFIERS.forEach(getProxy().getChannelRegistrar()::register); - // register legacy commands - if (configuration.doRegisterLegacyCommands()) { - // Override Velocity commands - if (configuration.doOverrideBungeeCommands()) { - getProxy().getCommandManager().register("glist", new RedisBungeeCommands.GlistCommand(this), "redisbungee", "rglist"); - } - getProxy().getCommandManager().register("sendtoall", new RedisBungeeCommands.SendToAll(this), "rsendtoall"); - getProxy().getCommandManager().register("serverid", new RedisBungeeCommands.ServerId(this), "rserverid"); - getProxy().getCommandManager().register("serverids", new RedisBungeeCommands.ServerIds(this)); - getProxy().getCommandManager().register("pproxy", new RedisBungeeCommands.PlayerProxyCommand(this)); - getProxy().getCommandManager().register("plist", new RedisBungeeCommands.PlistCommand(this), "rplist"); - getProxy().getCommandManager().register("lastseen", new RedisBungeeCommands.LastSeenCommand(this), "rlastseen"); - getProxy().getCommandManager().register("ip", new RedisBungeeCommands.IpCommand(this), "playerip", "rip", "rplayerip"); - getProxy().getCommandManager().register("find", new RedisBungeeCommands.FindCommand(this), "rfind"); - } + // load commands + CommandPlatformHelper.init(new VelocityCommandPlatformHelper()); + this.commandManager = new VelocityCommandManager(this.getProxy(), this); + CommandLoader.initCommands(this.commandManager, this); + logInfo("RedisBungee initialized successfully "); } @@ -292,19 +294,18 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con public void stop() { logInfo("Turning off redis connections....."); // Poison the PubSub listener - if (psl != null) { - psl.poison(); - } - if (integrityCheck != null) { - integrityCheck.cancel(); + if (cleanUpTask != null) { + cleanUpTask.cancel(); } if (heartbeatTask != null) { heartbeatTask.cancel(); } - ShutdownUtils.shutdownCleanup(this); + + try { + this.proxyDataManager.close(); this.jedisSummoner.close(); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } @@ -315,6 +316,7 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con } catch (InterruptedException e) { throw new RuntimeException(e); } + if (commandManager != null) commandManager.unregisterCommands(); logInfo("RedisBungee shutdown complete"); } @@ -325,16 +327,16 @@ public class RedisBungeeVelocityPlugin implements RedisBungeePlugin, Con this.redisBungeeMode = mode; } + @Override + public void onLangConfigLoad(LangConfiguration langConfiguration) { + this.langConfiguration = langConfiguration; + } @Override public RedisBungeeMode getRedisBungeeMode() { return this.redisBungeeMode; } - @Override - public void updateProxiesIds() { - this.proxiesIds = this.getCurrentProxiesIds(false); - } @Subscribe(order = PostOrder.FIRST) public void onProxyInitializeEvent(ProxyInitializeEvent event) { diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityCommandPlatformHelper.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityCommandPlatformHelper.java new file mode 100644 index 0000000..07cd9de --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityCommandPlatformHelper.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee; + +import co.aikar.commands.CommandIssuer; +import co.aikar.commands.VelocityCommandIssuer; +import com.imaginarycode.minecraft.redisbungee.commands.utils.CommandPlatformHelper; +import net.kyori.adventure.text.Component; + +public class VelocityCommandPlatformHelper extends CommandPlatformHelper { + + @Override + public void sendMessage(CommandIssuer issuer, Component component) { + VelocityCommandIssuer vIssuer = (VelocityCommandIssuer) issuer; + vIssuer.getIssuer().sendMessage(component); + } + +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java deleted file mode 100644 index 4ad9ef3..0000000 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityDataManager.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee; - -import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; -import com.imaginarycode.minecraft.redisbungee.api.AbstractDataManager; -import com.imaginarycode.minecraft.redisbungee.api.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; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; - -import java.util.UUID; - - -public class VelocityDataManager extends AbstractDataManager { - - public VelocityDataManager(RedisBungeePlugin plugin) { - super(plugin); - } - - @Override - @Subscribe - public void onPostLogin(PostLoginEvent event) { - invalidate(event.getPlayer().getUniqueId()); - } - - @Override - @Subscribe - public void onPlayerDisconnect(DisconnectEvent event) { - invalidate(event.getPlayer().getUniqueId()); - } - - @Override - @Subscribe - public void onPubSubMessage(PubSubMessageEvent event) { - handlePubSubMessage(event.getChannel(), event.getMessage()); - } - - private final LegacyComponentSerializer serializer = LegacyComponentSerializer.legacySection(); - @Override - public boolean handleKick(UUID target, String message) { - Player player = plugin.getPlayer(target); - if (player == null) { - return false; - } - player.disconnect(serializer.deserialize(message)); - return true; - } -} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerDataManager.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerDataManager.java new file mode 100644 index 0000000..ea89d90 --- /dev/null +++ b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerDataManager.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2013-present RedisBungee contributors + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.imaginarycode.minecraft.redisbungee; + +import com.imaginarycode.minecraft.redisbungee.api.PlayerDataManager; +import com.imaginarycode.minecraft.redisbungee.api.RedisBungeePlugin; +import com.imaginarycode.minecraft.redisbungee.api.config.RedisBungeeConfiguration; +import com.imaginarycode.minecraft.redisbungee.events.PlayerChangedServerNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PlayerLeftNetworkEvent; +import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; +import com.velocitypowered.api.event.Continuation; +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.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.text.Component; + +import java.util.concurrent.TimeUnit; + +public class VelocityPlayerDataManager extends PlayerDataManager { + public VelocityPlayerDataManager(RedisBungeePlugin plugin) { + super(plugin); + } + + @Override + @Subscribe + public void onPlayerChangedServerNetworkEvent(PlayerChangedServerNetworkEvent event) { + handleNetworkPlayerServerChange(event); + } + + @Override + @Subscribe + public void onNetworkPlayerQuit(PlayerLeftNetworkEvent event) { + handleNetworkPlayerQuit(event); + } + + @Override + @Subscribe + public void onPubSubMessageEvent(PubSubMessageEvent event) { + handlePubSubMessageEvent(event); + } + + @Override + @Subscribe + public void onServerConnectedEvent(ServerConnectedEvent event) { + final String currentServer = event.getServer().getServerInfo().getName(); + final String oldServer; + if (event.getPreviousServer().isPresent()) { + oldServer = event.getPreviousServer().get().getServerInfo().getName(); + } else { + oldServer = null; + } + super.playerChangedServer(event.getPlayer().getUniqueId(), oldServer, currentServer); + } + + @Subscribe + public void onLoginEvent(LoginEvent event, Continuation continuation) { + // check if online + if (getLastOnline(event.getPlayer().getUniqueId()) == 0) { + if (plugin.configuration().kickWhenOnline()) { + kickPlayer(event.getPlayer().getUniqueId(), plugin.langConfiguration().messages().loggedInFromOtherLocation()); + // wait 3 seconds before releasing the event + plugin.executeAsyncAfter(continuation::resume, TimeUnit.SECONDS, 3); + } else { + event.setResult(ResultedEvent.ComponentResult.denied(plugin.langConfiguration().messages().alreadyLoggedIn())); + continuation.resume(); + } + } else { + continuation.resume(); + } + } + + @Override + @Subscribe + public void onLoginEvent(PostLoginEvent event) { + addPlayer(event.getPlayer().getUniqueId(), event.getPlayer().getRemoteAddress().getAddress()); + } + + @Override + @Subscribe + public void onDisconnectEvent(DisconnectEvent event) { + if (event.getLoginStatus() == DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN || event.getLoginStatus() == DisconnectEvent.LoginStatus.PRE_SERVER_JOIN) { + removePlayer(event.getPlayer().getUniqueId()); + } + } +} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerUtils.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerUtils.java deleted file mode 100644 index 2f43fc8..0000000 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/VelocityPlayerUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee; - -import com.imaginarycode.minecraft.redisbungee.api.util.player.PlayerUtils; -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ServerConnection; -import redis.clients.jedis.UnifiedJedis; - -import java.util.Optional; - -public class VelocityPlayerUtils { - protected static void createVelocityPlayer(Player player, UnifiedJedis unifiedJedis, boolean fireEvent) { - Optional optionalServerConnection = player.getCurrentServer(); - String serverName = null; - if (optionalServerConnection.isPresent()) { - serverName = optionalServerConnection.get().getServerInfo().getName(); - } - PlayerUtils.createPlayer(player.getUniqueId(), unifiedJedis, serverName, player.getRemoteAddress().getAddress(), fireEvent); - } - - -} diff --git a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java b/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java deleted file mode 100644 index 3537981..0000000 --- a/RedisBungee-Velocity/src/main/java/com/imaginarycode/minecraft/redisbungee/commands/RedisBungeeCommands.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * Copyright (c) 2013-present RedisBungee contributors - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * - * http://www.eclipse.org/legal/epl-v10.html - */ - -package com.imaginarycode.minecraft.redisbungee.commands; - -import java.net.InetAddress; -import java.text.SimpleDateFormat; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; - -import com.google.common.base.Joiner; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.imaginarycode.minecraft.redisbungee.AbstractRedisBungeeAPI; -import com.imaginarycode.minecraft.redisbungee.RedisBungeeVelocityPlugin; -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.command.SimpleCommand; -import com.velocitypowered.api.proxy.server.RegisteredServer; -import com.velocitypowered.api.proxy.server.ServerInfo; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.format.NamedTextColor; - - -/** - * This class contains subclasses that are used for the commands RedisBungee overrides or includes: /glist, /find and /lastseen. - *

- * All classes use the {@link AbstractRedisBungeeAPI}. - * - * @author tuxed - * @since 0.2.3 - */ -public class RedisBungeeCommands { - - private static final Component NO_PLAYER_SPECIFIED = - Component.text("You must specify a player name.", NamedTextColor.RED); - private static final Component PLAYER_NOT_FOUND = - Component.text("No such player found.", NamedTextColor.RED); - private static final Component NO_COMMAND_SPECIFIED = - Component.text("You must specify a command to be run.", NamedTextColor.RED); - - private static String playerPlural(int num) { - return num == 1 ? num + " player is" : num + " players are"; - } - - public static class GlistCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public GlistCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - int count = plugin.getAbstractRedisBungeeApi().getPlayerCount(); - Component playersOnline = Component.text(playerPlural(count) + " currently online.", NamedTextColor.YELLOW); - CommandSource sender = invocation.source(); - if (invocation.arguments().length > 0 && invocation.arguments()[0].equals("showall")) { - Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - Multimap human = HashMultimap.create(); - serverToPlayers.forEach((key, value) -> { - // if for any reason UUID translation fails just return the uuid as name, to make command finish executing. - String playerName = plugin.getUuidTranslator().getNameFromUuid(value, false); - human.put(key, playerName != null ? playerName : value.toString()); - }); - for (String server : new TreeSet<>(serverToPlayers.keySet())) { - Component serverName = Component.text("[" + server + "] ", NamedTextColor.GREEN); - Component serverCount = Component.text("(" + serverToPlayers.get(server).size() + "): ", NamedTextColor.YELLOW); - Component serverPlayers = Component.text(Joiner.on(", ").join(human.get(server)), NamedTextColor.WHITE); - sender.sendMessage(Component.textOfChildren(serverName, serverCount, serverPlayers)); - } - sender.sendMessage(playersOnline); - } else { - sender.sendMessage(playersOnline); - sender.sendMessage(Component.text("To see all players online, use /glist showall.", NamedTextColor.YELLOW)); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("velocity.command.server"); - } - } - - public static class FindCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public FindCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - String[] args = invocation.arguments(); - CommandSource sender = invocation.source(); - 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().getServer(plugin.getAbstractRedisBungeeApi().getServerNameFor(uuid)).map(RegisteredServer::getServerInfo).orElse(null); - if (si != null) { - Component message = Component.text(args[0] + " is on " + si.getName() + ".", NamedTextColor.BLUE); - sender.sendMessage(message); - } else { - sender.sendMessage(PLAYER_NOT_FOUND); - } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.find"); - } - } - - public static class LastSeenCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public LastSeenCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - String[] args = invocation.arguments(); - CommandSource sender = invocation.source(); - if (args.length > 0) { - UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); - if (uuid == null) { - sender.sendMessage(PLAYER_NOT_FOUND); - return; - } - long secs = plugin.getAbstractRedisBungeeApi().getLastOnline(uuid); - TextComponent.Builder message = Component.text(); - if (secs == 0) { - message.color(NamedTextColor.GREEN); - message.content(args[0] + " is currently online."); - } else if (secs != -1) { - message.color(NamedTextColor.BLUE); - message.content(args[0] + " was last online on " + new SimpleDateFormat().format(secs) + "."); - } else { - message.color(NamedTextColor.RED); - message.content(args[0] + " has never been online."); - } - sender.sendMessage(message.build()); - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.lastseen"); - } - } - - public static class IpCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public IpCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - CommandSource sender = invocation.source(); - String[] args = invocation.arguments(); - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - if (args.length > 0) { - UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); - if (uuid == null) { - sender.sendMessage(PLAYER_NOT_FOUND); - return; - } - InetAddress ia = plugin.getAbstractRedisBungeeApi().getPlayerIp(uuid); - if (ia != null) { - TextComponent message = Component.text(args[0] + " is connected from " + ia.toString() + ".", NamedTextColor.GREEN); - sender.sendMessage(message); - } else { - sender.sendMessage(PLAYER_NOT_FOUND); - } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.ip"); - } - } - - public static class PlayerProxyCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public PlayerProxyCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - CommandSource sender = invocation.source(); - String[] args = invocation.arguments(); - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - if (args.length > 0) { - UUID uuid = plugin.getUuidTranslator().getTranslatedUuid(args[0], true); - if (uuid == null) { - sender.sendMessage(PLAYER_NOT_FOUND); - return; - } - String proxy = plugin.getAbstractRedisBungeeApi().getProxy(uuid); - if (proxy != null) { - TextComponent message = Component.text(args[0] + " is connected to " + proxy + ".", NamedTextColor.GREEN); - sender.sendMessage(message); - } else { - sender.sendMessage(PLAYER_NOT_FOUND); - } - } else { - sender.sendMessage(NO_PLAYER_SPECIFIED); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.pproxy"); - } - } - - public static class SendToAll implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public SendToAll(RedisBungeeVelocityPlugin plugin) { - //super("sendtoall", "redisbungee.command.sendtoall", "rsendtoall"); - this.plugin = plugin; - } - - @Override - public void execute(final Invocation invocation) { - String[] args = invocation.arguments(); - CommandSource sender = invocation.source(); - if (args.length > 0) { - String command = Joiner.on(" ").skipNulls().join(args); - plugin.getAbstractRedisBungeeApi().sendProxyCommand(command); - TextComponent message = Component.text("Sent the command /" + command + " to all proxies.", NamedTextColor.GREEN); - sender.sendMessage(message); - } else { - sender.sendMessage(NO_COMMAND_SPECIFIED); - } - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.sendtoall"); - } - } - - public static class ServerId implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public ServerId(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(Invocation invocation) { - invocation.source().sendMessage(Component.text("You are on " + plugin.getAbstractRedisBungeeApi().getProxyId() + ".", NamedTextColor.YELLOW)); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.serverid"); - } - } - - public static class ServerIds implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public ServerIds(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(Invocation invocation) { - invocation.source().sendMessage( - Component.text("All server IDs: " + Joiner.on(", ").join(plugin.getAbstractRedisBungeeApi().getAllProxies()), NamedTextColor.YELLOW)); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.serverids"); - } - } - - public static class PlistCommand implements SimpleCommand { - private final RedisBungeeVelocityPlugin plugin; - - public PlistCommand(RedisBungeeVelocityPlugin plugin) { - this.plugin = plugin; - } - - @Override - public void execute(Invocation invocation) { - CommandSource sender = invocation.source(); - String[] args = invocation.arguments(); - plugin.getProxy().getScheduler().buildTask(plugin, () -> { - String proxy = args.length >= 1 ? args[0] : plugin.getConfiguration().getProxyId(); - if (!plugin.getProxiesIds().contains(proxy)) { - sender.sendMessage(Component.text(proxy + " is not a valid proxy. See /serverids for valid proxies.", NamedTextColor.RED)); - return; - } - Set players = plugin.getAbstractRedisBungeeApi().getPlayersOnProxy(proxy); - Component playersOnline = Component.text(playerPlural(players.size()) + " currently on proxy " + proxy + ".", NamedTextColor.YELLOW); - if (args.length >= 2 && args[1].equals("showall")) { - Multimap serverToPlayers = plugin.getAbstractRedisBungeeApi().getServerToPlayers(); - Multimap human = HashMultimap.create(); - serverToPlayers.forEach((key, value) -> { - if (players.contains(value)) { - human.put(key, plugin.getUuidTranslator().getNameFromUuid(value, false)); - } - }); - for (String server : new TreeSet<>(human.keySet())) { - TextComponent serverName = Component.text("[" + server + "] ", NamedTextColor.RED); - TextComponent serverCount = Component.text("(" + human.get(server).size() + "): ", NamedTextColor.YELLOW); - TextComponent serverPlayers = Component.text(Joiner.on(", ").join(human.get(server)), NamedTextColor.WHITE); - sender.sendMessage(Component.textOfChildren(serverName, serverCount, serverPlayers)); - } - sender.sendMessage(playersOnline); - } else { - sender.sendMessage(playersOnline); - sender.sendMessage(Component.text("To see all players online, use /plist " + proxy + " showall.", NamedTextColor.YELLOW)); - } - }).schedule(); - } - - @Override - public boolean hasPermission(Invocation invocation) { - return invocation.source().hasPermission("redisbungee.command.plist"); - } - } - -} \ No newline at end of file diff --git a/gradle.build.kts b/build.gradle.kts similarity index 100% rename from gradle.build.kts rename to build.gradle.kts diff --git a/gradle.properties b/gradle.properties index 5440772..18ec268 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -group = com.imaginarycode.minecraft -version = 0.11.4-SNAPSHOT +group=com.imaginarycode.minecraft +version=0.12.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 62076 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cb..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/settings.gradle.kts b/settings.gradle.kts index 181ac78..f3ff03f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,5 +7,70 @@ pluginManagement { rootProject.name = "RedisBungee-Parent" include(":RedisBungee-Velocity") +include(":RedisBungee-Commands") include(":RedisBungee-Bungee") include(":RedisBungee-API") + +dependencyResolutionManagement { + repositories { + mavenCentral() + maven { + name = "PaperMC" + url = uri("https://repo.papermc.io/repository/maven-public/") + } + maven { + // hosts the bungeecord apis + name = "sonatype" + url = uri("https://oss.sonatype.org/content/repositories/snapshots") + } + maven { + name = "aikar repo" + url = uri("https://repo.aikar.co/content/groups/aikar/") + } + + } + versionCatalogs { + val jedisVersion = "5.1.2" + val configurateVersion = "3.7.3" + val guavaVersion = "31.1-jre" + val okHttpVersion = "2.7.5" + val caffeineVersion = "3.1.8" + val adventureVersion = "4.16.0" + val acf = "0.5.1-SNAPSHOT" + val bungeecordApiVersion = "1.20-R0.1-SNAPSHOT" + val velocityVersion = "3.3.0-SNAPSHOT"; + + + create("libs") { + + library("guava", "com.google.guava:guava:$guavaVersion") + library("jedis", "redis.clients:jedis:$jedisVersion") + library("okhttp", "com.squareup.okhttp:okhttp:$okHttpVersion") + library("configurate", "org.spongepowered:configurate-yaml:$configurateVersion") + library("caffeine", "com.github.ben-manes.caffeine:caffeine:$caffeineVersion") + + library("adventure-api", "net.kyori:adventure-api:$adventureVersion") + library("adventure-gson", "net.kyori:adventure-text-serializer-gson:$adventureVersion") + library("adventure-legacy", "net.kyori:adventure-text-serializer-legacy:$adventureVersion") + library("adventure-plain", "net.kyori:adventure-text-serializer-plain:$adventureVersion") + library("adventure-miniMessage", "net.kyori:adventure-text-minimessage:$adventureVersion") + + library("acf-core", "co.aikar:acf-core:$acf") + library("acf-bungeecord", "co.aikar:acf-bungee:$acf") + library("acf-velocity", "co.aikar:acf-velocity:$acf") + + library("platform-bungeecord","net.md-5:bungeecord-api:$bungeecordApiVersion") + library("adventure-platforms-bungeecord", "net.kyori:adventure-platform-bungeecord:4.3.2") + + library("platform-velocity", "com.velocitypowered:velocity-api:$velocityVersion") + + + + + } + + + } + + +} \ No newline at end of file