Repository navigation
refactor(bootstrap)!: make minestom extensions a platform module - #407
Merged
Merged
Conversation
TheMeinerLP
force-pushed
the
fix/close-scope-on-failed-start
branch
from
October 10, 2026 21:08
e13d340 to
a4b0ebc
Compare
TheMeinerLP
changed the base branch from
fix/close-scope-on-failed-start
to
main
October 10, 2026 22:08
…built TitanApplication called minestom-extensions directly, so runtime depended on the extension loader and the server init order was only implicit. A ServerBootstrap contract in core is chosen through ServiceLoader (plain Minestom without a platform), initialised, then the scope is built and the server started (TitanStartup). Start-up settings are read through pure functions. Claude-Session: https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1
The extension loader moves to platform/extensions and is selected by variant. apps/cloudnet includes it with the CloudNet deliver (platform/cloudnet, primary bean chosen by the .wrapper directory); apps/local starts plain Minestom and no longer loads extensions/. runtime keeps a secondary debug deliver for a server without a platform. TracedDeliver moves to common so both sides wrap the deliver the same way. BREAKING CHANGE: only the apps/cloudnet variant loads the extensions folder; apps/local starts plain Minestom without the extension loader. Claude-Session: https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1
Integer.getInteger decodes the value (hex and octal literals), but the bind port was parsed as decimal only. Use Integer.decode so a configured port like 0x9C41 binds the same as before. Claude-Session: https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1
loadedExtensions() asked the extension manager, which throws until init(). The ServerBootstrap contract says an empty list before init, so return it without touching the manager. Claude-Session: https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1
TheMeinerLP
force-pushed
the
refactor/optional-extensions-bootstrap
branch
from
October 10, 2026 22:14
21e772d to
0e3ef94
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR
This PR is stacked on #403 (
fix(runtime): close the bean scope when the startup check fails), branchfix/close-scope-on-failed-start. The diff against that branch is only this change. Merge #403 first, or retarget this PR tomainafter it lands.What
Minestom is no longer a hard dependency of the lobby bootstrap. A variant now gets exactly one
ServerBootstrap, chosen at start:core: newServerBootstrapcontract (name,init(Auth),start(host, port),loadedExtensions()).runtime:ServerBootstraps.selectpicks the one candidate, plain Minestom (PlainMinestomBootstrap) when there is none, and aborts naming all candidates when there are several.TitanStartupfixes the order: init the server, install the exception handler, build the Avaje scope, start the server.platform/extensions(new):ExtensionServerBootstraponminestom-extensions, registered throughMETA-INF/services.platform/cloudnet(new): the CloudNet deliver (MessageChannelDeliver), chosen by the.wrapperdirectory and marked@Primary.runtimekeepsDebugDeliveras a@Secondaryfallback.TracedDelivermoves tocommonso both sides wrap the deliver the same way.apps/cloudnetincludesextensions,cloudnetandluckperms.apps/localincludes none of them.platform/luckpermsreads the loaded extensions from the injectedServerBootstrapand no longer depends onminestom-extensions.Server bootstrap: <name>and the per-variant behaviour.bootstrap.start(...)now logs and exits with status 1. Previously it propagated out ofmain, and the JVM could hang on non-daemon threads.Why
runtimedepended on the extension loader, so every variant loadedextensions/, including the local development server. The server init order was only implicit inmain.BREAKING CHANGE: only the apps/cloudnet variant loads the extensions folder; apps/local starts plain Minestom without the extension loader.
Deviations from design.md
LoadedModules: eachServerBootstrapfound on the loader counts as<name>Platform(soextensionsPlatform). This is used by the boot check and by the test-side verification alike.CloudNetEnvironmentstays incommon:runtime(profile selection) andConnectorStartupCheckneed it, so moving it would invert the dependency. It gained anisPresent(Path)overload for the tests.TracedDelivermoves tocommon: not in the design.platform/cloudnethas to wrap its deliver in the same trace, and must not depend onruntime.platform/cloudnetsets its owngroup: it shares the namecloudnetwithapps/cloudnet, and Gradle resolved the platform dependency to the app project (circular task graph). Found while wiring the variant.ServerBootstrap.loadedExtensions()returns an orderedListinstead of aSet, so the start log and checks stay stable.new Titan(bootstrap); the bootstrap goes into the scope as a bean.How it was tested
Unit (F.I.R.S.T., no server, no real working directory, no system properties):
ServerBootstrapsTest: no candidate falls back to plain Minestom; one candidate is chosen; two abort naming both.TitanStartupTest: init, exception handler, scope, start in that order; a failing scope never starts the server; the start log lineServer bootstrap: {}is captured through aListAppender.BootstrapSettingsTest(characterisation, task 1.4): forwarding secret file before property, blank file ignored, missing file falls back, bind host and port defaults and overrides, non-numeric port falls back to 25565.ExtensionNamesTest,ExtensionServerBootstrapRegistrationTest: stable name order; theServiceLoaderregistration.CloudNetDeliverSelectionTest(@TempDir):.wrapperselectsMessageChannelDeliver, without itDebugDeliver.MessageChannelDeliverTest(moved, now uses aListAppender),TracedDeliverTest(moved).DeliverFallbackBeansTest,LuckPermsPermissionServiceStartTest(aLuckPermsextension reported by the bootstrap aborts the start).ExpectedModulesTest:extensionsPlatformis reported missing under plain Minestom.apps/cloudnetVariantStartTestasserts the active bootstrap isextensionsandextensionsPlatformis loaded;apps/localassertsminestom.Verification:
./gradlew spotlessApply build --no-daemonpasses../gradlew :apps:cloudnet:bootSmokeTest :apps:local:bootSmokeTest --rerun --no-daemonpasses.openspec validate --all: see the archive commit.Scenario mapping (task 4.5, read-only check)
Every
server-bootstrapscenario maps to at least one test (the extension-loader and socket scenarios are partial). Not covered by unit tests, all of them need a live process: jars inextensions/loaded before the first player connects, jars ignored inapps/local, forwarding enforced in both variants, the bind address applied to the socket. These are the manual checks in 4.4. The read-only check also noted thatTitanStartupTestattaches itsListAppenderto a shared logger, the same patternVariantStartupCheckTestalready uses; it detaches infinally.Unticked tasks (manual, need a live server)
ExtensionServerBootstrapand an emptyextensions/in a@TempDir.MinecraftServer.initis process-global and the loader readsextensions/relative to the working directory, so it cannot run as a unit test. Covered by 4.4 instead.titan-cloudnet.jarwith the CloudNet bridge and the Titan bridge inextensions/: both load, the log showsServer bootstrap: extensions, the navigator transfer works inside CloudNet, and the chat message appears outside it.titan-local.jarwith a filledextensions/: nothing loads, the log showsServer bootstrap: minestom.forwarding.secretand-Dservice.bind.portwork in both.Task 5.1 (this PR) is ticked after the PR is opened, as in #403.
https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1