From 223e6e82bd01c76d15a745762514f9dea3c55409 Mon Sep 17 00:00:00 2001 From: objz Date: Sat, 21 Mar 2026 23:52:52 +0100 Subject: [PATCH 1/4] feat(api): add public API module --- api/build.gradle.kts | 22 +++++++++++ .../commandbridge/api/CommandBridgeAPI.java | 35 +++++++++++++++++ .../api/CommandBridgeProvider.java | 39 +++++++++++++++++++ .../api/channel/ChannelPayload.java | 4 ++ .../api/channel/ChannelType.java | 16 ++++++++ .../commandbridge/api/channel/Channels.java | 12 ++++++ .../api/channel/MessageChannel.java | 21 ++++++++++ .../api/channel/command/CommandChannel.java | 21 ++++++++++ .../channel/command/CommandChannelType.java | 10 +++++ .../api/channel/command/CommandPayload.java | 20 ++++++++++ .../api/channel/command/RunAs.java | 7 ++++ .../api/message/MessageContext.java | 13 +++++++ .../api/message/MessageListener.java | 8 ++++ .../api/message/ServerEventListener.java | 8 ++++ .../api/message/Subscription.java | 6 +++ .../api/platform/ConnectionState.java | 28 +++++++++++++ .../commandbridge/api/platform/Platform.java | 13 +++++++ .../api/platform/PlayerLocator.java | 9 +++++ backends/build.gradle.kts | 1 + settings.gradle.kts | 1 + velocity/build.gradle.kts | 1 + 21 files changed, 295 insertions(+) create mode 100644 api/build.gradle.kts create mode 100644 api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java create mode 100644 api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 00000000..878084c4 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `java-library` + `checkstyle` +} + +java { + toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } + withSourcesJar() + withJavadocJar() +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(libs.junit.jupiter) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java new file mode 100644 index 00000000..7eff70a6 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java @@ -0,0 +1,35 @@ +package dev.objz.commandbridge.api; + +import dev.objz.commandbridge.api.channel.ChannelPayload; +import dev.objz.commandbridge.api.channel.ChannelType; +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.message.ServerEventListener; +import dev.objz.commandbridge.api.message.Subscription; +import dev.objz.commandbridge.api.platform.ConnectionState; +import dev.objz.commandbridge.api.platform.Platform; +import dev.objz.commandbridge.api.platform.PlayerLocator; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; + +public interface CommandBridgeAPI { + + > C channel(ChannelType type); + +

void broadcast(MessageChannel

channel, P payload); + + Platform.ServerTarget server(); + + ConnectionState connectionState(); + + Optional> connectedServers(); + + Optional playerLocator(); + + Subscription onServerConnected(ServerEventListener listener); + + Subscription onServerDisconnected(ServerEventListener listener); + + Subscription onConnectionStateChanged(Consumer listener); +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java new file mode 100644 index 00000000..1a79898a --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java @@ -0,0 +1,39 @@ +package dev.objz.commandbridge.api; + +import java.util.Objects; + +public final class CommandBridgeProvider { + + private static volatile CommandBridgeAPI instance; + + private CommandBridgeProvider() { + throw new UnsupportedOperationException("Provider can't be instanced"); + } + + public static CommandBridgeAPI get() { + if (instance == null) { + throw new IllegalStateException("CommandBridge is not available. Is it installed and running?"); + } + return instance; + } + + public static T get(Class type) { + CommandBridgeAPI api = get(); + if (!type.isInstance(api)) { + throw new IllegalStateException(type.getSimpleName() + " is not available on this platform"); + } + return type.cast(api); + } + + static void register(CommandBridgeAPI impl) { + Objects.requireNonNull(impl); + if (instance != null) { + throw new IllegalStateException("CommandBridge already registered"); + } + instance = impl; + } + + static void unregister() { + instance = null; + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java new file mode 100644 index 00000000..42064755 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java @@ -0,0 +1,4 @@ +package dev.objz.commandbridge.api.channel; + +public interface ChannelPayload { +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java new file mode 100644 index 00000000..7df5aa5c --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java @@ -0,0 +1,16 @@ +package dev.objz.commandbridge.api.channel; + +import java.util.Objects; + +public abstract class ChannelType> { + + private final Class type; + + protected ChannelType(Class type) { + this.type = Objects.requireNonNull(type); + } + + public Class type() { + return type; + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java b/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java new file mode 100644 index 00000000..17e0e5d0 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java @@ -0,0 +1,12 @@ +package dev.objz.commandbridge.api.channel; + +import dev.objz.commandbridge.api.channel.command.CommandChannelType; + +public final class Channels { + + public static final CommandChannelType COMMAND = new CommandChannelType(); + + private Channels() { + throw new UnsupportedOperationException("Channels can't be instanced"); + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java new file mode 100644 index 00000000..a42c840a --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java @@ -0,0 +1,21 @@ +package dev.objz.commandbridge.api.channel; + +import dev.objz.commandbridge.api.message.MessageListener; +import dev.objz.commandbridge.api.message.Subscription; +import dev.objz.commandbridge.api.platform.Platform; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public interface MessageChannel

{ + + void send(Platform.ServerTarget target, P payload); + + CompletableFuture sendAsync(Platform.ServerTarget target, P payload); + + CompletableFuture

request(Platform.ServerTarget target, P payload); + + CompletableFuture

request(Platform.ServerTarget target, P payload, Duration timeout); + + Subscription listen(MessageListener

listener); +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java new file mode 100644 index 00000000..96bcac57 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java @@ -0,0 +1,21 @@ +package dev.objz.commandbridge.api.channel.command; + +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.platform.Platform; + +import java.util.UUID; + +public interface CommandChannel extends MessageChannel { + + default void console(Platform.ServerTarget target, String command) { + send(target, CommandPayload.console(command)); + } + + default void player(Platform.ServerTarget target, String command, UUID player) { + send(target, CommandPayload.player(command, player)); + } + + default void operator(Platform.ServerTarget target, String command, UUID player) { + send(target, CommandPayload.operator(command, player)); + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java new file mode 100644 index 00000000..b9ba2ded --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java @@ -0,0 +1,10 @@ +package dev.objz.commandbridge.api.channel.command; + +import dev.objz.commandbridge.api.channel.ChannelType; + +public final class CommandChannelType extends ChannelType { + + public CommandChannelType() { + super(CommandPayload.class); + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java new file mode 100644 index 00000000..b6987e9e --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java @@ -0,0 +1,20 @@ +package dev.objz.commandbridge.api.channel.command; + +import dev.objz.commandbridge.api.channel.ChannelPayload; + +import java.util.UUID; + +public record CommandPayload(String command, RunAs runAs, UUID player) implements ChannelPayload { + + public static CommandPayload console(String command) { + return new CommandPayload(command, RunAs.CONSOLE, null); + } + + public static CommandPayload player(String command, UUID player) { + return new CommandPayload(command, RunAs.PLAYER, player); + } + + public static CommandPayload operator(String command, UUID player) { + return new CommandPayload(command, RunAs.OPERATOR, player); + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java new file mode 100644 index 00000000..d5faafea --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java @@ -0,0 +1,7 @@ +package dev.objz.commandbridge.api.channel.command; + +public enum RunAs { + CONSOLE, + PLAYER, + OPERATOR +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java b/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java new file mode 100644 index 00000000..7d5f8242 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java @@ -0,0 +1,13 @@ +package dev.objz.commandbridge.api.message; + +import dev.objz.commandbridge.api.channel.ChannelPayload; +import dev.objz.commandbridge.api.channel.ChannelType; +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.platform.Platform; + +public record MessageContext( + ChannelType> channel, + Platform.ServerTarget from, + long timestamp +) { +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java b/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java new file mode 100644 index 00000000..907a79ed --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java @@ -0,0 +1,8 @@ +package dev.objz.commandbridge.api.message; + +import dev.objz.commandbridge.api.channel.ChannelPayload; + +@FunctionalInterface +public interface MessageListener { + void accept(MessageContext ctx, T payload); +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java b/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java new file mode 100644 index 00000000..ceecccad --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java @@ -0,0 +1,8 @@ +package dev.objz.commandbridge.api.message; + +import dev.objz.commandbridge.api.platform.Platform; + +@FunctionalInterface +public interface ServerEventListener { + void accept(Platform.ServerTarget server); +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java b/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java new file mode 100644 index 00000000..028775d7 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java @@ -0,0 +1,6 @@ +package dev.objz.commandbridge.api.message; + +@FunctionalInterface +public interface Subscription { + void cancel(); +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java b/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java new file mode 100644 index 00000000..c1297e45 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java @@ -0,0 +1,28 @@ +package dev.objz.commandbridge.api.platform; + +public enum ConnectionState { + + DISCONNECTED, + + CONNECTING, + + CONNECTED, + + AUTHENTICATED, + + RECONNECTING, + + AUTH_FAILED; + + public boolean isActive() { + return this == CONNECTED || this == AUTHENTICATED; + } + + public boolean canSend() { + return this == AUTHENTICATED; + } + + public boolean isTerminal() { + return this == AUTH_FAILED; + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java b/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java new file mode 100644 index 00000000..cabed602 --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java @@ -0,0 +1,13 @@ +package dev.objz.commandbridge.api.platform; + +public enum Platform { + BACKEND, + VELOCITY; + + public ServerTarget target(String id) { + return new ServerTarget(id, this); + } + + public record ServerTarget(String id, Platform type) { + } +} diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java b/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java new file mode 100644 index 00000000..9d6d43da --- /dev/null +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java @@ -0,0 +1,9 @@ +package dev.objz.commandbridge.api.platform; + +import java.util.Optional; +import java.util.UUID; + +@FunctionalInterface +public interface PlayerLocator { + Optional locate(UUID player); +} diff --git a/backends/build.gradle.kts b/backends/build.gradle.kts index 22ee10b1..57e935d0 100644 --- a/backends/build.gradle.kts +++ b/backends/build.gradle.kts @@ -13,6 +13,7 @@ repositories { } dependencies { + api(project(":api")) api(project(":core")) api("com.squareup.okhttp3:okhttp:4.12.0") api("redis.clients:jedis:7.1.0") diff --git a/settings.gradle.kts b/settings.gradle.kts index 746d2302..ac3f37fc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ plugins { rootProject.name = "CommandBridge" include("core") include("velocity") +include("api") include("dist") diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 693d4d3e..e0e18904 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -17,6 +17,7 @@ repositories { } dependencies { + implementation(project(":api")) implementation(project(":core")) implementation("redis.clients:jedis:7.1.0") From b1536849ea8038c30ec08f3f5e91d0ee100a6ab9 Mon Sep 17 00:00:00 2001 From: objz Date: Sat, 21 Mar 2026 23:55:17 +0100 Subject: [PATCH 2/4] refactor: use API RunAs and ConnectionState, remove internal duplicates --- .../commandbridge/folia/FoliaExecutor.java | 2 +- .../backends/net/client/RedisClient.java | 4 +- .../backends/net/client/WsClient.java | 4 +- .../backends/net/connection/ClientStatus.java | 22 ++++++++- .../net/connection/ConnectionHandler.java | 1 + .../net/connection/ConnectionState.java | 46 ------------------- .../net/in/ExecuteCommandHandler.java | 2 +- .../backends/net/routing/AuthHandler.java | 4 +- .../backends/net/routing/MessageRouter.java | 2 +- .../net/routing/RedisMessageRouter.java | 2 +- .../backends/platform/PlatformExecutor.java | 2 +- .../velocity/VelocityExecutor.java | 2 +- core/build.gradle.kts | 1 + .../net/payloads/cmd/ExecuteCommand.java | 2 +- .../scripting/model/Defaults.java | 2 +- .../scripting/model/enums/RunAs.java | 7 --- .../model/records/mapping/CmdMapping.java | 2 +- .../velocity/dispatch/CommandDispatcher.java | 2 +- .../dispatch/exec/LocalDispatcher.java | 2 +- .../net/out/ctx/ExecuteCommandContext.java | 2 +- 20 files changed, 41 insertions(+), 72 deletions(-) delete mode 100644 backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionState.java delete mode 100644 core/src/main/java/dev/objz/commandbridge/scripting/model/enums/RunAs.java diff --git a/backends/folia/src/main/java/dev/objz/commandbridge/folia/FoliaExecutor.java b/backends/folia/src/main/java/dev/objz/commandbridge/folia/FoliaExecutor.java index dffaa8ae..39db8e16 100644 --- a/backends/folia/src/main/java/dev/objz/commandbridge/folia/FoliaExecutor.java +++ b/backends/folia/src/main/java/dev/objz/commandbridge/folia/FoliaExecutor.java @@ -3,7 +3,7 @@ import dev.objz.commandbridge.backends.platform.PlatformExecutor; import dev.objz.commandbridge.logging.Log; import dev.objz.commandbridge.net.payloads.cmd.ExecuteCommand; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler; import io.papermc.paper.threadedregions.scheduler.EntityScheduler; import org.bukkit.Bukkit; diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java index 1f727ddd..ffea4ff7 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java @@ -1,7 +1,7 @@ package dev.objz.commandbridge.backends.net.client; import dev.objz.commandbridge.backends.net.connection.ClientStatus; -import dev.objz.commandbridge.backends.net.connection.ConnectionState; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.backends.net.connection.ReconnectHandler; import dev.objz.commandbridge.backends.net.routing.AuthHandler; import dev.objz.commandbridge.backends.net.routing.RedisMessageRouter; @@ -143,7 +143,7 @@ public synchronized void close() { @Override public ClientStatus status() { - return stateRef.get().toClientStatus(); + return ClientStatus.fromConnectionState(stateRef.get()); } @Override diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java index 9518c680..d3686d76 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java @@ -1,7 +1,7 @@ package dev.objz.commandbridge.backends.net.client; import dev.objz.commandbridge.backends.net.connection.ConnectionHandler; -import dev.objz.commandbridge.backends.net.connection.ConnectionState; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.backends.net.connection.ClientStatus; import dev.objz.commandbridge.backends.net.connection.ReconnectHandler; import dev.objz.commandbridge.backends.net.connection.ResourcePool; @@ -162,7 +162,7 @@ public synchronized void close() throws Exception { @Override public ClientStatus status() { - return stateRef.get().toClientStatus(); + return ClientStatus.fromConnectionState(stateRef.get()); } @Override diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ClientStatus.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ClientStatus.java index 6b82cb26..083670cd 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ClientStatus.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ClientStatus.java @@ -1,8 +1,28 @@ package dev.objz.commandbridge.backends.net.connection; +import dev.objz.commandbridge.api.platform.ConnectionState; + public enum ClientStatus { DISCONNECTED, CONNECTED, AUTH_OK, - AUTH_FAILED + AUTH_FAILED; + + public ConnectionState toConnectionState() { + return switch (this) { + case DISCONNECTED -> ConnectionState.DISCONNECTED; + case CONNECTED -> ConnectionState.CONNECTED; + case AUTH_OK -> ConnectionState.AUTHENTICATED; + case AUTH_FAILED -> ConnectionState.AUTH_FAILED; + }; + } + + public static ClientStatus fromConnectionState(ConnectionState state) { + return switch (state) { + case DISCONNECTED, CONNECTING, RECONNECTING -> DISCONNECTED; + case CONNECTED -> CONNECTED; + case AUTHENTICATED -> AUTH_OK; + case AUTH_FAILED -> AUTH_FAILED; + }; + } } diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionHandler.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionHandler.java index 9f1b645b..6ec6fad2 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionHandler.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionHandler.java @@ -1,5 +1,6 @@ package dev.objz.commandbridge.backends.net.connection; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.config.model.BackendsConfig; import dev.objz.commandbridge.config.model.TlsMode; import dev.objz.commandbridge.logging.Log; diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionState.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionState.java deleted file mode 100644 index 9a43d093..00000000 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/connection/ConnectionState.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.objz.commandbridge.backends.net.connection; - -public enum ConnectionState { - - DISCONNECTED, - - CONNECTING, - - CONNECTED, - - AUTHENTICATED, - - RECONNECTING, - - AUTH_FAILED; - - public boolean isActive() { - return this == CONNECTED || this == AUTHENTICATED; - } - - public boolean canSend() { - return this == AUTHENTICATED; - } - - public boolean isTerminal() { - return this == AUTH_FAILED; - } - - public static ConnectionState fromClientStatus(ClientStatus status) { - return switch (status) { - case DISCONNECTED -> DISCONNECTED; - case CONNECTED -> CONNECTED; - case AUTH_OK -> AUTHENTICATED; - case AUTH_FAILED -> AUTH_FAILED; - }; - } - - public ClientStatus toClientStatus() { - return switch (this) { - case DISCONNECTED, CONNECTING, RECONNECTING -> ClientStatus.DISCONNECTED; - case CONNECTED -> ClientStatus.CONNECTED; - case AUTHENTICATED -> ClientStatus.AUTH_OK; - case AUTH_FAILED -> ClientStatus.AUTH_FAILED; - }; - } -} diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/in/ExecuteCommandHandler.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/in/ExecuteCommandHandler.java index 94b41fa7..0b851627 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/in/ExecuteCommandHandler.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/in/ExecuteCommandHandler.java @@ -8,7 +8,7 @@ import dev.objz.commandbridge.net.payloads.cmd.ExecuteCommandResult; import dev.objz.commandbridge.net.proto.Envelope; import dev.objz.commandbridge.net.proto.MessageType; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import java.util.HashSet; import java.util.Objects; diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/AuthHandler.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/AuthHandler.java index 68c68476..30507e9d 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/AuthHandler.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/AuthHandler.java @@ -1,7 +1,7 @@ package dev.objz.commandbridge.backends.net.routing; import dev.objz.commandbridge.backends.net.connection.ClientStatus; -import dev.objz.commandbridge.backends.net.connection.ConnectionState; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.config.model.BackendsConfig; import dev.objz.commandbridge.logging.Log; import dev.objz.commandbridge.net.OutNode; @@ -31,7 +31,7 @@ public void onAuthenticated(Runnable callback) { public boolean authenticate() { Consumer statusUpdater = status -> { - ConnectionState newState = ConnectionState.fromClientStatus(status); + ConnectionState newState = status.toConnectionState(); stateRef.set(newState); if (newState == ConnectionState.AUTHENTICATED) { diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/MessageRouter.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/MessageRouter.java index 0dcebd4c..df6d13be 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/MessageRouter.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/MessageRouter.java @@ -1,6 +1,6 @@ package dev.objz.commandbridge.backends.net.routing; -import dev.objz.commandbridge.backends.net.connection.ConnectionState; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.backends.net.in.PingHandler; import dev.objz.commandbridge.backends.net.out.AuthRequest; import dev.objz.commandbridge.backends.net.out.InvokedCommandEvent; diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/RedisMessageRouter.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/RedisMessageRouter.java index 31f353d7..15c072f6 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/RedisMessageRouter.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/routing/RedisMessageRouter.java @@ -1,6 +1,6 @@ package dev.objz.commandbridge.backends.net.routing; -import dev.objz.commandbridge.backends.net.connection.ConnectionState; +import dev.objz.commandbridge.api.platform.ConnectionState; import dev.objz.commandbridge.backends.net.in.PingHandler; import dev.objz.commandbridge.backends.net.out.AuthRequest; import dev.objz.commandbridge.backends.net.out.InvokedCommandEvent; diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/platform/PlatformExecutor.java b/backends/src/main/java/dev/objz/commandbridge/backends/platform/PlatformExecutor.java index 6bc779dc..23b10b3c 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/platform/PlatformExecutor.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/platform/PlatformExecutor.java @@ -2,7 +2,7 @@ import dev.objz.commandbridge.logging.Log; import dev.objz.commandbridge.net.payloads.cmd.ExecuteCommand; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import dev.objz.commandbridge.backends.platform.cmd.CommandExecutor; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; diff --git a/backends/velocity/src/main/java/dev/objz/commandbridge/velocity/VelocityExecutor.java b/backends/velocity/src/main/java/dev/objz/commandbridge/velocity/VelocityExecutor.java index 0f0aefe4..b91de95a 100644 --- a/backends/velocity/src/main/java/dev/objz/commandbridge/velocity/VelocityExecutor.java +++ b/backends/velocity/src/main/java/dev/objz/commandbridge/velocity/VelocityExecutor.java @@ -7,7 +7,7 @@ import dev.objz.commandbridge.backends.platform.cmd.CommandExecutor; import dev.objz.commandbridge.logging.Log; import dev.objz.commandbridge.net.payloads.cmd.ExecuteCommand; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import net.kyori.adventure.text.Component; import java.util.Objects; diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 928a80c4..67053603 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -15,6 +15,7 @@ repositories { } dependencies { + api(project(":api")) api("com.fasterxml.jackson.core:jackson-databind:2.18.2") api("com.fasterxml.jackson.core:jackson-annotations:2.18") api("com.fasterxml.jackson.core:jackson-core:2.18.2") diff --git a/core/src/main/java/dev/objz/commandbridge/net/payloads/cmd/ExecuteCommand.java b/core/src/main/java/dev/objz/commandbridge/net/payloads/cmd/ExecuteCommand.java index 0306f0b5..8fb0ab30 100644 --- a/core/src/main/java/dev/objz/commandbridge/net/payloads/cmd/ExecuteCommand.java +++ b/core/src/main/java/dev/objz/commandbridge/net/payloads/cmd/ExecuteCommand.java @@ -2,7 +2,7 @@ import java.util.Set; import java.util.UUID; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; public record ExecuteCommand( String command, diff --git a/core/src/main/java/dev/objz/commandbridge/scripting/model/Defaults.java b/core/src/main/java/dev/objz/commandbridge/scripting/model/Defaults.java index 927d1f05..d4051fb9 100644 --- a/core/src/main/java/dev/objz/commandbridge/scripting/model/Defaults.java +++ b/core/src/main/java/dev/objz/commandbridge/scripting/model/Defaults.java @@ -7,7 +7,7 @@ import dev.objz.commandbridge.scripting.anno.Min; import dev.objz.commandbridge.scripting.anno.Model; import dev.objz.commandbridge.scripting.anno.YmlKey; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import dev.objz.commandbridge.scripting.model.records.Server; import dev.objz.commandbridge.scripting.model.records.mapping.IdMapping; diff --git a/core/src/main/java/dev/objz/commandbridge/scripting/model/enums/RunAs.java b/core/src/main/java/dev/objz/commandbridge/scripting/model/enums/RunAs.java deleted file mode 100644 index cd2b29ec..00000000 --- a/core/src/main/java/dev/objz/commandbridge/scripting/model/enums/RunAs.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.objz.commandbridge.scripting.model.enums; - -public enum RunAs { - CONSOLE, - PLAYER, - OPERATOR -} diff --git a/core/src/main/java/dev/objz/commandbridge/scripting/model/records/mapping/CmdMapping.java b/core/src/main/java/dev/objz/commandbridge/scripting/model/records/mapping/CmdMapping.java index ba297c31..8497e728 100644 --- a/core/src/main/java/dev/objz/commandbridge/scripting/model/records/mapping/CmdMapping.java +++ b/core/src/main/java/dev/objz/commandbridge/scripting/model/records/mapping/CmdMapping.java @@ -9,7 +9,7 @@ import dev.objz.commandbridge.scripting.anno.Required; import dev.objz.commandbridge.scripting.anno.Resolvable; import dev.objz.commandbridge.scripting.anno.YmlKey; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import dev.objz.commandbridge.scripting.model.records.Server; @Model("commands") diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/CommandDispatcher.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/CommandDispatcher.java index c1eb0fec..fbfbdb1d 100644 --- a/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/CommandDispatcher.java +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/CommandDispatcher.java @@ -8,7 +8,7 @@ import dev.objz.commandbridge.net.proto.MessageType; import dev.objz.commandbridge.scripting.model.Script; import dev.objz.commandbridge.scripting.model.enums.Location; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import dev.objz.commandbridge.scripting.model.records.mapping.CmdMapping; import dev.objz.commandbridge.scripting.model.records.mapping.IdMapping; import dev.objz.commandbridge.velocity.dispatch.exec.LocalDispatcher; diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/exec/LocalDispatcher.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/exec/LocalDispatcher.java index 822dc73c..603b9a75 100644 --- a/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/exec/LocalDispatcher.java +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/dispatch/exec/LocalDispatcher.java @@ -4,7 +4,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import dev.objz.commandbridge.logging.Log; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import java.util.Objects; import java.util.UUID; diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/net/out/ctx/ExecuteCommandContext.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/net/out/ctx/ExecuteCommandContext.java index d0e22eea..e7a9207f 100644 --- a/velocity/src/main/java/dev/objz/commandbridge/velocity/net/out/ctx/ExecuteCommandContext.java +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/net/out/ctx/ExecuteCommandContext.java @@ -1,6 +1,6 @@ package dev.objz.commandbridge.velocity.net.out.ctx; -import dev.objz.commandbridge.scripting.model.enums.RunAs; +import dev.objz.commandbridge.api.channel.command.RunAs; import dev.objz.commandbridge.velocity.net.session.ClientSession; import java.util.Objects; From ac5da9f56c308e3907109ffff1505a3bf41a3224 Mon Sep 17 00:00:00 2001 From: objz Date: Sun, 22 Mar 2026 00:50:59 +0100 Subject: [PATCH 3/4] feat: api implementation --- .../commandbridge/api/CommandBridgeAPI.java | 3 +- .../api/CommandBridgeProvider.java | 4 +- .../api/channel/MessageChannel.java | 4 +- .../api/channel/command/CommandChannel.java | 13 +- .../api/BackendCommandBridgeImpl.java | 309 +++++++++++++ .../api/BackendPluginMessageHandler.java | 41 ++ .../backends/net/client/RedisClient.java | 5 + .../backends/net/client/WsClient.java | 5 + .../net/channel/CommandMessageChannel.java | 15 + .../net/channel/PluginMessageChannel.java | 88 ++++ .../net/payloads/PluginMessage.java | 6 + .../commandbridge/net/proto/MessageType.java | 2 + .../dev/objz/commandbridge/velocity/Main.java | 26 +- .../api/VelocityCommandBridgeImpl.java | 410 ++++++++++++++++++ .../api/VelocityPluginMessageHandler.java | 27 ++ 15 files changed, 943 insertions(+), 15 deletions(-) create mode 100644 backends/src/main/java/dev/objz/commandbridge/backends/api/BackendCommandBridgeImpl.java create mode 100644 backends/src/main/java/dev/objz/commandbridge/backends/api/BackendPluginMessageHandler.java create mode 100644 core/src/main/java/dev/objz/commandbridge/net/channel/CommandMessageChannel.java create mode 100644 core/src/main/java/dev/objz/commandbridge/net/channel/PluginMessageChannel.java create mode 100644 core/src/main/java/dev/objz/commandbridge/net/payloads/PluginMessage.java create mode 100644 velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityCommandBridgeImpl.java create mode 100644 velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityPluginMessageHandler.java diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java index 7eff70a6..7908daca 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java @@ -11,13 +11,14 @@ import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public interface CommandBridgeAPI { > C channel(ChannelType type); -

void broadcast(MessageChannel

channel, P payload); +

CompletableFuture broadcast(MessageChannel

channel, P payload); Platform.ServerTarget server(); diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java index 1a79898a..2cfa313e 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java @@ -25,7 +25,7 @@ public static T get(Class type) { return type.cast(api); } - static void register(CommandBridgeAPI impl) { + public static void register(CommandBridgeAPI impl) { Objects.requireNonNull(impl); if (instance != null) { throw new IllegalStateException("CommandBridge already registered"); @@ -33,7 +33,7 @@ static void register(CommandBridgeAPI impl) { instance = impl; } - static void unregister() { + public static void unregister() { instance = null; } } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java index a42c840a..e5995ebe 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java @@ -9,9 +9,7 @@ public interface MessageChannel

{ - void send(Platform.ServerTarget target, P payload); - - CompletableFuture sendAsync(Platform.ServerTarget target, P payload); + CompletableFuture send(Platform.ServerTarget target, P payload); CompletableFuture

request(Platform.ServerTarget target, P payload); diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java index 96bcac57..07bf28be 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java @@ -4,18 +4,19 @@ import dev.objz.commandbridge.api.platform.Platform; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public interface CommandChannel extends MessageChannel { - default void console(Platform.ServerTarget target, String command) { - send(target, CommandPayload.console(command)); + default CompletableFuture console(Platform.ServerTarget target, String command) { + return send(target, CommandPayload.console(command)); } - default void player(Platform.ServerTarget target, String command, UUID player) { - send(target, CommandPayload.player(command, player)); + default CompletableFuture player(Platform.ServerTarget target, String command, UUID player) { + return send(target, CommandPayload.player(command, player)); } - default void operator(Platform.ServerTarget target, String command, UUID player) { - send(target, CommandPayload.operator(command, player)); + default CompletableFuture operator(Platform.ServerTarget target, String command, UUID player) { + return send(target, CommandPayload.operator(command, player)); } } diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendCommandBridgeImpl.java b/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendCommandBridgeImpl.java new file mode 100644 index 00000000..724f9d9f --- /dev/null +++ b/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendCommandBridgeImpl.java @@ -0,0 +1,309 @@ +package dev.objz.commandbridge.backends.api; + +import dev.objz.commandbridge.api.CommandBridgeAPI; +import dev.objz.commandbridge.api.CommandBridgeProvider; +import dev.objz.commandbridge.api.channel.ChannelPayload; +import dev.objz.commandbridge.api.channel.ChannelType; +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.channel.command.CommandChannelType; +import dev.objz.commandbridge.api.message.MessageContext; +import dev.objz.commandbridge.api.message.MessageListener; +import dev.objz.commandbridge.api.message.ServerEventListener; +import dev.objz.commandbridge.api.message.Subscription; +import dev.objz.commandbridge.api.platform.ConnectionState; +import dev.objz.commandbridge.api.platform.Platform; +import dev.objz.commandbridge.api.platform.PlayerLocator; +import dev.objz.commandbridge.backends.net.client.BackendClient; +import dev.objz.commandbridge.logging.Log; +import dev.objz.commandbridge.net.channel.CommandMessageChannel; +import dev.objz.commandbridge.net.channel.PluginMessageChannel; +import dev.objz.commandbridge.net.payloads.PluginMessage; +import dev.objz.commandbridge.net.proto.Envelope; +import dev.objz.commandbridge.net.proto.MessageType; + +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public final class BackendCommandBridgeImpl implements CommandBridgeAPI { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(15); + + private final BackendClient client; + private final ConcurrentHashMap, MessageChannel> channels = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> channelTypesByName = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> listenersByType = new ConcurrentHashMap<>(); + private final CopyOnWriteArraySet> stateListeners = new CopyOnWriteArraySet<>(); + private volatile ScheduledExecutorService statePoller; + private final AtomicReference lastState = new AtomicReference<>(ConnectionState.DISCONNECTED); + private final AtomicBoolean polling = new AtomicBoolean(false); + private final AtomicBoolean providerRegistered = new AtomicBoolean(false); + + public BackendCommandBridgeImpl(BackendClient client) { + this.client = Objects.requireNonNull(client); + } + + public void bootstrap() { + client.inboundRouter().register(MessageType.PLUGIN_MESSAGE, new BackendPluginMessageHandler(this, false)); + client.inboundRouter().register(MessageType.PLUGIN_MESSAGE_RESPONSE, + new BackendPluginMessageHandler(this, true)); + if (providerRegistered.compareAndSet(false, true)) { + CommandBridgeProvider.register(this); + } + } + + public void shutdown() { + if (providerRegistered.compareAndSet(true, false)) { + CommandBridgeProvider.unregister(); + } + if (statePoller != null) { + statePoller.shutdownNow(); + } + } + + @Override + public > C channel(ChannelType type) { + Objects.requireNonNull(type); + channelTypesByName.putIfAbsent(type.getClass().getName(), type); + MessageChannel channel = channels.computeIfAbsent(type.getClass(), ignored -> createChannel(type)); + return typeCast(channel); + } + + @Override + public

CompletableFuture broadcast(MessageChannel

channel, P payload) { + Objects.requireNonNull(channel); + Objects.requireNonNull(payload); + return channel.send(Platform.BACKEND.target("*"), payload); + } + + @Override + public Platform.ServerTarget server() { + return Platform.BACKEND.target(localServerId()); + } + + @Override + public ConnectionState connectionState() { + return client.status().toConnectionState(); + } + + @Override + public Optional> connectedServers() { + return Optional.empty(); + } + + @Override + public Optional playerLocator() { + return Optional.empty(); + } + + @Override + public Subscription onServerConnected(ServerEventListener listener) { + throw new UnsupportedOperationException("Server events are only available on the proxy"); + } + + @Override + public Subscription onServerDisconnected(ServerEventListener listener) { + throw new UnsupportedOperationException("Server events are only available on the proxy"); + } + + @Override + public Subscription onConnectionStateChanged(Consumer listener) { + Objects.requireNonNull(listener); + stateListeners.add(listener); + listener.accept(connectionState()); + startStatePolling(); + return () -> stateListeners.remove(listener); + } + + public PluginMessage handlePluginMessageRequest(Envelope env) { + String to = env.to(); + String localId = localServerId(); + if (to != null && !to.equals(localId) && !"*".equals(to)) { + return null; + } + + PluginMessage message = readPluginMessage(env); + if (message == null) { + return null; + } + + dispatchLocal(env, message); + if ("*".equals(to)) { + return null; + } + return message; + } + + public void handlePluginMessageResponse(Envelope env) { + String to = env.to(); + if (to != null && !to.equals(localServerId()) && !"*".equals(to)) { + Log.debug("Dropping plugin response for mismatched target {}", to); + } + } + + private > MessageChannel createChannel( + ChannelType type) { + if (type instanceof CommandChannelType commandType) { + return new CommandMessageChannel(commandType, + this::sendPluginMessage, + this::requestPluginMessage, + listener -> registerListener(commandType, listener)); + } + + return new PluginMessageChannel<>(type, + this::sendPluginMessage, + this::requestPluginMessage, + listener -> registerListener(type, listener)); + } + + private

Subscription registerListener(ChannelType> type, + MessageListener

listener) { + String key = type.getClass().getName(); + channelTypesByName.putIfAbsent(key, type); + CopyOnWriteArrayList> listeners = listenersByType.computeIfAbsent(key, + ignored -> new CopyOnWriteArrayList<>()); + listeners.add(listener); + return () -> listeners.remove(listener); + } + + private CompletableFuture sendPluginMessage(Platform.ServerTarget target, PluginMessage payload) { + Envelope env = Envelope.make(MessageType.PLUGIN_MESSAGE, localServerId(), target.id(), + Envelope.MAPPER.valueToTree(payload)); + return client.send(env).dispatch(); + } + + private CompletableFuture requestPluginMessage(Platform.ServerTarget target, + PluginMessage payload, + Duration timeout) { + Duration effectiveTimeout = timeout != null ? timeout : DEFAULT_TIMEOUT; + Envelope env = Envelope.make(MessageType.PLUGIN_MESSAGE, localServerId(), target.id(), + Envelope.MAPPER.valueToTree(payload)); + return client.send(env) + .expect(MessageType.PLUGIN_MESSAGE_RESPONSE) + .timeout(effectiveTimeout) + .await() + .thenApply(this::requirePluginMessage); + } + + private void dispatchLocal(Envelope env, PluginMessage message) { + ChannelType channelType = channelTypesByName.get(message.channelType()); + if (channelType == null) { + return; + } + + Object payload; + try { + payload = Envelope.MAPPER.treeToValue(message.data(), channelType.type()); + } catch (Exception e) { + Log.warn("Failed to decode plugin payload for {}: {}", message.channelType(), e.getMessage()); + return; + } + + CopyOnWriteArrayList> listeners = listenersByType.get(message.channelType()); + if (listeners == null || listeners.isEmpty()) { + return; + } + + if (env.from() == null || env.from().isBlank()) { + Log.warn("Received plugin message with missing source server ID"); + return; + } + Platform.ServerTarget source = Platform.BACKEND.target(env.from()); + MessageContext context = contextCast(new MessageContext<>( + typeCast(channelType), + source, + env.ts())); + + for (MessageListener raw : listeners) { + try { + listenerCast(raw).accept(context, payloadCast(payload)); + } catch (Exception e) { + Log.warn("Plugin message listener failed for {}: {}", message.channelType(), e.getMessage()); + } + } + } + + private PluginMessage readPluginMessage(Envelope env) { + try { + return Envelope.MAPPER.treeToValue(env.payload(), PluginMessage.class); + } catch (Exception e) { + Log.warn("Failed to decode plugin message: {}", e.getMessage()); + return null; + } + } + + private PluginMessage requirePluginMessage(Envelope env) { + try { + return Envelope.MAPPER.treeToValue(env.payload(), PluginMessage.class); + } catch (Exception e) { + throw new CompletionException(e); + } + } + + private String localServerId() { + String id = client.serverId(); + return (id == null || id.isBlank()) ? "backend" : id; + } + + private void startStatePolling() { + if (!polling.compareAndSet(false, true)) { + return; + } + + statePoller = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = new Thread(runnable, "commandbridge-api-state"); + thread.setDaemon(true); + return thread; + }); + + lastState.set(connectionState()); + statePoller.scheduleAtFixedRate(() -> { + try { + ConnectionState current = connectionState(); + ConnectionState previous = lastState.getAndSet(current); + if (current != previous) { + for (Consumer listener : stateListeners) { + try { + listener.accept(current); + } catch (Exception e) { + Log.warn("Failed to run state-change listener: {}", e.getMessage()); + } + } + } + } catch (Exception e) { + Log.warn("State polling failed: {}", e.getMessage()); + } + }, 500L, 500L, TimeUnit.MILLISECONDS); + } + + @SuppressWarnings("unchecked") + private static > C typeCast(Object value) { + return (C) value; + } + + @SuppressWarnings("unchecked") + private static MessageContext contextCast(MessageContext context) { + return (MessageContext) context; + } + + @SuppressWarnings("unchecked") + private static MessageListener listenerCast(MessageListener listener) { + return (MessageListener) listener; + } + + private static ChannelPayload payloadCast(Object value) { + return (ChannelPayload) value; + } +} diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendPluginMessageHandler.java b/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendPluginMessageHandler.java new file mode 100644 index 00000000..4b58a861 --- /dev/null +++ b/backends/src/main/java/dev/objz/commandbridge/backends/api/BackendPluginMessageHandler.java @@ -0,0 +1,41 @@ +package dev.objz.commandbridge.backends.api; + +import dev.objz.commandbridge.logging.Log; +import dev.objz.commandbridge.net.Endpoint; +import dev.objz.commandbridge.net.InboundHandler; +import dev.objz.commandbridge.net.payloads.PluginMessage; +import dev.objz.commandbridge.net.proto.Envelope; +import dev.objz.commandbridge.net.proto.MessageType; + +import java.util.Objects; + +public final class BackendPluginMessageHandler extends InboundHandler { + + private final BackendCommandBridgeImpl api; + private final boolean response; + + public BackendPluginMessageHandler(BackendCommandBridgeImpl api, boolean response) { + this.api = Objects.requireNonNull(api); + this.response = response; + } + + @Override + public void accept(Endpoint endpoint, Envelope env) { + if (response) { + api.handlePluginMessageResponse(env); + return; + } + + PluginMessage replyPayload = api.handlePluginMessageRequest(env); + if (replyPayload == null || !replyPayload.expectsResponse()) { + return; + } + + reply(endpoint, env, MessageType.PLUGIN_MESSAGE_RESPONSE, replyPayload) + .dispatch() + .exceptionally(ex -> { + Log.warn("Failed to send plugin message response: {}", ex.getMessage()); + return null; + }); + } +} diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java index ffea4ff7..cb5bcf7e 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/RedisClient.java @@ -2,6 +2,7 @@ import dev.objz.commandbridge.backends.net.connection.ClientStatus; import dev.objz.commandbridge.api.platform.ConnectionState; +import dev.objz.commandbridge.backends.api.BackendCommandBridgeImpl; import dev.objz.commandbridge.backends.net.connection.ReconnectHandler; import dev.objz.commandbridge.backends.net.routing.AuthHandler; import dev.objz.commandbridge.backends.net.routing.RedisMessageRouter; @@ -43,6 +44,7 @@ public final class RedisClient implements BackendClient { private final OutNode outNode = new OutNode(); private final ResponseAwaiter awaiter = new ResponseAwaiter(); private final AtomicReference stateRef = new AtomicReference<>(ConnectionState.DISCONNECTED); + private final BackendCommandBridgeImpl api; private volatile JedisPool pool; private volatile JedisPubSub subscriber; @@ -69,6 +71,7 @@ public RedisClient(BackendsConfig cfg, Path dataDir, PlatformAdapter adapter) location); outNode.setClientId(cfg.clientId()); + this.api = new BackendCommandBridgeImpl(this); } @Override @@ -89,6 +92,7 @@ public synchronized void start() throws Exception { proxyEndpoint = new RedisEndpoint("proxy", this::publishToProxy, this::isConnected); messageRouter.setupEndpoint(proxyEndpoint); + api.bootstrap(); startSubscriber(); stateRef.set(ConnectionState.CONNECTED); @@ -112,6 +116,7 @@ public synchronized void start() throws Exception { public synchronized void reconnect() throws Exception { Log.warn("Manual reconnection initiated"); reconnectHandler.shutdown(); + api.shutdown(); messageRouter.clearTap(); stopInternal(); stateRef.set(ConnectionState.DISCONNECTED); diff --git a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java index d3686d76..eaf70467 100644 --- a/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java +++ b/backends/src/main/java/dev/objz/commandbridge/backends/net/client/WsClient.java @@ -2,6 +2,7 @@ import dev.objz.commandbridge.backends.net.connection.ConnectionHandler; import dev.objz.commandbridge.api.platform.ConnectionState; +import dev.objz.commandbridge.backends.api.BackendCommandBridgeImpl; import dev.objz.commandbridge.backends.net.connection.ClientStatus; import dev.objz.commandbridge.backends.net.connection.ReconnectHandler; import dev.objz.commandbridge.backends.net.connection.ResourcePool; @@ -38,6 +39,7 @@ public final class WsClient implements BackendClient { private final InNode inNode = new InNode(); private final OutNode outNode = new OutNode(); private final ResponseAwaiter awaiter = new ResponseAwaiter(); + private final BackendCommandBridgeImpl api; private final AtomicReference stateRef = new AtomicReference<>(ConnectionState.DISCONNECTED); private Location location = Location.BACKEND; @@ -62,6 +64,7 @@ public WsClient(BackendsConfig cfg, Path dataDir, PlatformAdapter adapter) { location); outNode.setClientId(cfg.clientId()); + this.api = new BackendCommandBridgeImpl(this); } @Override @@ -91,6 +94,7 @@ public synchronized void start() throws Exception { stateRef.set(ConnectionState.CONNECTED); messageRouter.setupChannel(channel); + api.bootstrap(); authHandler.authenticate(); @@ -144,6 +148,7 @@ public synchronized void close() throws Exception { Log.debug("Closing WsClient"); reconnectHandler.shutdown(); + api.shutdown(); try { messageRouter.clearTap(); diff --git a/core/src/main/java/dev/objz/commandbridge/net/channel/CommandMessageChannel.java b/core/src/main/java/dev/objz/commandbridge/net/channel/CommandMessageChannel.java new file mode 100644 index 00000000..fe6d92f5 --- /dev/null +++ b/core/src/main/java/dev/objz/commandbridge/net/channel/CommandMessageChannel.java @@ -0,0 +1,15 @@ +package dev.objz.commandbridge.net.channel; + +import dev.objz.commandbridge.api.channel.command.CommandChannel; +import dev.objz.commandbridge.api.channel.command.CommandChannelType; +import dev.objz.commandbridge.api.channel.command.CommandPayload; + +public final class CommandMessageChannel extends PluginMessageChannel implements CommandChannel { + + public CommandMessageChannel(CommandChannelType type, + SendTransport sendTransport, + RequestTransport requestTransport, + ListenerRegistrar listenerRegistrar) { + super(type, sendTransport, requestTransport, listenerRegistrar); + } +} diff --git a/core/src/main/java/dev/objz/commandbridge/net/channel/PluginMessageChannel.java b/core/src/main/java/dev/objz/commandbridge/net/channel/PluginMessageChannel.java new file mode 100644 index 00000000..1c8da935 --- /dev/null +++ b/core/src/main/java/dev/objz/commandbridge/net/channel/PluginMessageChannel.java @@ -0,0 +1,88 @@ +package dev.objz.commandbridge.net.channel; + +import dev.objz.commandbridge.api.channel.ChannelPayload; +import dev.objz.commandbridge.api.channel.ChannelType; +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.message.MessageListener; +import dev.objz.commandbridge.api.message.Subscription; +import dev.objz.commandbridge.api.platform.Platform; +import dev.objz.commandbridge.net.payloads.PluginMessage; +import dev.objz.commandbridge.net.proto.Envelope; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class PluginMessageChannel

implements MessageChannel

{ + + @FunctionalInterface + public interface SendTransport { + CompletableFuture send(Platform.ServerTarget target, PluginMessage payload); + } + + @FunctionalInterface + public interface RequestTransport { + CompletableFuture request(Platform.ServerTarget target, PluginMessage payload, Duration timeout); + } + + @FunctionalInterface + public interface ListenerRegistrar { + Subscription listen(MessageListener listener); + } + + private final ChannelType> channelType; + private final SendTransport sendTransport; + private final RequestTransport requestTransport; + private final ListenerRegistrar

listenerRegistrar; + + public PluginMessageChannel(ChannelType> channelType, + SendTransport sendTransport, + RequestTransport requestTransport, + ListenerRegistrar

listenerRegistrar) { + this.channelType = Objects.requireNonNull(channelType); + this.sendTransport = Objects.requireNonNull(sendTransport); + this.requestTransport = Objects.requireNonNull(requestTransport); + this.listenerRegistrar = Objects.requireNonNull(listenerRegistrar); + } + + @Override + public CompletableFuture send(Platform.ServerTarget target, P payload) { + Objects.requireNonNull(target); + Objects.requireNonNull(payload); + return sendTransport.send(target, toPluginMessage(payload, false)); + } + + @Override + public CompletableFuture

request(Platform.ServerTarget target, P payload) { + return request(target, payload, Duration.ofSeconds(15)); + } + + @Override + public CompletableFuture

request(Platform.ServerTarget target, P payload, Duration timeout) { + Objects.requireNonNull(target); + Objects.requireNonNull(payload); + Objects.requireNonNull(timeout); + return requestTransport.request(target, toPluginMessage(payload, true), timeout) + .thenApply(this::toPayload); + } + + @Override + public Subscription listen(MessageListener

listener) { + Objects.requireNonNull(listener); + return listenerRegistrar.listen(listener); + } + + private PluginMessage toPluginMessage(P payload, boolean expectsResponse) { + return new PluginMessage(channelType.getClass().getName(), + Envelope.MAPPER.valueToTree(payload), expectsResponse); + } + + private P toPayload(PluginMessage response) { + try { + return Envelope.MAPPER.treeToValue(response.data(), channelType.type()); + } catch (Exception e) { + throw new CompletionException(e); + } + } +} diff --git a/core/src/main/java/dev/objz/commandbridge/net/payloads/PluginMessage.java b/core/src/main/java/dev/objz/commandbridge/net/payloads/PluginMessage.java new file mode 100644 index 00000000..d63b5fdb --- /dev/null +++ b/core/src/main/java/dev/objz/commandbridge/net/payloads/PluginMessage.java @@ -0,0 +1,6 @@ +package dev.objz.commandbridge.net.payloads; + +import com.fasterxml.jackson.databind.JsonNode; + +public record PluginMessage(String channelType, JsonNode data, boolean expectsResponse) { +} diff --git a/core/src/main/java/dev/objz/commandbridge/net/proto/MessageType.java b/core/src/main/java/dev/objz/commandbridge/net/proto/MessageType.java index 002d4aa1..946dc053 100644 --- a/core/src/main/java/dev/objz/commandbridge/net/proto/MessageType.java +++ b/core/src/main/java/dev/objz/commandbridge/net/proto/MessageType.java @@ -12,6 +12,8 @@ public enum MessageType { PLAYER_JOIN, PLAYER_LEAVE, PING, PONG, + PLUGIN_MESSAGE, + PLUGIN_MESSAGE_RESPONSE, RESOLVE_UUID, RESOLVE_UUID_RESPONSE, DUMP_REQUEST, DUMP_RESPONSE } diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/Main.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/Main.java index fc997f90..0ebcd1ce 100644 --- a/velocity/src/main/java/dev/objz/commandbridge/velocity/Main.java +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/Main.java @@ -10,6 +10,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; +import dev.objz.commandbridge.api.CommandBridgeProvider; import dev.objz.commandbridge.config.ConfigManager; import dev.objz.commandbridge.config.model.EndpointType; import dev.objz.commandbridge.config.model.VelocityConfig; @@ -29,6 +30,8 @@ import dev.objz.commandbridge.util.ModrinthAPI; import dev.objz.commandbridge.velocity.cli.CBCommand; import dev.objz.commandbridge.velocity.dispatch.CommandEntry; +import dev.objz.commandbridge.velocity.api.VelocityCommandBridgeImpl; +import dev.objz.commandbridge.velocity.api.VelocityPluginMessageHandler; import dev.objz.commandbridge.velocity.cmd.bridge.framework.ArgumentBridge; import dev.objz.commandbridge.velocity.cmd.bridge.packetevents.PacketEventsArgumentBridge; import dev.objz.commandbridge.velocity.cmd.bridge.types.OfflinePlayerArgumentType; @@ -88,6 +91,7 @@ public final class Main { private ScriptManager scriptManager; private CommandEntry commandEntry; private dev.objz.commandbridge.lifecycle.BackendLifecycle backendBootstrap; + private VelocityCommandBridgeImpl api; private ArgumentBridge argumentBridge; private PlatformFeatures platformFeatures; private boolean legacyDetected; @@ -151,7 +155,12 @@ public void onProxyInitialization(ProxyInitializeEvent e) { sessions = new SessionHub(); playerTracker = new PlayerTracker(); - sessions.onRemove(session -> playerTracker.remove(session.id())); + sessions.onRemove(session -> { + playerTracker.remove(session.id()); + if (api != null) { + api.onServerDisconnected(session); + } + }); inNode = new InNode(); outNode = new OutNode(); outNode.setServerId(cfg.serverId()); @@ -197,9 +206,9 @@ public void onProxyInitialization(ProxyInitializeEvent e) { registrations.load(scriptManager.enabled()); - installRoutes(); + api = new VelocityCommandBridgeImpl(sessions, playerTracker, cfg.serverId(), endpointServer); - authHandler.onAuthenticated(registrations::onClientAuthenticated); + installRoutes(); command = new CBCommand( configManager, @@ -210,6 +219,13 @@ public void onProxyInitialization(ProxyInitializeEvent e) { cfg, dataDir); command.register(); + + CommandBridgeProvider.register(api); + authHandler.onAuthenticated(session -> { + registrations.onClientAuthenticated(session); + api.onServerConnected(session); + }); + checkForUpdate(); } @@ -228,6 +244,8 @@ public void onProxyShutdown(ProxyShutdownEvent e) { if (endpointServer != null) { endpointServer.stop(); } + + CommandBridgeProvider.unregister(); } private void installRoutes() { @@ -242,6 +260,8 @@ private void installRoutes() { var playerUpdateHandler = new PlayerUpdateHandler(sessions, playerTracker); inNode.register(MessageType.PLAYER_JOIN, playerUpdateHandler); inNode.register(MessageType.PLAYER_LEAVE, playerUpdateHandler); + inNode.register(MessageType.PLUGIN_MESSAGE, new VelocityPluginMessageHandler(api, false)); + inNode.register(MessageType.PLUGIN_MESSAGE_RESPONSE, new VelocityPluginMessageHandler(api, true)); outNode.setEndpointSendFactory((endpoint, env) -> endpointServer.send(endpoint, env)); outNode.register(MessageType.REGISTER_COMMANDS, new RegistrationRequest()); diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityCommandBridgeImpl.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityCommandBridgeImpl.java new file mode 100644 index 00000000..91efb77a --- /dev/null +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityCommandBridgeImpl.java @@ -0,0 +1,410 @@ +package dev.objz.commandbridge.velocity.api; + +import dev.objz.commandbridge.api.CommandBridgeAPI; +import dev.objz.commandbridge.api.channel.ChannelPayload; +import dev.objz.commandbridge.api.channel.ChannelType; +import dev.objz.commandbridge.api.channel.MessageChannel; +import dev.objz.commandbridge.api.channel.command.CommandChannelType; +import dev.objz.commandbridge.api.message.MessageContext; +import dev.objz.commandbridge.api.message.MessageListener; +import dev.objz.commandbridge.api.message.ServerEventListener; +import dev.objz.commandbridge.api.message.Subscription; +import dev.objz.commandbridge.api.platform.ConnectionState; +import dev.objz.commandbridge.api.platform.Platform; +import dev.objz.commandbridge.api.platform.PlayerLocator; +import dev.objz.commandbridge.logging.Log; +import dev.objz.commandbridge.net.Endpoint; +import dev.objz.commandbridge.net.channel.CommandMessageChannel; +import dev.objz.commandbridge.net.channel.PluginMessageChannel; +import dev.objz.commandbridge.net.payloads.PluginMessage; +import dev.objz.commandbridge.net.proto.Envelope; +import dev.objz.commandbridge.net.proto.MessageType; +import dev.objz.commandbridge.scripting.model.enums.Location; +import dev.objz.commandbridge.security.AuthStatus; +import dev.objz.commandbridge.velocity.net.EndpointServer; +import dev.objz.commandbridge.velocity.net.session.ClientSession; +import dev.objz.commandbridge.velocity.net.session.SessionHub; +import dev.objz.commandbridge.velocity.util.PlayerTracker; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; + +public final class VelocityCommandBridgeImpl implements CommandBridgeAPI { + + private final SessionHub sessions; + private final PlayerTracker playerTracker; + private final String serverId; + private final EndpointServer endpointServer; + + private final ConcurrentHashMap, MessageChannel> channels = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> channelTypesByName = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> listenersByType = new ConcurrentHashMap<>(); + private final CopyOnWriteArraySet connectedListeners = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet disconnectedListeners = new CopyOnWriteArraySet<>(); + + public VelocityCommandBridgeImpl(SessionHub sessions, + PlayerTracker playerTracker, + String serverId, + EndpointServer endpointServer) { + this.sessions = Objects.requireNonNull(sessions); + this.playerTracker = Objects.requireNonNull(playerTracker); + this.serverId = Objects.requireNonNull(serverId); + this.endpointServer = Objects.requireNonNull(endpointServer); + } + + @Override + public > C channel(ChannelType type) { + Objects.requireNonNull(type); + channelTypesByName.putIfAbsent(type.getClass().getName(), type); + MessageChannel channel = channels.computeIfAbsent(type.getClass(), ignored -> createChannel(type)); + return typeCast(channel); + } + + @Override + public

CompletableFuture broadcast(MessageChannel

channel, P payload) { + Objects.requireNonNull(channel); + Objects.requireNonNull(payload); + List> futures = new ArrayList<>(); + for (ClientSession session : sessions) { + if (!isRoutable(session)) { + continue; + } + futures.add(channel.send(toPlatform(session.location()).target(session.id()), payload)); + } + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + } + + @Override + public Platform.ServerTarget server() { + return Platform.VELOCITY.target(serverId); + } + + @Override + public ConnectionState connectionState() { + return ConnectionState.AUTHENTICATED; + } + + @Override + public Optional> connectedServers() { + Set result = new LinkedHashSet<>(); + for (ClientSession session : sessions) { + if (isRoutable(session)) { + result.add(session.id()); + } + } + return Optional.of(Set.copyOf(result)); + } + + @Override + public Optional playerLocator() { + return Optional.of(player -> { + for (ClientSession session : sessions) { + if (isRoutable(session) && playerTracker.isPlayerOn(player, session.id())) { + return Optional.of(toPlatform(session.location()).target(session.id())); + } + } + return Optional.empty(); + }); + } + + @Override + public Subscription onServerConnected(ServerEventListener listener) { + Objects.requireNonNull(listener); + connectedListeners.add(listener); + return () -> connectedListeners.remove(listener); + } + + @Override + public Subscription onServerDisconnected(ServerEventListener listener) { + Objects.requireNonNull(listener); + disconnectedListeners.add(listener); + return () -> disconnectedListeners.remove(listener); + } + + @Override + public Subscription onConnectionStateChanged(Consumer listener) { + Objects.requireNonNull(listener); + listener.accept(ConnectionState.AUTHENTICATED); + return () -> { + }; + } + + public void onServerConnected(ClientSession session) { + if (session == null || !isRoutable(session)) { + return; + } + Platform.ServerTarget target = toPlatform(session.location()).target(session.id()); + for (ServerEventListener listener : connectedListeners) { + try { + listener.accept(target); + } catch (Exception e) { + Log.warn("Failed to run server-connect listener: {}", e.getMessage()); + } + } + } + + public void onServerDisconnected(ClientSession session) { + if (session == null || session.id() == null || session.id().isBlank()) { + return; + } + Platform.ServerTarget target = toPlatform(session.location()).target(session.id()); + for (ServerEventListener listener : disconnectedListeners) { + try { + listener.accept(target); + } catch (Exception e) { + Log.warn("Failed to run server-disconnect listener: {}", e.getMessage()); + } + } + } + + public void handlePluginMessageRequest(Endpoint endpoint, Envelope env) { + String to = env.to(); + if (to != null && !to.equals(serverId) && !"*".equals(to)) { + relayDirect(env); + return; + } + + PluginMessage message = readPluginMessage(env); + if (message == null) { + return; + } + + dispatchLocal(env, message); + + if ("*".equals(to)) { + relayBroadcast(env); + return; + } + + if (message.expectsResponse()) { + Envelope response = Envelope.reply(env, MessageType.PLUGIN_MESSAGE_RESPONSE, serverId, + Envelope.MAPPER.valueToTree(message)); + endpointServer.send(endpoint, response).dispatch().exceptionally(ex -> { + Log.warn("Failed to send plugin message response: {}", ex.getMessage()); + return null; + }); + } + } + + public void handlePluginMessageResponse(Envelope env) { + String to = env.to(); + if (to != null && !to.equals(serverId) && !"*".equals(to)) { + relayDirect(env); + } + } + + private > MessageChannel createChannel(ChannelType type) { + if (type instanceof CommandChannelType commandType) { + return new CommandMessageChannel(commandType, + this::sendPluginMessage, + this::requestPluginMessage, + listener -> registerListener(commandType, listener)); + } + + return new PluginMessageChannel<>(type, + this::sendPluginMessage, + this::requestPluginMessage, + listener -> registerListener(type, listener)); + } + + private

Subscription registerListener(ChannelType> type, + MessageListener

listener) { + String key = type.getClass().getName(); + channelTypesByName.putIfAbsent(key, type); + CopyOnWriteArrayList> listeners = listenersByType.computeIfAbsent(key, + ignored -> new CopyOnWriteArrayList<>()); + listeners.add(listener); + return () -> listeners.remove(listener); + } + + private CompletableFuture sendPluginMessage(Platform.ServerTarget target, PluginMessage payload) { + if (isLocalVelocity(target)) { + Envelope local = Envelope.make(MessageType.PLUGIN_MESSAGE, serverId, serverId, + Envelope.MAPPER.valueToTree(payload)); + dispatchLocal(local, payload); + return CompletableFuture.completedFuture(null); + } + + Optional session = sessions.findSession(target.id(), toLocation(target.type())); + if (session.isEmpty()) { + return CompletableFuture.failedFuture( + new IllegalStateException("Target server is not connected: " + target.id())); + } + + Envelope env = Envelope.make(MessageType.PLUGIN_MESSAGE, serverId, target.id(), + Envelope.MAPPER.valueToTree(payload)); + return endpointServer.send(session.get().endpoint(), env).dispatch(); + } + + private CompletableFuture requestPluginMessage(Platform.ServerTarget target, + PluginMessage payload, + Duration timeout) { + Objects.requireNonNull(timeout); + if (isLocalVelocity(target)) { + Envelope local = Envelope.make(MessageType.PLUGIN_MESSAGE, serverId, serverId, + Envelope.MAPPER.valueToTree(payload)); + dispatchLocal(local, payload); + return CompletableFuture.completedFuture(payload); + } + + Optional session = sessions.findSession(target.id(), toLocation(target.type())); + if (session.isEmpty()) { + return CompletableFuture.failedFuture( + new IllegalStateException("Target server is not connected: " + target.id())); + } + + Envelope env = Envelope.make(MessageType.PLUGIN_MESSAGE, serverId, target.id(), + Envelope.MAPPER.valueToTree(payload)); + return endpointServer.send(session.get().endpoint(), env) + .expect(MessageType.PLUGIN_MESSAGE_RESPONSE) + .timeout(timeout) + .await() + .thenApply(this::requirePluginMessage); + } + + private void dispatchLocal(Envelope env, PluginMessage message) { + ChannelType channelType = channelTypesByName.get(message.channelType()); + if (channelType == null) { + return; + } + + Object payload; + try { + payload = Envelope.MAPPER.treeToValue(message.data(), channelType.type()); + } catch (Exception e) { + Log.warn("Failed to decode plugin payload for {}: {}", message.channelType(), e.getMessage()); + return; + } + + CopyOnWriteArrayList> listeners = listenersByType.get(message.channelType()); + if (listeners == null || listeners.isEmpty()) { + return; + } + + Platform.ServerTarget source = sourceTarget(env.from()); + MessageContext context = contextCast(new MessageContext<>( + typeCast(channelType), + source, + env.ts())); + + for (MessageListener raw : listeners) { + try { + listenerCast(raw).accept(context, payloadCast(payload)); + } catch (Exception e) { + Log.warn("Plugin message listener failed for {}: {}", message.channelType(), e.getMessage()); + } + } + } + + private void relayDirect(Envelope env) { + if (env.to() == null || env.to().isBlank()) { + return; + } + + Optional target = sessions.get(env.to()); + if (target.isEmpty() || !isRoutable(target.get())) { + return; + } + + endpointServer.send(target.get().endpoint(), env).dispatch().exceptionally(ex -> { + Log.warn("Failed to relay plugin message to {}: {}", env.to(), ex.getMessage()); + return null; + }); + } + + private void relayBroadcast(Envelope env) { + for (ClientSession session : sessions) { + if (!isRoutable(session) || session.id().equals(env.from())) { + continue; + } + + Envelope forwarded = new Envelope(env.v(), env.id(), env.type(), env.from(), session.id(), env.ts(), + env.payload()); + endpointServer.send(session.endpoint(), forwarded).dispatch().exceptionally(ex -> { + Log.warn("Failed to relay broadcast plugin message to {}: {}", session.id(), ex.getMessage()); + return null; + }); + } + } + + private PluginMessage readPluginMessage(Envelope env) { + try { + return Envelope.MAPPER.treeToValue(env.payload(), PluginMessage.class); + } catch (Exception e) { + Log.warn("Failed to decode plugin message: {}", e.getMessage()); + return null; + } + } + + private PluginMessage requirePluginMessage(Envelope env) { + try { + return Envelope.MAPPER.treeToValue(env.payload(), PluginMessage.class); + } catch (Exception e) { + throw new CompletionException(e); + } + } + + private Platform.ServerTarget sourceTarget(String sourceId) { + if (sourceId == null || sourceId.isBlank()) { + return Platform.BACKEND.target("unknown"); + } + if (serverId.equals(sourceId)) { + return Platform.VELOCITY.target(sourceId); + } + + return sessions.get(sourceId) + .map(session -> toPlatform(session.location()).target(sourceId)) + .orElse(Platform.BACKEND.target(sourceId)); + } + + private boolean isLocalVelocity(Platform.ServerTarget target) { + return target.type() == Platform.VELOCITY && serverId.equals(target.id()); + } + + private boolean isRoutable(ClientSession session) { + return session != null + && session.id() != null + && !session.id().isBlank() + && session.status() == AuthStatus.AUTH_OK + && session.endpoint() != null + && session.endpoint().isOpen(); + } + + private static Platform toPlatform(Location location) { + return location == Location.VELOCITY ? Platform.VELOCITY : Platform.BACKEND; + } + + private static Location toLocation(Platform platform) { + return platform == Platform.VELOCITY ? Location.VELOCITY : Location.BACKEND; + } + + @SuppressWarnings("unchecked") + private static > C typeCast(Object value) { + return (C) value; + } + + @SuppressWarnings("unchecked") + private static MessageContext contextCast(MessageContext context) { + return (MessageContext) context; + } + + @SuppressWarnings("unchecked") + private static MessageListener listenerCast(MessageListener listener) { + return (MessageListener) listener; + } + + private static ChannelPayload payloadCast(Object value) { + return (ChannelPayload) value; + } +} diff --git a/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityPluginMessageHandler.java b/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityPluginMessageHandler.java new file mode 100644 index 00000000..25d1a722 --- /dev/null +++ b/velocity/src/main/java/dev/objz/commandbridge/velocity/api/VelocityPluginMessageHandler.java @@ -0,0 +1,27 @@ +package dev.objz.commandbridge.velocity.api; + +import dev.objz.commandbridge.net.Endpoint; +import dev.objz.commandbridge.net.InboundHandler; +import dev.objz.commandbridge.net.proto.Envelope; + +import java.util.Objects; + +public final class VelocityPluginMessageHandler extends InboundHandler { + + private final VelocityCommandBridgeImpl api; + private final boolean response; + + public VelocityPluginMessageHandler(VelocityCommandBridgeImpl api, boolean response) { + this.api = Objects.requireNonNull(api); + this.response = response; + } + + @Override + public void accept(Endpoint endpoint, Envelope env) { + if (response) { + api.handlePluginMessageResponse(env); + return; + } + api.handlePluginMessageRequest(endpoint, env); + } +} From 2bfeb1b3d06c073007c2d6a5dacab6f60b5b1602 Mon Sep 17 00:00:00 2001 From: objz Date: Sun, 22 Mar 2026 00:57:13 +0100 Subject: [PATCH 4/4] doc: added comments --- .../commandbridge/api/CommandBridgeAPI.java | 38 +++++++++++++++++++ .../api/CommandBridgeProvider.java | 20 ++++++++++ .../api/channel/ChannelPayload.java | 1 + .../api/channel/ChannelType.java | 7 ++++ .../commandbridge/api/channel/Channels.java | 2 + .../api/channel/MessageChannel.java | 33 ++++++++++++++++ .../api/channel/command/CommandChannel.java | 24 ++++++++++++ .../channel/command/CommandChannelType.java | 2 + .../api/channel/command/CommandPayload.java | 10 +++++ .../api/channel/command/RunAs.java | 4 ++ .../api/message/MessageContext.java | 8 ++++ .../api/message/MessageListener.java | 11 ++++++ .../api/message/ServerEventListener.java | 6 +++ .../api/message/Subscription.java | 2 + .../api/platform/ConnectionState.java | 10 +++++ .../commandbridge/api/platform/Platform.java | 15 ++++++++ .../api/platform/PlayerLocator.java | 7 ++++ 17 files changed, 200 insertions(+) diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java index 7908daca..fd3977dd 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeAPI.java @@ -14,23 +14,61 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +/** Main entry point for interacting with the CommandBridge network. */ public interface CommandBridgeAPI { + /** + * Obtains a {@link MessageChannel} for the given {@link ChannelType}. + * + * @param type the channel type identity + * @param the payload type + * @param the channel interface type + * @return the message channel + */ > C channel(ChannelType type); + /** + * Broadcasts a payload to all connected servers on a specific channel. + * + * @param channel the channel to broadcast on + * @param payload the data to send + * @return a future that completes when the broadcast is dispatched + */

CompletableFuture broadcast(MessageChannel

channel, P payload); + /** @return the identity of the current server */ Platform.ServerTarget server(); + /** @return the current connection state to the bridge network */ ConnectionState connectionState(); + /** @return the IDs of all currently connected servers, if available */ Optional> connectedServers(); + /** @return the player location lookup service, if available */ Optional playerLocator(); + /** + * Subscribes to server connection events. + * + * @param listener the listener to call when a server connects + * @return a subscription handle to cancel the listener + */ Subscription onServerConnected(ServerEventListener listener); + /** + * Subscribes to server disconnection events. + * + * @param listener the listener to call when a server disconnects + * @return a subscription handle to cancel the listener + */ Subscription onServerDisconnected(ServerEventListener listener); + /** + * Subscribes to connection state changes. + * + * @param listener the listener to call when the state changes + * @return a subscription handle to cancel the listener + */ Subscription onConnectionStateChanged(Consumer listener); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java index 2cfa313e..afbc5e56 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java +++ b/api/src/main/java/dev/objz/commandbridge/api/CommandBridgeProvider.java @@ -2,6 +2,7 @@ import java.util.Objects; +/** Static provider for accessing the {@link CommandBridgeAPI} instance. */ public final class CommandBridgeProvider { private static volatile CommandBridgeAPI instance; @@ -10,6 +11,10 @@ private CommandBridgeProvider() { throw new UnsupportedOperationException("Provider can't be instanced"); } + /** + * @return the registered API instance + * @throws IllegalStateException if the API is not registered + */ public static CommandBridgeAPI get() { if (instance == null) { throw new IllegalStateException("CommandBridge is not available. Is it installed and running?"); @@ -17,6 +22,14 @@ public static CommandBridgeAPI get() { return instance; } + /** + * Obtains the API instance cast to a specific type. + * + * @param type the API class type + * @param the API type + * @return the cast API instance + * @throws IllegalStateException if the instance is not available or compatible + */ public static T get(Class type) { CommandBridgeAPI api = get(); if (!type.isInstance(api)) { @@ -25,6 +38,12 @@ public static T get(Class type) { return type.cast(api); } + /** + * Registers the API implementation. + * + * @param impl the implementation to register + * @throws IllegalStateException if an implementation is already registered + */ public static void register(CommandBridgeAPI impl) { Objects.requireNonNull(impl); if (instance != null) { @@ -33,6 +52,7 @@ public static void register(CommandBridgeAPI impl) { instance = impl; } + /** Unregisters the current API implementation. */ public static void unregister() { instance = null; } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java index 42064755..426f4290 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelPayload.java @@ -1,4 +1,5 @@ package dev.objz.commandbridge.api.channel; +/** Marker interface for data sent over a {@link MessageChannel}. */ public interface ChannelPayload { } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java index 7df5aa5c..1aa9997c 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/ChannelType.java @@ -2,6 +2,12 @@ import java.util.Objects; +/** + * Identity and type information for a {@link MessageChannel}. + * + * @param the payload type + * @param the channel interface type + */ public abstract class ChannelType> { private final Class type; @@ -10,6 +16,7 @@ protected ChannelType(Class type) { this.type = Objects.requireNonNull(type); } + /** @return the class of the payload handled by this channel */ public Class type() { return type; } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java b/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java index 17e0e5d0..fc026bdf 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/Channels.java @@ -2,8 +2,10 @@ import dev.objz.commandbridge.api.channel.command.CommandChannelType; +/** Registry of built-in {@link ChannelType}s. */ public final class Channels { + /** The default channel for executing commands. */ public static final CommandChannelType COMMAND = new CommandChannelType(); private Channels() { diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java index e5995ebe..79c37848 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/MessageChannel.java @@ -7,13 +7,46 @@ import java.time.Duration; import java.util.concurrent.CompletableFuture; +/** + * Communication pipe for sending and receiving {@link ChannelPayload}s. + * + * @param

the type of payload handled by this channel + */ public interface MessageChannel

{ + /** + * Sends a payload to a target server without expecting a response. + * + * @param target the destination server + * @param payload the data to send + * @return a future that completes when the message is sent + */ CompletableFuture send(Platform.ServerTarget target, P payload); + /** + * Sends a request to a target server and waits for a response. + * + * @param target the destination server + * @param payload the request data + * @return a future containing the response payload + */ CompletableFuture

request(Platform.ServerTarget target, P payload); + /** + * Sends a request to a target server with a custom timeout. + * + * @param target the destination server + * @param payload the request data + * @param timeout the maximum time to wait for a response + * @return a future containing the response payload + */ CompletableFuture

request(Platform.ServerTarget target, P payload, Duration timeout); + /** + * Subscribes a listener to messages received on this channel. + * + * @param listener the listener to call for incoming messages + * @return a subscription handle to cancel the listener + */ Subscription listen(MessageListener

listener); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java index 07bf28be..613673a6 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannel.java @@ -6,16 +6,40 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +/** Specialized channel for dispatching commands to servers. */ public interface CommandChannel extends MessageChannel { + /** + * Executes a command as the console. + * + * @param target the destination server + * @param command the command string to run + * @return a future that completes when the command is sent + */ default CompletableFuture console(Platform.ServerTarget target, String command) { return send(target, CommandPayload.console(command)); } + /** + * Executes a command as a specific player. + * + * @param target the destination server + * @param command the command string to run + * @param player the UUID of the player to run as + * @return a future that completes when the command is sent + */ default CompletableFuture player(Platform.ServerTarget target, String command, UUID player) { return send(target, CommandPayload.player(command, player)); } + /** + * Executes a command with operator permissions, bypassing standard checks. + * + * @param target the destination server + * @param command the command string to run + * @param player the UUID of the player to run as + * @return a future that completes when the command is sent + */ default CompletableFuture operator(Platform.ServerTarget target, String command, UUID player) { return send(target, CommandPayload.operator(command, player)); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java index b9ba2ded..2bf89af8 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandChannelType.java @@ -2,8 +2,10 @@ import dev.objz.commandbridge.api.channel.ChannelType; +/** Identity for the {@link CommandChannel}. */ public final class CommandChannelType extends ChannelType { + /** Creates a new command channel type. */ public CommandChannelType() { super(CommandPayload.class); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java index b6987e9e..cb74a2ba 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/CommandPayload.java @@ -4,16 +4,26 @@ import java.util.UUID; +/** + * Payload for command execution. + * + * @param command the command string to run + * @param runAs the execution mode + * @param player the optional player context + */ public record CommandPayload(String command, RunAs runAs, UUID player) implements ChannelPayload { + /** Creates a payload for console execution. */ public static CommandPayload console(String command) { return new CommandPayload(command, RunAs.CONSOLE, null); } + /** Creates a payload for player execution. */ public static CommandPayload player(String command, UUID player) { return new CommandPayload(command, RunAs.PLAYER, player); } + /** Creates a payload for operator execution. */ public static CommandPayload operator(String command, UUID player) { return new CommandPayload(command, RunAs.OPERATOR, player); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java b/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java index d5faafea..6645dca8 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java +++ b/api/src/main/java/dev/objz/commandbridge/api/channel/command/RunAs.java @@ -1,7 +1,11 @@ package dev.objz.commandbridge.api.channel.command; +/** Defines the execution context for a command. */ public enum RunAs { + /** Run as the server console. */ CONSOLE, + /** Run as a specific player. */ PLAYER, + /** Run as a player with temporary operator permissions. */ OPERATOR } diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java b/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java index 7d5f8242..1a6c093a 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java +++ b/api/src/main/java/dev/objz/commandbridge/api/message/MessageContext.java @@ -5,6 +5,14 @@ import dev.objz.commandbridge.api.channel.MessageChannel; import dev.objz.commandbridge.api.platform.Platform; +/** + * Metadata for a received message. + * + * @param channel the channel the message was received on + * @param from the server that sent the message + * @param timestamp the time the message was sent + * @param the payload type + */ public record MessageContext( ChannelType> channel, Platform.ServerTarget from, diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java b/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java index 907a79ed..a6c219fc 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java +++ b/api/src/main/java/dev/objz/commandbridge/api/message/MessageListener.java @@ -2,7 +2,18 @@ import dev.objz.commandbridge.api.channel.ChannelPayload; +/** + * Handles incoming messages for a specific channel. + * + * @param the payload type + */ @FunctionalInterface public interface MessageListener { + /** + * Invoked when a message is received. + * + * @param ctx the message metadata + * @param payload the received data + */ void accept(MessageContext ctx, T payload); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java b/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java index ceecccad..ee84a8f5 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java +++ b/api/src/main/java/dev/objz/commandbridge/api/message/ServerEventListener.java @@ -2,7 +2,13 @@ import dev.objz.commandbridge.api.platform.Platform; +/** Listener for server lifecycle events in the bridge network. */ @FunctionalInterface public interface ServerEventListener { + /** + * Invoked when a server connects or disconnects. + * + * @param server the server target involved in the event + */ void accept(Platform.ServerTarget server); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java b/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java index 028775d7..3ee6eb36 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java +++ b/api/src/main/java/dev/objz/commandbridge/api/message/Subscription.java @@ -1,6 +1,8 @@ package dev.objz.commandbridge.api.message; +/** A handle to a registered listener that can be cancelled. */ @FunctionalInterface public interface Subscription { + /** Stops the listener from receiving further events. */ void cancel(); } diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java b/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java index c1297e45..b3a4d970 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/ConnectionState.java @@ -1,27 +1,37 @@ package dev.objz.commandbridge.api.platform; +/** Represents the network connection status between a server and the bridge. */ public enum ConnectionState { + /** No connection established. */ DISCONNECTED, + /** Attempting to establish a socket connection. */ CONNECTING, + /** Socket connected, awaiting authentication. */ CONNECTED, + /** Fully connected and authorized to send/receive messages. */ AUTHENTICATED, + /** Connection lost, attempting to re-establish. */ RECONNECTING, + /** Authentication failed; no further attempts will be made. */ AUTH_FAILED; + /** @return true if the connection is established, even if not yet authenticated */ public boolean isActive() { return this == CONNECTED || this == AUTHENTICATED; } + /** @return true if the connection is ready for message transport */ public boolean canSend() { return this == AUTHENTICATED; } + /** @return true if the connection reached a non-recoverable error state */ public boolean isTerminal() { return this == AUTH_FAILED; } diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java b/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java index cabed602..ab635d33 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/Platform.java @@ -1,13 +1,28 @@ package dev.objz.commandbridge.api.platform; +/** Enumeration of supported bridge platforms. */ public enum Platform { + /** A backend Minecraft server (e.g. Paper, Folia). */ BACKEND, + /** The Velocity proxy. */ VELOCITY; + /** + * Creates a {@link ServerTarget} for this platform. + * + * @param id the unique server identifier + * @return the target representation + */ public ServerTarget target(String id) { return new ServerTarget(id, this); } + /** + * Identifies a specific server in the bridge network. + * + * @param id the unique server identifier + * @param type the platform type of the server + */ public record ServerTarget(String id, Platform type) { } } diff --git a/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java b/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java index 9d6d43da..393ba8b7 100644 --- a/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java +++ b/api/src/main/java/dev/objz/commandbridge/api/platform/PlayerLocator.java @@ -3,7 +3,14 @@ import java.util.Optional; import java.util.UUID; +/** Service for finding which server a player is currently connected to. */ @FunctionalInterface public interface PlayerLocator { + /** + * Resolves the server target for a player UUID. + * + * @param player the UUID of the player to locate + * @return the server target, or empty if the player is offline or not found + */ Optional locate(UUID player); }