diff --git a/.gitignore b/.gitignore index 1b71ca1..27cc24b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,12 @@ build/ .kotlin/ +# CloudNet extension jars dropped in for local testing (see bridge/README.md) +extensions/ + +# LuckPerms standalone runtime data (generated on boot when running locally) +data/ + # IntelliJ IDEA .idea/ *.iws diff --git a/bridge/README.md b/bridge/README.md new file mode 100644 index 0000000..f268ed7 --- /dev/null +++ b/bridge/README.md @@ -0,0 +1,30 @@ +# Bounce Bridge + +Minestom extension that teaches CloudNet's bridge module how Bounce resolves permissions — +it registers a `MinestomPermissionChecker` backed by Adventure's `PermissionChecker.POINTER`, +the same pointer `PermissionAwarePlayer` installs and LuckPerms answers through. + +## Why this exists + +CloudNet's bridge ships a default permission checker that only inspects +`player.getPermissionLevel()`, which is always `0` on a LuckPerms-managed server. Without this +extension, CloudNet's maintenance-mode bypass and any task-level `requiredPermission` check +reject every player — staff included. + +## Deployment + +This module is never bundled into the game or setup fat jars. Build it and drop the resulting +jar into the CloudNet service's `extensions/` folder, next to the `CloudNet_Bridge` extension +it depends on (declared via `dependencies = "CloudNet_Bridge"` on +`BounceBridgePermissionExtension`, so it will refuse to load before the bridge is present): + +``` +./gradlew :bridge:build +cp bridge/build/libs/bridge-*.jar /extensions/ +``` + +If this jar is missing from a service's `extensions/` folder, the server boots and runs +normally — CloudNet's bridge silently falls back to its `getPermissionLevel()`-based checker, +reproducing the exact maintenance-mode/permission-gate bug this extension exists to fix. There +is currently no log line anywhere that flags a missing extension; treat this file as the +authoritative reminder until that gap is closed. diff --git a/bridge/build.gradle.kts b/bridge/build.gradle.kts new file mode 100644 index 0000000..e477701 --- /dev/null +++ b/bridge/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("bounce.java-conventions") +} + +// 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(platform(libs.minestom.extensions.bom)) + compileOnly(libs.minestom.extensions) + compileOnly(libs.minestom.extensions.processor) + annotationProcessor(platform(libs.minestom.extensions.bom)) + annotationProcessor(libs.minestom.extensions.processor) + + compileOnly(platform(libs.cloudnet.bom)) + compileOnly(libs.cloudnet.driver.api) + compileOnly(libs.cloudnet.bridge) + compileOnly(libs.cloudnet.bridge.impl) +} + +// The annotation processor generates extension.json but cannot know the project version. Subprojects +// do not inherit the root version, so read it from the root project explicitly. +tasks.compileJava { + options.compilerArgs.add("-Aminestom.extension.version=${rootProject.version}") +} diff --git a/bridge/src/main/java/net/theevilreaper/bounce/bridge/BounceBridgePermissionExtension.java b/bridge/src/main/java/net/theevilreaper/bounce/bridge/BounceBridgePermissionExtension.java new file mode 100644 index 0000000..68b3b60 --- /dev/null +++ b/bridge/src/main/java/net/theevilreaper/bounce/bridge/BounceBridgePermissionExtension.java @@ -0,0 +1,45 @@ +package net.theevilreaper.bounce.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; +import net.onelitefeather.minestom.extensions.processor.ExtensionInfo; + +/** + * Minestom extension that teaches the CloudNet bridge how Bounce 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 the {@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. The {@code CloudNet_Bridge} dependency declared below + * 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. + */ +@ExtensionInfo( + name = "BounceCloudNetPermissions", + authors = "theEvilReaper", + dependencies = "CloudNet_Bridge" +) +public final class BounceBridgePermissionExtension 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, "bounce-luckperms", checker) + .markAsDefaultService(); + } + + @Override + public void terminate() { + } +} diff --git a/bridge/src/main/java/net/theevilreaper/bounce/bridge/package-info.java b/bridge/src/main/java/net/theevilreaper/bounce/bridge/package-info.java new file mode 100644 index 0000000..5c71a30 --- /dev/null +++ b/bridge/src/main/java/net/theevilreaper/bounce/bridge/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.bridge; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/build.gradle.kts b/build.gradle.kts index 81263a8..fad46f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,6 @@ description = "Bounce" dependencies { implementation(project(":common")) implementation(platform(libs.aonyx.bom)) - implementation(platform(libs.cloudnet.bom)) implementation(platform(libs.falco.bom)) implementation(project(":block")) implementation(libs.adventure) @@ -20,10 +19,21 @@ dependencies { implementation(libs.pvp) implementation(libs.minestom) implementation(libs.aves) - implementation(libs.bundles.cloudnet) implementation(libs.slf4j.api) + // SLF4J needs a binding at runtime; without one it falls back to NOP and the + // server logs nothing at all. + runtimeOnly(libs.slf4j.simple) implementation(libs.xerus) + implementation(platform(libs.minestom.extensions.bom)) + implementation(libs.minestom.extensions) + + // Guava used to arrive transitively through CloudNet; bundle it explicitly now that CloudNet + // is no longer a direct dependency of this module (it loads as an extension at runtime instead). + implementation(libs.guava) + compileOnly(libs.luckperms.api) + runtimeOnly(libs.luckperms.minestom.loader) + testImplementation(libs.minestom) testImplementation(libs.aves) testImplementation(libs.cyano) @@ -33,6 +43,12 @@ dependencies { testRuntimeOnly(libs.junit.engine) } +// Keeps the loader off the test class path, which is what makes LuckPermsSupport report absent and +// every permission check answer TRUE during tests. +configurations.testRuntimeClasspath { + exclude(group = "net.luckperms", module = "minestom-loader") +} + application { mainClass.set("net.theevilreaper.bounce.BounceServer") } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index dd575bb..647b0d5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -10,8 +10,14 @@ dependencies { compileOnly(libs.minestom) compileOnly(libs.aves) compileOnly(libs.xerus) + compileOnly(libs.luckperms.api) + // Only to compile LuckPermsSupport.bootstrap(). The artifact is shipped by the game and setup + // modules as runtimeOnly - common must not put it on any runtime class path, because its + // absence is exactly what LuckPermsSupport detects. + compileOnly(libs.luckperms.minestom.loader) testImplementation(libs.minestom) + testImplementation(libs.luckperms.api) testImplementation(libs.aves) testImplementation(libs.cyano) testImplementation(libs.junit.api) diff --git a/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrap.java b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrap.java new file mode 100644 index 0000000..5b77975 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrap.java @@ -0,0 +1,88 @@ +package net.theevilreaper.bounce.common.bootstrap; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Wires the parts a CloudNet-managed service process needs: reading the bind address CloudNet + * assigns per-service, and reacting to CloudNet's stdin-based stop signal instead of being killed + * after a timeout. + */ +public final class ServiceBootstrap { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBootstrap.class); + private static final String DEFAULT_BIND_HOST = "localhost"; + private static final int DEFAULT_BIND_PORT = 25565; + private static final String DEFAULT_WORKING_DIR = ""; + + private ServiceBootstrap() { + } + + /** + * Resolves the host to bind the server to. + * + * @return the value of the {@code service.bind.host} system property CloudNet assigns per + * service, or {@value #DEFAULT_BIND_HOST} for standalone (non-CloudNet) runs + */ + public static String resolveBindHost() { + return System.getProperty("service.bind.host", DEFAULT_BIND_HOST); + } + + /** + * Resolves the port to bind the server to. + * + * @return the value of the {@code service.bind.port} system property CloudNet assigns per + * service, or {@value #DEFAULT_BIND_PORT} for standalone (non-CloudNet) runs + */ + public static int resolveBindPort() { + return Integer.getInteger("service.bind.port", DEFAULT_BIND_PORT); + } + + /** + * Resolves the working directory root used to locate config, map, and other data files. + * + * @return the value of the {@code service.working.dir} system property CloudNet assigns per + * service, or the JVM's current working directory for standalone (non-CloudNet) runs + */ + public static Path resolveWorkingDirectory() { + return Paths.get(System.getProperty("service.working.dir", DEFAULT_WORKING_DIR)); + } + + /** + * Registers the {@code StopCommand} and starts a daemon thread reading commands from stdin, + * so CloudNet can stop the service cleanly instead of killing it after a timeout. Should be + * called once during startup, before the server starts accepting connections. + */ + public static void installShutdownHandling() { + MinecraftServer.getCommandManager().register(new StopCommand()); + Thread.ofPlatform().name("bounce-console-input").daemon().start(ServiceBootstrap::listenForConsoleInput); + } + + /** + * Reads lines from {@link System#in} until the stream closes and executes each one as a + * console command. This is what lets CloudNet's stdin-based {@code stop} signal reach the + * {@code StopCommand}. + */ + private static void listenForConsoleInput() { + CommandManager commandManager = MinecraftServer.getCommandManager(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isBlank()) continue; + String command = line; + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> commandManager.execute(commandManager.getConsoleSender(), command)); + } + } catch (IOException e) { + LOGGER.error("Failed to read command from stdin", e); + } + } +} diff --git a/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/StopCommand.java b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/StopCommand.java new file mode 100644 index 0000000..37392a9 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/StopCommand.java @@ -0,0 +1,50 @@ +package net.theevilreaper.bounce.common.bootstrap; + +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; +import net.theevilreaper.bounce.common.permission.LuckPermsSupport; + +/** + * Shuts the service down cleanly. Reserved for non-player senders (the console/CloudNet) and + * players holding {@value #PERMISSION}, since a service should not be stoppable by regular players. + *

+ * Unlike every other permission check in this codebase, this command does NOT fall back to + * granting access when LuckPerms is absent from the class path — a joining player being able to + * stop a live service is a materially larger risk than the general "grant everything locally" + * fallback other permissions rely on for local/test runs. In that mode, only non-player senders + * (console, CloudNet's stdin stop signal) can run this command. + */ +public final class StopCommand extends Command { + + private static final String PERMISSION = "bounce.command.stop"; + + /** + * Creates a new instance of the {@link StopCommand} and wires its condition and executor. + */ + public StopCommand() { + super("stop"); + setCondition((sender, commandString) -> !(sender instanceof Player) || (LuckPermsSupport.isPresent() && hasStopPermission(sender))); + setDefaultExecutor((sender, context) -> Thread.ofPlatform().name("bounce-shutdown").start(() -> { + MinecraftServer.stopCleanly(); + System.exit(0); + })); + } + + /** + * Checks whether the given sender is allowed to run this command. + *

+ * Reads Adventure's {@link PermissionChecker#POINTER}, which the player implementation backs + * with LuckPerms (see {@code PermissionAwarePlayer}). A sender without that pointer is denied. + * + * @param sender the sender to check + * @return {@code true} if the sender holds {@value #PERMISSION}, {@code false} otherwise + */ + 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/theevilreaper/bounce/common/bootstrap/package-info.java b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/package-info.java new file mode 100644 index 0000000..52dbbda --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/bootstrap/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.common.bootstrap; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/common/src/main/java/net/theevilreaper/bounce/common/permission/LuckPermsSupport.java b/common/src/main/java/net/theevilreaper/bounce/common/permission/LuckPermsSupport.java new file mode 100644 index 0000000..0608a88 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/permission/LuckPermsSupport.java @@ -0,0 +1,105 @@ +package net.theevilreaper.bounce.common.permission; + +import me.lucko.luckperms.minestom.loader.MinestomLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Decides whether Bounce runs with LuckPerms. + *

+ * Minestom has no plugin folder, so LuckPerms is bootstrapped from {@code main} and shipped inside + * the fat jar. A class path that leaves the loader out - the test class path does exactly that - + * runs without any permission backend, and {@code PermissionAwarePlayer} then grants every + * permission instead of failing on {@code LuckPermsProvider.get()}. + *

+ * Detection reads the class path once and caches the answer, so a permission check never pays for + * it twice. + */ +public final class LuckPermsSupport { + + private static final Logger LOGGER = LoggerFactory.getLogger(LuckPermsSupport.class); + private static final String LOADER_CLASS = "me.lucko.luckperms.minestom.loader.MinestomLoader"; + private static final boolean PRESENT = detect(); + private static final AtomicBoolean FALLBACK_GRANT_LOGGED = new AtomicBoolean(false); + private static final AtomicBoolean BROKEN_PROVIDER_LOGGED = new AtomicBoolean(false); + + /** + * Returns whether LuckPerms can be used. + * + * @return {@code true} if the LuckPerms loader is on the class path + */ + public static boolean isPresent() { + return PRESENT; + } + + /** + * Logs, once, that the fallback is actually granting permissions. + *

+ * The startup WARN from {@link #detect()} is easy to miss underneath Minestom's own boot + * output, so this leaves a second trace at the moment the fallback first does something + * observable: a real permission check resolving to {@link net.kyori.adventure.util.TriState#TRUE} + * purely because LuckPerms is absent. Guarded by a single atomic compare-and-set so repeated + * calls cost one volatile read and nothing else. + * + * @param permission the permission node that triggered the fallback + */ + public static void noteFallbackGrant(String permission) { + if (FALLBACK_GRANT_LOGGED.compareAndSet(false, true)) { + LOGGER.warn("Granting '{}' unconditionally because LuckPerms is absent from the class path. " + + "Every subsequent permission check does the same; this line only prints once.", permission); + } + } + + /** + * Logs, once, that LuckPerms is on the class path but its provider never started - every + * permission check fails closed instead of throwing. + * + * @param permission the permission node that triggered the failure + * @param cause the exception {@link net.luckperms.api.LuckPermsProvider#get()} threw + */ + public static void noteBrokenProvider(String permission, Throwable cause) { + if (BROKEN_PROVIDER_LOGGED.compareAndSet(false, true)) { + LOGGER.error("LuckPerms is on the class path but its provider never started; denying '{}' and every " + + "subsequent permission check instead of throwing. This line only prints once.", permission, cause); + } + } + + /** + * Starts LuckPerms and registers its shutdown hook. Does nothing when LuckPerms is absent. + */ + public static void bootstrap() { + if (!PRESENT) { + return; + } + startLuckPerms(); + } + + /** + * Starts LuckPerms. Kept separate so resolving {@link MinestomLoader} cannot happen while + * {@link #bootstrap()} itself is being verified. + */ + private static void startLuckPerms() { + MinestomLoader.get().load().registerShutdownHook().start(); + } + + /** + * Resolves the loader class without initialising it. + * + * @return {@code true} if the class is available, {@code false} after logging a warning + */ + private static boolean detect() { + try { + Class.forName(LOADER_CLASS, false, LuckPermsSupport.class.getClassLoader()); + return true; + } catch (ClassNotFoundException exception) { + LOGGER.warn("LuckPerms is not on the class path. Every permission check resolves to TRUE. " + + "This mode is meant for local runs and tests, never for production."); + return false; + } + } + + private LuckPermsSupport() { + } +} diff --git a/common/src/main/java/net/theevilreaper/bounce/common/permission/TriStates.java b/common/src/main/java/net/theevilreaper/bounce/common/permission/TriStates.java new file mode 100644 index 0000000..b8c263a --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/permission/TriStates.java @@ -0,0 +1,28 @@ +package net.theevilreaper.bounce.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. + */ +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/theevilreaper/bounce/common/permission/package-info.java b/common/src/main/java/net/theevilreaper/bounce/common/permission/package-info.java new file mode 100644 index 0000000..fd47694 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/permission/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.common.permission; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/common/src/main/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayer.java b/common/src/main/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayer.java new file mode 100644 index 0000000..a3d4854 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayer.java @@ -0,0 +1,81 @@ +package net.theevilreaper.bounce.common.player; + +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.pointer.Pointers; +import net.kyori.adventure.util.TriState; +import net.luckperms.api.LuckPerms; +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.theevilreaper.bounce.common.permission.LuckPermsSupport; +import net.theevilreaper.bounce.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, {@code StopCommand}, 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. + *

+ * Without LuckPerms on the class path every check answers {@link TriState#TRUE} instead, so local + * runs and tests reach permission-gated paths at all. See {@link LuckPermsSupport}. + */ +public abstract class PermissionAwarePlayer extends Player implements PermissionChecker { + + private final Pointers pointers = 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 {@link TriState#TRUE} when LuckPerms is absent, the value LuckPerms holds for the + * node otherwise, or {@link TriState#FALSE} when LuckPerms has no user data for this player + */ + @Override + public @NotNull TriState value(@NotNull String permission) { + if (!LuckPermsSupport.isPresent()) { + LuckPermsSupport.noteFallbackGrant(permission); + return TriState.TRUE; + } + try { + LuckPerms luckPerms = LuckPermsProvider.get(); + User user = luckPerms.getUserManager().getUser(getUuid()); + if (user == null) { + return TriState.FALSE; + } + QueryOptions queryOptions = luckPerms.getContextManager().getQueryOptions(this); + return TriStates.fromLuckPerms(user.getCachedData().getPermissionData(queryOptions).checkPermission(permission)); + } catch (IllegalStateException exception) { + LuckPermsSupport.noteBrokenProvider(permission, exception); + return TriState.FALSE; + } + } +} diff --git a/common/src/main/java/net/theevilreaper/bounce/common/player/package-info.java b/common/src/main/java/net/theevilreaper/bounce/common/player/package-info.java new file mode 100644 index 0000000..d6007a2 --- /dev/null +++ b/common/src/main/java/net/theevilreaper/bounce/common/player/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.common.player; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrapTest.java b/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrapTest.java new file mode 100644 index 0000000..6caf1fc --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/ServiceBootstrapTest.java @@ -0,0 +1,75 @@ +package net.theevilreaper.bounce.common.bootstrap; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; + +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ServiceBootstrapTest { + + private String originalBindHost; + private String originalBindPort; + private String originalWorkingDir; + + @BeforeEach + void captureSystemProperties() { + originalBindHost = System.getProperty("service.bind.host"); + originalBindPort = System.getProperty("service.bind.port"); + originalWorkingDir = System.getProperty("service.working.dir"); + } + + @AfterEach + void restoreSystemProperties() { + restoreOrClear("service.bind.host", originalBindHost); + restoreOrClear("service.bind.port", originalBindPort); + restoreOrClear("service.working.dir", originalWorkingDir); + } + + private static void restoreOrClear(String key, String originalValue) { + if (originalValue == null) { + System.clearProperty(key); + } else { + System.setProperty(key, originalValue); + } + } + + @Test + @DisabledIfSystemProperty(named = "service.bind.host", matches = ".+") + void testDefaultBindHost() { + assertEquals("localhost", ServiceBootstrap.resolveBindHost()); + } + + @Test + @DisabledIfSystemProperty(named = "service.bind.port", matches = ".+") + void testDefaultBindPort() { + assertEquals(25565, ServiceBootstrap.resolveBindPort()); + } + + @Test + void testBindHostFromSystemProperty() { + System.setProperty("service.bind.host", "0.0.0.0"); + assertEquals("0.0.0.0", ServiceBootstrap.resolveBindHost()); + } + + @Test + void testBindPortFromSystemProperty() { + System.setProperty("service.bind.port", "30000"); + assertEquals(30000, ServiceBootstrap.resolveBindPort()); + } + + @Test + @DisabledIfSystemProperty(named = "service.working.dir", matches = ".+") + void testDefaultWorkingDirectory() { + assertEquals(Paths.get(""), ServiceBootstrap.resolveWorkingDirectory()); + } + + @Test + void testWorkingDirectoryFromSystemProperty() { + System.setProperty("service.working.dir", "/app"); + assertEquals(Paths.get("/app"), ServiceBootstrap.resolveWorkingDirectory()); + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/StopCommandTest.java b/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/StopCommandTest.java new file mode 100644 index 0000000..ced76fd --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/bootstrap/StopCommandTest.java @@ -0,0 +1,63 @@ +package net.theevilreaper.bounce.common.bootstrap; + +import net.minestom.server.command.CommandSender; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import net.theevilreaper.bounce.common.player.PermissionAwarePlayer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MicrotusExtension.class) +class StopCommandTest { + + @BeforeAll + static void setUp(Env env) { + env.process().connection().setPlayerProvider(TestPlayer::new); + } + + @Test + void testCommandName() { + StopCommand command = new StopCommand(); + assertEquals("stop", command.getName()); + } + + @Test + void testConsoleSenderIsAlwaysAllowed(@NotNull Env env) { + StopCommand command = new StopCommand(); + CommandSender consoleSender = env.process().command().getConsoleSender(); + + assertTrue(command.getCondition().canUse(consoleSender, "stop")); + } + + @Test + void testPlayerIsDeniedWithoutLuckPerms(@NotNull Env env) { + StopCommand command = new StopCommand(); + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + assertFalse(command.getCondition().canUse(player, "stop")); + + env.destroyInstance(instance, true); + } + + /** + * A player which adds nothing to {@link PermissionAwarePlayer}, so the command sees the same + * pointer a real Bounce player carries. + */ + private static final class TestPlayer extends PermissionAwarePlayer { + + private TestPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/permission/LuckPermsSupportTest.java b/common/src/test/java/net/theevilreaper/bounce/common/permission/LuckPermsSupportTest.java new file mode 100644 index 0000000..66bc14f --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/permission/LuckPermsSupportTest.java @@ -0,0 +1,40 @@ +package net.theevilreaper.bounce.common.permission; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Pins the assumption every other test relies on: the LuckPerms loader is kept off the test class + * path, so this module runs in its LuckPerms-free mode while tests execute. + */ +class LuckPermsSupportTest { + + @Test + void testLoaderIsAbsentDuringTests() { + assertFalse(LuckPermsSupport.isPresent(), "The test class path must not carry the LuckPerms loader"); + } + + @Test + void testBootstrapIsSilentWithoutLoader() { + assertDoesNotThrow(LuckPermsSupport::bootstrap); + } + + @Test + void testNoteFallbackGrantDoesNotThrowOnRepeatedCalls() { + assertDoesNotThrow(() -> { + LuckPermsSupport.noteFallbackGrant("bounce.test"); + LuckPermsSupport.noteFallbackGrant("bounce.test"); + LuckPermsSupport.noteFallbackGrant("bounce.other"); + }); + } + + @Test + void testNoteBrokenProviderDoesNotThrowOnRepeatedCalls() { + assertDoesNotThrow(() -> { + LuckPermsSupport.noteBrokenProvider("bounce.test", new IllegalStateException("no provider")); + LuckPermsSupport.noteBrokenProvider("bounce.test", new IllegalStateException("no provider")); + }); + } +} diff --git a/common/src/test/java/net/theevilreaper/bounce/common/permission/TriStatesTest.java b/common/src/test/java/net/theevilreaper/bounce/common/permission/TriStatesTest.java new file mode 100644 index 0000000..be689c0 --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/permission/TriStatesTest.java @@ -0,0 +1,34 @@ +package net.theevilreaper.bounce.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/common/src/test/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayerIntegrationTest.java b/common/src/test/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayerIntegrationTest.java new file mode 100644 index 0000000..7ab4db5 --- /dev/null +++ b/common/src/test/java/net/theevilreaper/bounce/common/player/PermissionAwarePlayerIntegrationTest.java @@ -0,0 +1,82 @@ +package net.theevilreaper.bounce.common.player; + +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.TriState; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.minestom.testing.Env; +import net.minestom.testing.extension.MicrotusExtension; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Verifies that a player answers permission questions without LuckPerms present, which is the state + * every test run is in. + */ +@ExtendWith(MicrotusExtension.class) +class PermissionAwarePlayerIntegrationTest { + + @BeforeAll + static void setUp(Env env) { + env.process().connection().setPlayerProvider(TestPlayer::new); + } + + @Test + void testPermissionIsGrantedWithoutLuckPerms(Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + PermissionAwarePlayer permissionAware = assertInstanceOf(PermissionAwarePlayer.class, player); + assertEquals(TriState.TRUE, permissionAware.value("bounce.test")); + + env.destroyInstance(instance, true); + } + + @Test + void testRepeatedFallbackChecksDoNotThrow(Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + PermissionAwarePlayer permissionAware = assertInstanceOf(PermissionAwarePlayer.class, player); + assertDoesNotThrow(() -> { + for (int i = 0; i < 5; i++) { + assertEquals(TriState.TRUE, permissionAware.value("bounce.test")); + assertEquals(TriState.TRUE, permissionAware.value("bounce.other")); + } + }); + + env.destroyInstance(instance, true); + } + + @Test + void testPointerResolvesThroughTheSamePath(Env env) { + Instance instance = env.createFlatInstance(); + Player player = env.createPlayer(instance); + + PermissionChecker checker = player.getOrDefault( + PermissionChecker.POINTER, + PermissionChecker.always(TriState.FALSE) + ); + assertEquals(TriState.TRUE, checker.value("bounce.test")); + + env.destroyInstance(instance, true); + } + + /** + * A player which adds nothing to {@link PermissionAwarePlayer}, so the test observes the + * permission handling of the base class and nothing else. + */ + private static final class TestPlayer extends PermissionAwarePlayer { + + private TestPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f45f01f..56a9e34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,10 @@ dependencyResolutionManagement { mavenCentral() maven("https://central.sonatype.com/repository/maven-snapshots/") maven("https://repository.derklaro.dev/snapshots/") + maven { + name = "OneLiteFeatherReleases" + url = uri("https://repo.onelitefeather.dev/releases") + } maven { name = "OneLiteFeatherRepository" url = uri("https://repo.onelitefeather.dev/onelitefeather") @@ -30,6 +34,10 @@ dependencyResolutionManagement { version("cloudnet", "4.0.0-RC18-SNAPSHOT") version("slf4j", "2.0.18") version("falco", "2.1.0") + version("luckperms", "5.5") + version("luckperms-minestom-loader", "5.6-SNAPSHOT") + version("minestom-extensions", "2.1.1") + version("guava", "33.7.1-jre") library("aonyx.bom", "net.onelitefeather", "aonyx-bom").versionRef("aonyx") @@ -37,6 +45,7 @@ dependencyResolutionManagement { library("falco.anvil", "net.onelitefeather", "falco-anvil").withoutVersion() library("slf4j.api", "org.slf4j", "slf4j-api").versionRef("slf4j") + library("slf4j.simple", "org.slf4j", "slf4j-simple").versionRef("slf4j") library("pvp", "io.github.togar2", "MinestomPvP").versionRef("pvp") library("minestom", "net.minestom", "minestom").withoutVersion() library("adventure", "net.kyori", "adventure-text-minimessage").withoutVersion() @@ -48,8 +57,16 @@ dependencyResolutionManagement { library("junit.params", "org.junit.jupiter", "junit-jupiter-params").withoutVersion() library("aves", "net.theevilreaper", "aves").withoutVersion() library("xerus", "net.theevilreaper", "xerus").withoutVersion() + library("luckperms.api", "net.luckperms", "api").versionRef("luckperms") + library("luckperms.minestom.loader", "net.luckperms", "minestom-loader").versionRef("luckperms-minestom-loader") + library("guava", "com.google.guava", "guava").versionRef("guava") + + library("minestom-extensions-bom", "net.onelitefeather", "minestom-extensions-bom").versionRef("minestom-extensions") + library("minestom-extensions", "net.onelitefeather", "minestom-extensions").withoutVersion() + library("minestom-extensions-processor", "net.onelitefeather", "minestom-extensions-processor").withoutVersion() library("cloudnet-bom", "eu.cloudnetservice.cloudnet", "bom").versionRef("cloudnet") + library("cloudnet-driver-api", "eu.cloudnetservice.cloudnet", "driver-api").withoutVersion() library("cloudnet-bridge", "eu.cloudnetservice.cloudnet", "bridge-api").withoutVersion() library("cloudnet-bridge-impl", "eu.cloudnetservice.cloudnet", "bridge-impl").withoutVersion() library("cloudnet-driver-impl", "eu.cloudnetservice.cloudnet", "driver-impl").withoutVersion() @@ -57,21 +74,11 @@ dependencyResolutionManagement { library("cloudnet-jvm-wrapper", "eu.cloudnetservice.cloudnet", "wrapper-jvm-api").withoutVersion() plugin("shadow", "com.gradleup.shadow").versionRef("shadow") - - bundle( - "cloudnet", - listOf( - "cloudnet-bridge", - "cloudnet-bridge-impl", - "cloudnet-driver-impl", - "cloudnet-platform-inject", - "cloudnet-jvm-wrapper" - ) - ) } } } include("common") include("setup") -include("block") \ No newline at end of file +include("block") +include("bridge") \ No newline at end of file diff --git a/setup/build.gradle.kts b/setup/build.gradle.kts index 3871811..c90f354 100644 --- a/setup/build.gradle.kts +++ b/setup/build.gradle.kts @@ -18,9 +18,19 @@ dependencies { implementation(libs.minestom) implementation(libs.aves) implementation(libs.slf4j.api) + // SLF4J needs a binding at runtime; without one it falls back to NOP and the + // server logs nothing at all. + runtimeOnly(libs.slf4j.simple) implementation(libs.xerus) implementation(libs.guira) + implementation(platform(libs.minestom.extensions.bom)) + implementation(libs.minestom.extensions) + + implementation(libs.guava) + compileOnly(libs.luckperms.api) + runtimeOnly(libs.luckperms.minestom.loader) + testImplementation(libs.minestom) testImplementation(libs.aves) testImplementation(libs.cyano) @@ -30,6 +40,12 @@ dependencies { testRuntimeOnly(libs.junit.engine) } +// Keeps the loader off the test class path, which is what makes LuckPermsSupport report absent and +// every permission check answer TRUE during tests. +configurations.testRuntimeClasspath { + exclude(group = "net.luckperms", module = "minestom-loader") +} + application { mainClass.set("net.theevilreaper.bounce.BounceSetupServer") } diff --git a/setup/src/main/java/net/theevilreaper/bounce/BounceSetupServer.java b/setup/src/main/java/net/theevilreaper/bounce/BounceSetupServer.java index a3ebe73..9ac1164 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/BounceSetupServer.java +++ b/setup/src/main/java/net/theevilreaper/bounce/BounceSetupServer.java @@ -1,15 +1,21 @@ package net.theevilreaper.bounce; -import net.minestom.server.MinecraftServer; +import net.hollowcube.minestom.extensions.ExtensionBootstrap; +import net.theevilreaper.bounce.common.bootstrap.ServiceBootstrap; +import net.theevilreaper.bounce.common.permission.LuckPermsSupport; import net.theevilreaper.bounce.setup.BounceSetup; public class BounceSetupServer { static void main() { - MinecraftServer minecraftServer = MinecraftServer.init(); + // minestom-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(); + LuckPermsSupport.bootstrap(); BounceSetup bounceSetup = new BounceSetup(); bounceSetup.initialize(); - - minecraftServer.start("localhost", 25565); + ServiceBootstrap.installShutdownHandling(); + bootstrap.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); } } diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java index 2d53f03..48375f9 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/BounceSetup.java @@ -11,6 +11,7 @@ import net.onelitefeather.guira.event.SetupFinishEvent; import net.theevilreaper.aves.util.functional.PlayerConsumer; import net.theevilreaper.bounce.common.ListenerHandling; +import net.theevilreaper.bounce.common.bootstrap.ServiceBootstrap; import net.theevilreaper.bounce.setup.command.GameModeCommand; import net.theevilreaper.bounce.setup.command.SetupCommand; import net.theevilreaper.bounce.setup.dialog.DialogRegistry; @@ -43,6 +44,7 @@ import net.theevilreaper.bounce.setup.listener.push.PlayerPushIndexChangeListener; import net.theevilreaper.bounce.setup.listener.state.GameMapBuilderStateNotifyListener; import net.theevilreaper.bounce.setup.map.SetupMapProvider; +import net.theevilreaper.bounce.setup.player.SetupPlayer; import net.theevilreaper.bounce.setup.util.SetupItems; import org.jetbrains.annotations.NotNull; @@ -59,12 +61,13 @@ public final class BounceSetup implements ListenerHandling { private final DialogRegistry dialogRegistry; public BounceSetup() { - Path path = Path.of(""); + Path path = ServiceBootstrap.resolveWorkingDirectory(); this.mapProvider = new SetupMapProvider(path); this.setupDataService = SetupDataService.create(); this.inventoryService = new InventoryService(this.mapProvider::getEntries); this.dialogRegistry = new SetupDialogRegistry(); MinecraftServer.getSchedulerManager().buildShutdownTask(this::onShutdown); + MinecraftServer.getConnectionManager().setPlayerProvider(SetupPlayer::new); } public void initialize() { diff --git a/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java b/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java index fbf2edf..5491792 100644 --- a/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java +++ b/setup/src/main/java/net/theevilreaper/bounce/setup/player/SetupPlayer.java @@ -1,33 +1,12 @@ package net.theevilreaper.bounce.setup.player; -import net.minestom.server.coordinate.BlockVec; -import net.minestom.server.entity.Player; +import net.theevilreaper.bounce.common.player.PermissionAwarePlayer; import net.minestom.server.network.player.GameProfile; import net.minestom.server.network.player.PlayerConnection; -import org.jetbrains.annotations.Nullable; -public class SetupPlayer extends Player { - - private @Nullable BlockVec leftCorner; - private @Nullable BlockVec rightCorner; +public class SetupPlayer extends PermissionAwarePlayer { public SetupPlayer(PlayerConnection playerConnection, GameProfile gameProfile) { super(playerConnection, gameProfile); } - - public void setLeftCorner(@Nullable BlockVec leftCorner) { - this.leftCorner = leftCorner; - } - - public void setRightCorner(@Nullable BlockVec rightCorner) { - this.rightCorner = rightCorner; - } - - public @Nullable BlockVec getRightCorner() { - return rightCorner; - } - - public @Nullable BlockVec getLeftCorner() { - return leftCorner; - } } diff --git a/src/main/java/net/theevilreaper/bounce/Bounce.java b/src/main/java/net/theevilreaper/bounce/Bounce.java index d808721..6a862ca 100644 --- a/src/main/java/net/theevilreaper/bounce/Bounce.java +++ b/src/main/java/net/theevilreaper/bounce/Bounce.java @@ -19,6 +19,7 @@ import net.theevilreaper.bounce.block.type.lantern.LanternBlockFactory; import net.theevilreaper.bounce.commands.StartCommand; import net.theevilreaper.bounce.common.ListenerHandling; +import net.theevilreaper.bounce.common.bootstrap.ServiceBootstrap; import net.theevilreaper.bounce.common.config.GameConfig; import net.theevilreaper.bounce.common.config.GameConfigReader; import net.theevilreaper.bounce.event.BounceGameFinishEvent; @@ -37,6 +38,7 @@ import net.theevilreaper.bounce.listener.game.PlayerLavaListener; import net.theevilreaper.bounce.listener.game.ScoreUpdateListener; import net.theevilreaper.bounce.map.BounceMapProvider; +import net.theevilreaper.bounce.player.BouncePlayer; import net.theevilreaper.bounce.profile.BounceProfile; import net.theevilreaper.bounce.profile.ProfileService; import net.theevilreaper.bounce.timer.PlayingPhase; @@ -49,7 +51,6 @@ import net.theevilreaper.xerus.api.phase.Phase; import java.nio.file.Path; -import java.nio.file.Paths; public class Bounce implements ListenerHandling { @@ -61,7 +62,7 @@ public class Bounce implements ListenerHandling { private final PlayerUtil playerUtil; public Bounce() { - Path path = Paths.get(""); + Path path = ServiceBootstrap.resolveWorkingDirectory(); this.gameConfig = new GameConfigReader(path.resolve("config")).getConfig(); this.mapProvider = new BounceMapProvider(path); this.phaseSeries = new LinearPhaseSeries<>("Game"); @@ -71,6 +72,7 @@ public Bounce() { this.registerPhases(); MinecraftServer.getSchedulerManager().buildShutdownTask(this::unload); + MinecraftServer.getConnectionManager().setPlayerProvider(BouncePlayer::new); } public void load() { diff --git a/src/main/java/net/theevilreaper/bounce/BounceServer.java b/src/main/java/net/theevilreaper/bounce/BounceServer.java index 336e36a..525d948 100644 --- a/src/main/java/net/theevilreaper/bounce/BounceServer.java +++ b/src/main/java/net/theevilreaper/bounce/BounceServer.java @@ -1,28 +1,26 @@ package net.theevilreaper.bounce; -import dev.derklaro.aerogel.Injector; -import eu.cloudnetservice.driver.inject.InjectionLayer; -import eu.cloudnetservice.modules.bridge.impl.platform.minestom.MinestomBridgeExtension; import io.github.togar2.pvp.MinestomPvP; -import net.minestom.server.MinecraftServer; +import net.hollowcube.minestom.extensions.ExtensionBootstrap; +import net.theevilreaper.bounce.common.bootstrap.ServiceBootstrap; +import net.theevilreaper.bounce.common.permission.LuckPermsSupport; /** - * Initializes some necessary components and starts the {@link MinecraftServer} which is required for the game to run. - * - * @version 1.0.0 - * @since .1.0 - * @author theEvilReaper + * Initializes some necessary components and starts the {@link net.minestom.server.MinecraftServer} + * which is required for the game to run. */ public class BounceServer { static void main() { - MinecraftServer minecraftServer = MinecraftServer.init(); + // minestom-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(); + LuckPermsSupport.bootstrap(); MinestomPvP.init(); Bounce bounce = new Bounce(); bounce.load(); - try (InjectionLayer layer = InjectionLayer.ext()) { - layer.instance(MinestomBridgeExtension.class).onLoad(); - } - minecraftServer.start("localhost", 25565); + ServiceBootstrap.installShutdownHandling(); + bootstrap.start(ServiceBootstrap.resolveBindHost(), ServiceBootstrap.resolveBindPort()); } } diff --git a/src/main/java/net/theevilreaper/bounce/player/BouncePlayer.java b/src/main/java/net/theevilreaper/bounce/player/BouncePlayer.java new file mode 100644 index 0000000..7b222e0 --- /dev/null +++ b/src/main/java/net/theevilreaper/bounce/player/BouncePlayer.java @@ -0,0 +1,17 @@ +package net.theevilreaper.bounce.player; + +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; +import net.theevilreaper.bounce.common.player.PermissionAwarePlayer; + +/** + * The {@link net.minestom.server.entity.Player} implementation used for the actual game. Currently + * adds nothing beyond {@link PermissionAwarePlayer}'s LuckPerms-backed permission checks; a hook + * for game-specific player state later. + */ +public final class BouncePlayer extends PermissionAwarePlayer { + + public BouncePlayer(PlayerConnection playerConnection, GameProfile gameProfile) { + super(playerConnection, gameProfile); + } +} diff --git a/src/main/java/net/theevilreaper/bounce/player/package-info.java b/src/main/java/net/theevilreaper/bounce/player/package-info.java new file mode 100644 index 0000000..e544a9b --- /dev/null +++ b/src/main/java/net/theevilreaper/bounce/player/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.theevilreaper.bounce.player; + +import org.jetbrains.annotations.NotNullByDefault;