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/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. 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() {