From e93cf306e270303f9f4bb8669b9381e5be15be08 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Tue, 4 Aug 2026 12:28:20 +0200 Subject: [PATCH 1/2] fix(cloudnet): load the bridge as a Minestom extension in game and setup The game loaded CloudNet's MinestomBridgeExtension directly via Aerogel DI inside a try-with-resources on InjectionLayer.ext(). That layer is an UncloseableInjectionLayer whose close() always throws, so the catch block logged "Failed to initialize CloudNet MinestomBridgeExtension" on every single start - even when it had succeeded - and onDisable() was never reached. The setup service had no CloudNet integration at all beyond the bind address. Both services now bootstrap minestom-ce-extensions and load the bridge from extensions/ like Titan does, so no CloudNet artifact is bundled any more: the wrapper provides the driver at runtime and the bridge arrives as an extension. cygnus.jar and setup.jar contain zero eu/cloudnetservice classes now. Permissions went through CloudNet's default checker, which only inspects player.getPermissionLevel(). That is always 0 on a LuckPerms-managed server, so maintenance bypass and task-level requiredPermission checks rejected everyone, staff included. Players now carry Adventure's PermissionChecker pointer backed by LuckPerms (PermissionAwarePlayer, the shared base of CygnusPlayer and SetupPlayer), which is the pointer LuckPerms' own sender factory reads. The new :bridge extension registers a checker reading that same pointer as the CloudNet registry default, and /stop resolves its permission through it instead of calling LuckPerms directly. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GsZ8rsCxv99T6SfC1zBpip --- bridge/build.gradle.kts | 96 +++++++++++++++++++ .../CygnusBridgePermissionExtension.java | 43 +++++++++ bridge/src/main/resources/extension.json | 7 ++ common/build.gradle.kts | 3 + .../cygnus/common/bootstrap/StopCommand.java | 26 ++--- .../cygnus/common/permission/TriStates.java | 32 +++++++ .../player/InstanceSwitchChunkPlayer.java | 6 +- .../common/player/PermissionAwarePlayer.java | 70 ++++++++++++++ .../common/permission/TriStatesTest.java | 34 +++++++ game/build.gradle.kts | 14 ++- .../onelitefeather/cygnus/CygnusLoader.java | 18 ++-- settings.gradle.kts | 28 ++++-- setup/build.gradle.kts | 13 ++- .../cygnus/setup/SetupLoader.java | 8 +- 14 files changed, 357 insertions(+), 41 deletions(-) create mode 100644 bridge/build.gradle.kts create mode 100644 bridge/src/main/java/net/onelitefeather/cygnus/bridge/CygnusBridgePermissionExtension.java create mode 100644 bridge/src/main/resources/extension.json create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/permission/TriStates.java create mode 100644 common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java create mode 100644 common/src/test/java/net/onelitefeather/cygnus/common/permission/TriStatesTest.java diff --git a/bridge/build.gradle.kts b/bridge/build.gradle.kts new file mode 100644 index 00000000..78c6794b --- /dev/null +++ b/bridge/build.gradle.kts @@ -0,0 +1,96 @@ +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + id("cygnus.java-conventions") + `maven-publish` +} + +// Minestom extension that bridges CloudNet permission checks to LuckPerms. It is packaged as a +// standalone extension jar (dropped into a CloudNet service's extensions/ folder next to the +// CloudNet bridge) and never bundled into a fat jar. Everything it compiles against is provided at +// runtime: the CloudNet driver by the CloudNet wrapper, the bridge by the CloudNet_Bridge +// extension, Minestom and Adventure by the application classloader. +dependencies { + compileOnly(platform(libs.aonyx.bom)) + compileOnly(libs.minestom) + compileOnly(libs.adventure) + compileOnly(libs.minestom.ce.extensions) + + compileOnly(platform(libs.cloudnet.bom)) + compileOnly(libs.cloudnet.driver.api) + compileOnly(libs.cloudnet.bridge) + compileOnly(libs.cloudnet.bridge.impl) +} + +// Stamp the version into extension.json (@version@ placeholder). Subprojects do not inherit the +// root version, so read it from the root project - the same source the publications use. +tasks.processResources { + val tokens = mapOf("version" to rootProject.version.toString()) + inputs.properties(tokens) + filesMatching("extension.json") { + filter("tokens" to tokens) + } +} + +publishing { + repositories { + maven { + authentication { + credentials(PasswordCredentials::class) { + // Those credentials need to be set under "Settings -> Secrets -> Actions" in your repository + username = System.getenv("ONELITEFEATHER_MAVEN_USERNAME") + password = System.getenv("ONELITEFEATHER_MAVEN_PASSWORD") + } + } + name = "OneLiteFeatherRepository" + url = if (rootProject.version.toString().contains("SNAPSHOT")) { + uri("https://repo.onelitefeather.dev/onelitefeather-snapshots") + } else { + uri("https://repo.onelitefeather.dev/onelitefeather-releases") + } + } + } + publications { + create("maven") { + artifact(project.tasks.getByName("jar")) + version = rootProject.version as String + artifactId = "cygnus-bridge" + groupId = rootProject.group as String + pom { + description.set("CloudNet bridge extension that resolves permissions through LuckPerms") + name = "Cygnus Bridge Component" + url = "https://github.com/OneLiteFeatherNET/Cygnus" + licenses { + license { + name = "AGPL-3.0 License" + url = "https://www.gnu.org/licenses/agpl-3.0.en.html" + } + } + developers { + developer { + name.set("OneliteFeather") + contributors { + contributor { + name.set("theEvilReaper") + } + contributor { + name.set("TheMeinerLP") + } + } + } + } + + issueManagement { + system.set("Github") + url.set("https://github.com/OneLiteFeatherNET/Cygnus/issues") + } + + scm { + connection = "scm:git:git://github.com:OneLiteFeatherNET/Cygnus.git" + developerConnection = "scm:git:ssh://git@github.com:OneLiteFeatherNET/Cygnus.git" + url = "https://github.com/OneLiteFeatherNET/Cygnus" + } + } + } + } +} diff --git a/bridge/src/main/java/net/onelitefeather/cygnus/bridge/CygnusBridgePermissionExtension.java b/bridge/src/main/java/net/onelitefeather/cygnus/bridge/CygnusBridgePermissionExtension.java new file mode 100644 index 00000000..72802dd3 --- /dev/null +++ b/bridge/src/main/java/net/onelitefeather/cygnus/bridge/CygnusBridgePermissionExtension.java @@ -0,0 +1,43 @@ +package net.onelitefeather.cygnus.bridge; + +import eu.cloudnetservice.driver.registry.ServiceRegistry; +import eu.cloudnetservice.modules.bridge.impl.platform.minestom.MinestomPermissionChecker; +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.TriState; +import net.minestom.server.extensions.Extension; + +/** + * Minestom extension that teaches the CloudNet bridge how Cygnus resolves permissions. + *

+ * The bridge ships a default checker that only inspects {@code player.getPermissionLevel()}, which + * is always {@code 0} on a LuckPerms-managed server — maintenance bypass and task-level + * {@code requiredPermission} checks would therefore reject every player, staff included. This + * extension registers a checker that reads Adventure's {@link PermissionChecker#POINTER} instead, + * the same pointer LuckPerms and our {@code /stop} command read, and marks it the registry default. + *

+ * {@link MinestomPermissionChecker} only exists inside the CloudNet bridge's extension classloader, + * so this glue cannot live in the application. Declaring a dependency on the {@code CloudNet_Bridge} + * extension (see {@code extension.json}) makes this extension load after the bridge and share its + * classloader hierarchy. Minestom and Adventure come from the application classloader above, so the + * pointer read here is the very one the player carries. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 2.6.7 + **/ +public final class CygnusBridgePermissionExtension extends Extension { + + @Override + public void initialize() { + MinestomPermissionChecker checker = (player, permission) -> + player.getOrDefault(PermissionChecker.POINTER, PermissionChecker.always(TriState.FALSE)) + .test(permission); + ServiceRegistry.registry() + .registerProvider(MinestomPermissionChecker.class, "cygnus-luckperms", checker) + .markAsDefaultService(); + } + + @Override + public void terminate() { + } +} diff --git a/bridge/src/main/resources/extension.json b/bridge/src/main/resources/extension.json new file mode 100644 index 00000000..04c06719 --- /dev/null +++ b/bridge/src/main/resources/extension.json @@ -0,0 +1,7 @@ +{ + "name": "CygnusCloudNetPermissions", + "version": "@version@", + "entrypoint": "net.onelitefeather.cygnus.bridge.CygnusBridgePermissionExtension", + "authors": ["OneLiteFeather"], + "dependencies": ["CloudNet_Bridge"] +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 24f4c0f1..09bab65f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { } testImplementation(libs.minestom) + testImplementation(libs.luckperms.api) { + exclude(group = "net.kyori.adventure") + } testImplementation(libs.cyano) testImplementation(libs.aves) testImplementation(libs.xerus) diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/bootstrap/StopCommand.java b/common/src/main/java/net/onelitefeather/cygnus/common/bootstrap/StopCommand.java index bd80f76c..5ba30765 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/bootstrap/StopCommand.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/bootstrap/StopCommand.java @@ -1,8 +1,9 @@ package net.onelitefeather.cygnus.common.bootstrap; -import net.luckperms.api.LuckPermsProvider; -import net.luckperms.api.model.user.User; +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.TriState; import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.Command; import net.minestom.server.entity.Player; @@ -11,7 +12,7 @@ * players holding {@value #PERMISSION}, since a service should not be stoppable by regular players. * * @author TheMeinerLP - * @version 1.0.0 + * @version 1.1.0 * @since 2.6.7 **/ public final class StopCommand extends Command { @@ -23,7 +24,7 @@ public final class StopCommand extends Command { */ public StopCommand() { super("stop"); - setCondition((sender, commandString) -> !(sender instanceof Player player) || hasStopPermission(player)); + setCondition((sender, commandString) -> !(sender instanceof Player) || hasStopPermission(sender)); setDefaultExecutor((sender, context) -> Thread.ofPlatform().name("cygnus-shutdown").start(() -> { MinecraftServer.stopCleanly(); System.exit(0); @@ -31,17 +32,16 @@ public StopCommand() { } /** - * Checks whether the given player is allowed to run this command via LuckPerms. + * Checks whether the given sender is allowed to run this command. *

- * Assumes LuckPerms has already been bootstrapped (see {@code MinestomLoader}), which is - * guaranteed by the time a player can connect and send commands. + * Reads Adventure's {@link PermissionChecker#POINTER}, which our player implementation backs + * with LuckPerms (see {@code PermissionAwarePlayer}). A sender without that pointer is denied. * - * @param player the player to check - * @return {@code true} if the player holds {@value #PERMISSION}, {@code false} otherwise - * (including when LuckPerms has no cached data for the player yet) + * @param sender the sender to check + * @return {@code true} if the sender holds {@value #PERMISSION}, {@code false} otherwise */ - private static boolean hasStopPermission(Player player) { - User user = LuckPermsProvider.get().getUserManager().getUser(player.getUuid()); - return user != null && user.getCachedData().getPermissionData().checkPermission(PERMISSION).asBoolean(); + private static boolean hasStopPermission(CommandSender sender) { + return sender.getOrDefault(PermissionChecker.POINTER, PermissionChecker.always(TriState.FALSE)) + .test(PERMISSION); } } diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/permission/TriStates.java b/common/src/main/java/net/onelitefeather/cygnus/common/permission/TriStates.java new file mode 100644 index 00000000..1eae35c6 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/permission/TriStates.java @@ -0,0 +1,32 @@ +package net.onelitefeather.cygnus.common.permission; + +import net.kyori.adventure.util.TriState; +import net.luckperms.api.util.Tristate; + +/** + * Converts between LuckPerms' and Adventure's tri-state types, which model the same three values + * under two unrelated types. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 2.6.7 + **/ +public final class TriStates { + + private TriStates() { + } + + /** + * Converts a LuckPerms tri-state into its Adventure counterpart. + * + * @param tristate the LuckPerms value to convert + * @return the matching Adventure value, where {@code UNDEFINED} maps to {@code NOT_SET} + */ + public static TriState fromLuckPerms(Tristate tristate) { + return switch (tristate) { + case TRUE -> TriState.TRUE; + case FALSE -> TriState.FALSE; + case UNDEFINED -> TriState.NOT_SET; + }; + } +} diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/player/InstanceSwitchChunkPlayer.java b/common/src/main/java/net/onelitefeather/cygnus/common/player/InstanceSwitchChunkPlayer.java index 46152dca..0b77244d 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/player/InstanceSwitchChunkPlayer.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/player/InstanceSwitchChunkPlayer.java @@ -35,13 +35,15 @@ * *

Remove this class once the server runs a Minestom build that contains PR #3308 — as of * {@code 2026.07.22-26.2} the newest published build predates the merge. Upstream itself intends to - * revert the workaround for 26.3, where the client bug is fixed.

+ * revert the workaround for 26.3, where the client bug is fixed. When that happens, let the + * subclasses extend {@link PermissionAwarePlayer} directly — the permission pointer it installs is + * unrelated to this workaround and must survive its removal.

* * @author TheMeinerLP * @version 1.0.0 * @since 2.6.7 */ -public abstract class InstanceSwitchChunkPlayer extends Player { +public abstract class InstanceSwitchChunkPlayer extends PermissionAwarePlayer { private volatile @Nullable TargetView targetView; diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java b/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java new file mode 100644 index 00000000..838b4e58 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/player/PermissionAwarePlayer.java @@ -0,0 +1,70 @@ +package net.onelitefeather.cygnus.common.player; + +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.util.TriState; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import net.minestom.server.entity.Player; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.onelitefeather.cygnus.common.permission.TriStates; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link Player} that answers permission questions through LuckPerms. + *

+ * Minestom has no permission system of its own — it only carries Adventure's + * {@link PermissionChecker#POINTER}, and everything that asks about permissions reads it from + * there: LuckPerms' own command sender factory, our {@code /stop} command, and the CloudNet bridge + * extension. Neither Minestom nor LuckPerms ever installs that pointer, though; the server + * implementation has to supply it. Without it every permission check silently resolves to + * {@code false}, which would lock staff out of CloudNet maintenance mode just like everyone else. + *

+ * The pointer is dynamic, so no LuckPerms class is touched until a permission is actually queried. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 2.6.7 + **/ +public abstract class PermissionAwarePlayer extends Player implements PermissionChecker { + + private final @NotNull Pointers pointers = PermissionAwarePlayer.super.pointers() + .toBuilder() + .withDynamic(PermissionChecker.POINTER, () -> this) + .build(); + + /** + * {@inheritDoc} + */ + protected PermissionAwarePlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } + + /** + * {@inheritDoc} + */ + @Override + public Pointers pointers() { + return this.pointers; + } + + /** + * Resolves a permission for this player through LuckPerms, honouring the contexts LuckPerms + * has calculated for them. + * + * @param permission the permission node to check + * @return the value LuckPerms holds for the node, or {@link TriState#FALSE} when LuckPerms has + * no user data for this player + */ + @Override + public @NotNull TriState value(@NotNull String permission) { + User user = LuckPermsProvider.get().getUserManager().getUser(getUuid()); + if (user == null) { + return TriState.FALSE; + } + QueryOptions queryOptions = LuckPermsProvider.get().getContextManager().getQueryOptions(this); + return TriStates.fromLuckPerms(user.getCachedData().getPermissionData(queryOptions).checkPermission(permission)); + } +} diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/permission/TriStatesTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/permission/TriStatesTest.java new file mode 100644 index 00000000..d39d4ae9 --- /dev/null +++ b/common/src/test/java/net/onelitefeather/cygnus/common/permission/TriStatesTest.java @@ -0,0 +1,34 @@ +package net.onelitefeather.cygnus.common.permission; + +import net.kyori.adventure.util.TriState; +import net.luckperms.api.util.Tristate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class TriStatesTest { + + @Test + void testConvertTrue() { + assertEquals(TriState.TRUE, TriStates.fromLuckPerms(Tristate.TRUE)); + } + + @Test + void testConvertFalse() { + assertEquals(TriState.FALSE, TriStates.fromLuckPerms(Tristate.FALSE)); + } + + @Test + void testConvertUndefined() { + assertEquals(TriState.NOT_SET, TriStates.fromLuckPerms(Tristate.UNDEFINED)); + } + + @ParameterizedTest + @EnumSource(Tristate.class) + void testEveryValueIsMapped(Tristate tristate) { + assertNotNull(TriStates.fromLuckPerms(tristate)); + } +} diff --git a/game/build.gradle.kts b/game/build.gradle.kts index 1e380f37..8ce8d1a0 100644 --- a/game/build.gradle.kts +++ b/game/build.gradle.kts @@ -24,11 +24,13 @@ dependencies { // server logs nothing at all. runtimeOnly(libs.slf4j.simple) - //CloudNet - implementation(platform(libs.cloudnet.bom)) - implementation(libs.bundles.cloudnet) + // CloudNet is provided by the CloudNet wrapper at runtime and its bridge is loaded as a + // Minestom extension (separate classloader, see the :bridge module), so :game neither + // references nor bundles any CloudNet artifact. + implementation(libs.minestom.ce.extensions) + implementation(libs.kotlin.stdlib.jdk8) - //LuckPerms + // LuckPerms; guava used to arrive transitively through CloudNet, so bundle it explicitly now. implementation(libs.guava) compileOnly(libs.luckperms.api) { exclude(group = "net.kyori.adventure") @@ -65,6 +67,10 @@ tasks { archiveClassifier.set("") archiveFileName.set("cygnus.jar") mergeServiceFiles() + // Shaded deps ship signed and multi-release jars that break a relocation-free + // application fat jar; drop signatures and module-info. + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + exclude("module-info.class", "META-INF/versions/**/module-info.class") } } diff --git a/game/src/main/java/net/onelitefeather/cygnus/CygnusLoader.java b/game/src/main/java/net/onelitefeather/cygnus/CygnusLoader.java index d41f20cf..c1ddb26d 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/CygnusLoader.java +++ b/game/src/main/java/net/onelitefeather/cygnus/CygnusLoader.java @@ -1,27 +1,27 @@ package net.onelitefeather.cygnus; -import dev.derklaro.aerogel.Injector; -import eu.cloudnetservice.driver.inject.InjectionLayer; -import eu.cloudnetservice.modules.bridge.impl.platform.minestom.MinestomBridgeExtension; import me.lucko.luckperms.minestom.loader.MinestomLoader; -import net.minestom.server.MinecraftServer; +import net.hollowcube.minestom.extensions.ExtensionBootstrap; import net.onelitefeather.cygnus.common.bootstrap.ServiceBootstrap; import net.onelitefeather.cygnus.common.dimension.DimensionFactory; public final class CygnusLoader { static void main() { - MinecraftServer server = MinecraftServer.init(); + // minestom-ce-extensions loads platform extensions - the CloudNet bridge and our + // :bridge permission extension among them - from the extensions/ folder. Running + // standalone simply loads none. This also performs MinecraftServer.init(). + ExtensionBootstrap bootstrap = ExtensionBootstrap.init(); MinestomLoader.get().load().registerShutdownHook().start(); String customDimensions = System.getProperty("cygnus.customDimension", "false"); if (Boolean.parseBoolean(customDimensions)) { DimensionFactory.registerAll(); } new Cygnus(); - try (InjectionLayer layer = InjectionLayer.ext()) { - layer.instance(MinestomBridgeExtension.class).onLoad(); - } ServiceBootstrap.installShutdownHandling(); - server.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); + bootstrap.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); + } + + private CygnusLoader() { } } diff --git a/settings.gradle.kts b/settings.gradle.kts index e423b2d4..e477d881 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,8 +3,15 @@ rootProject.name = "Cygnus" dependencyResolutionManagement { repositories { mavenCentral() + // minestom-ce-extensions pulls com.github.Minestom:DependencyGetter from JitPack; + // resolve it through the OneLiteFeather reposilite proxy that caches JitPack. + maven { + name = "reposiliteRepositoryOnelitefeatherProxy" + url = uri("https://repo.onelitefeather.dev/onelitefeather-proxy") + } maven("https://central.sonatype.com/repository/maven-snapshots/") maven("https://repository.derklaro.dev/snapshots/") + maven("https://repository.derklaro.dev/releases/") maven { name = "OneLiteFeatherRepository" url = uri("https://repo.onelitefeather.dev/onelitefeather") @@ -33,6 +40,8 @@ dependencyResolutionManagement { version("luckperms-minestom-loader", "5.6-SNAPSHOT") version("guava", "33.6.0-jre") version("falco", "1.0.0") + version("minestom-ce-extensions", "1.2.0") + version("kotlin", "2.4.0") library("aonyx.bom", "net.onelitefeather", "aonyx-bom").versionRef("aonyx") library("slf4j.api", "org.slf4j", "slf4j-api").versionRef("slf4j") @@ -42,6 +51,11 @@ dependencyResolutionManagement { library("luckperms.minestom.loader", "net.luckperms", "minestom-loader").versionRef("luckperms-minestom-loader") library("minestom", "net.minestom", "minestom").withoutVersion() + library("minestom-ce-extensions", "dev.hollowcube", "minestom-ce-extensions").versionRef("minestom-ce-extensions") + // minestom-ce-extensions resolves extension dependencies through a Kotlin class + // (net.minestom.dependencies.maven.MavenRepository); without the Kotlin stdlib on the + // classpath ExtensionBootstrap init fails with NoClassDefFoundError on kotlin/jvm/internal/Intrinsics. + library("kotlin-stdlib-jdk8", "org.jetbrains.kotlin", "kotlin-stdlib-jdk8").versionRef("kotlin") library("adventure", "net.kyori", "adventure-text-minimessage").withoutVersion() library("cyano", "net.onelitefeather", "cyano").withoutVersion() library("guira", "net.onelitefeather", "guira").withoutVersion() @@ -56,9 +70,12 @@ dependencyResolutionManagement { library("falco.anvil", "net.onelitefeather", "falco-anvil").withoutVersion() library("canis", "com.github.theEvilReaper", "Canis").version("master-SNAPSHOT") + // CloudNet is never bundled: the wrapper provides the driver at runtime and the bridge + // arrives as a Minestom extension. Only the :bridge extension module compiles against it. library("cloudnet-bom", "eu.cloudnetservice.cloudnet", "bom").versionRef("cloudnet") library("cloudnet-bridge", "eu.cloudnetservice.cloudnet", "bridge-api").withoutVersion() library("cloudnet-bridge-impl", "eu.cloudnetservice.cloudnet", "bridge-impl").withoutVersion() + library("cloudnet-driver-api", "eu.cloudnetservice.cloudnet", "driver-api").withoutVersion() library("cloudnet-driver-impl", "eu.cloudnetservice.cloudnet", "driver-impl").withoutVersion() library("cloudnet-platform-inject", "eu.cloudnetservice.cloudnet", "platform-inject-api").withoutVersion() library("cloudnet-jvm-wrapper", "eu.cloudnetservice.cloudnet", "wrapper-jvm-api").withoutVersion() @@ -66,16 +83,6 @@ dependencyResolutionManagement { plugin("shadow", "com.gradleup.shadow").versionRef("shadow") plugin("cyclonedx", "org.cyclonedx.bom").versionRef("cyclonedx") - bundle( - "cloudnet", - listOf( - "cloudnet-bridge", - "cloudnet-bridge-impl", - "cloudnet-driver-impl", - "cloudnet-platform-inject", - "cloudnet-jvm-wrapper" - ) - ) } } } @@ -83,3 +90,4 @@ dependencyResolutionManagement { include("common") include("setup") include("game") +include("bridge") diff --git a/setup/build.gradle.kts b/setup/build.gradle.kts index b719871c..2a53ac39 100644 --- a/setup/build.gradle.kts +++ b/setup/build.gradle.kts @@ -26,7 +26,13 @@ dependencies { // server logs nothing at all. runtimeOnly(libs.slf4j.simple) - //LuckPerms + // CloudNet is provided by the CloudNet wrapper at runtime and its bridge is loaded as a + // Minestom extension (separate classloader, see the :bridge module), so :setup neither + // references nor bundles any CloudNet artifact. + implementation(libs.minestom.ce.extensions) + implementation(libs.kotlin.stdlib.jdk8) + + // LuckPerms implementation(libs.guava) compileOnly(libs.luckperms.api) { exclude(group = "net.kyori.adventure") @@ -60,6 +66,11 @@ tasks { shadowJar { archiveClassifier.set("") archiveFileName.set("setup.jar") + mergeServiceFiles() + // Shaded deps ship signed and multi-release jars that break a relocation-free + // application fat jar; drop signatures and module-info. + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") + exclude("module-info.class", "META-INF/versions/**/module-info.class") } } diff --git a/setup/src/main/java/net/onelitefeather/cygnus/setup/SetupLoader.java b/setup/src/main/java/net/onelitefeather/cygnus/setup/SetupLoader.java index a606681b..89deb024 100644 --- a/setup/src/main/java/net/onelitefeather/cygnus/setup/SetupLoader.java +++ b/setup/src/main/java/net/onelitefeather/cygnus/setup/SetupLoader.java @@ -1,6 +1,7 @@ package net.onelitefeather.cygnus.setup; import me.lucko.luckperms.minestom.loader.MinestomLoader; +import net.hollowcube.minestom.extensions.ExtensionBootstrap; import net.minestom.server.MinecraftServer; import net.onelitefeather.cygnus.common.bootstrap.ServiceBootstrap; import net.onelitefeather.cygnus.setup.player.SetupPlayerProvider; @@ -8,12 +9,15 @@ public class SetupLoader { static void main() { - MinecraftServer minecraftServer = MinecraftServer.init(); + // minestom-ce-extensions loads platform extensions - the CloudNet bridge and our + // :bridge permission extension among them - from the extensions/ folder. Running + // standalone simply loads none. This also performs MinecraftServer.init(). + ExtensionBootstrap bootstrap = ExtensionBootstrap.init(); MinestomLoader.get().load().registerShutdownHook().start(); new SetupExtension(); MinecraftServer.getConnectionManager().setPlayerProvider(new SetupPlayerProvider()); ServiceBootstrap.installShutdownHandling(); - minecraftServer.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); + bootstrap.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); } private SetupLoader() { From dee2d65b0fab9294f3116d8e8b82f7d2b445af59 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Tue, 4 Aug 2026 13:28:18 +0200 Subject: [PATCH 2/2] docs(cloudnet): add deployment guide for the game and setup services Covers the artifacts, how the node launches a service, the service directory layout for both services, why both extension jars are required, and how to verify a deployment. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GsZ8rsCxv99T6SfC1zBpip --- docs/cloudnet-deployment.md | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/cloudnet-deployment.md diff --git a/docs/cloudnet-deployment.md b/docs/cloudnet-deployment.md new file mode 100644 index 00000000..205d8628 --- /dev/null +++ b/docs/cloudnet-deployment.md @@ -0,0 +1,81 @@ +# CloudNet deployment + +How to deploy the `game` and `setup` services as CloudNet services. + +## Artifacts + +| Artifact | Maven | Goes to | +|---|---|---| +| `cygnus.jar` | `cygnus-game` | service root (application file) | +| `setup.jar` | `cygnus-setup` | service root (application file) | +| `bridge.jar` | `cygnus-bridge` | `extensions/` | + +## How CloudNet starts the service + +``` +java … -javaagent: + -Dservice.bind.host= -Dservice.bind.port= + -cp : +``` + +- The application jar is launched through `-cp` plus its manifest `Main-Class`, not through `-jar`. Both jars end + up in one classloader, which is why no CloudNet artifact may ever be bundled into the fat jar — everything + CloudNet-related stays `compileOnly`. +- `service.bind.host` / `service.bind.port` are always set by the node. Standalone runs fall back to + `localhost:25565`; no system properties are needed for local testing. +- To stop a service the node writes `end` and then `stop` to stdin. `stop` triggers a clean shutdown; + `end` is not a registered command and is ignored. + +## Service directory layout + +Everything except `data/` belongs in the CloudNet template. `data/` is created by LuckPerms on first start and +holds its config and H2 database — it is per service and must not be shared between services. + +``` +/ / +├── cygnus.jar ├── setup.jar +├── extensions/ ├── extensions/ +│ ├── CloudNet-Bridge.jar │ ├── CloudNet-Bridge.jar +│ └── bridge.jar │ └── bridge.jar +├── game/maps//… ├── setup/maps//… +└── data/ └── data/ +``` + +Map paths are resolved relative to the working directory and are hardcoded: `game/maps` for the game service, +`setup/maps` for the setup service. A game map needs a `region/` folder and a `map.json`; the setup service only +requires `region/`. + +## Extensions + +Both jars in `extensions/` are required for the CloudNet integration to be complete: + +- **`CloudNet-Bridge.jar`** — CloudNet's own bridge. Provides the player manager, service info updates and + fallback handling. +- **`bridge.jar`** — registers a permission checker that resolves through Adventure's `PermissionChecker` + pointer, which our player implementation backs with LuckPerms. Without it CloudNet falls back to a checker that + only inspects the Minestom permission level, which is always `0` here — maintenance bypass and task-level + `requiredPermission` would then reject every player, staff included. + +`bridge.jar` declares a dependency on `CloudNet_Bridge`. If the CloudNet bridge is missing, it is skipped with a +log message and the server still starts normally. That is also what happens when running locally without CloudNet. + +The name of the application file (`cygnus.jar` / `setup.jar`) is defined by the Minestom service environment, not +by this repository. Check the environment registration if the node cannot find the application file. + +## Verifying a deployment + +1. Start one service and check the log. `CygnusCloudNetPermissions requires an extension called CloudNet_Bridge` + means the CloudNet bridge is missing from `extensions/` and permission checks are not wired up. +2. Run `stop ` on the node. The process must exit on its own instead of being killed after a timeout — + the log ends with `[cygnus-shutdown] Stopping Minestom server` and `[luckperms-shutdown-hook] Goodbye!`. +3. Put the task into maintenance and join with an account holding `cloudnet.bridge.maintenance`. Getting through + confirms the whole permission chain; being kicked points back to step 1. + +## Running locally + +``` +java -jar build/libs/cygnus.jar +``` + +Binds `localhost:25565`. Run it from a directory that contains `game/maps` (or `setup/maps`). `stop` on the +console shuts it down. No CloudNet, no extensions and no system properties are required.