Skip to content

refactor(bootstrap)!: make minestom extensions a platform module - #407

Merged
TheMeinerLP merged 6 commits into
mainfrom
refactor/optional-extensions-bootstrap
Oct 10, 2026
Merged

TheMeinerLP merged 6 commits into
mainfrom
refactor/optional-extensions-bootstrap

Conversation

@TheMeinerLP

@TheMeinerLP TheMeinerLP commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Stacked PR

This PR is stacked on #403 (fix(runtime): close the bean scope when the startup check fails), branch fix/close-scope-on-failed-start. The diff against that branch is only this change. Merge #403 first, or retarget this PR to main after 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: new ServerBootstrap contract (name, init(Auth), start(host, port), loadedExtensions()).
  • runtime: ServerBootstraps.select picks the one candidate, plain Minestom (PlainMinestomBootstrap) when there is none, and aborts naming all candidates when there are several. TitanStartup fixes the order: init the server, install the exception handler, build the Avaje scope, start the server.
  • platform/extensions (new): ExtensionServerBootstrap on minestom-extensions, registered through META-INF/services.
  • platform/cloudnet (new): the CloudNet deliver (MessageChannelDeliver), chosen by the .wrapper directory and marked @Primary.
  • runtime keeps DebugDeliver as a @Secondary fallback. TracedDeliver moves to common so both sides wrap the deliver the same way.
  • apps/cloudnet includes extensions, cloudnet and luckperms. apps/local includes none of them.
  • platform/luckperms reads the loaded extensions from the injected ServerBootstrap and no longer depends on minestom-extensions.
  • README: the start log line Server bootstrap: <name> and the per-variant behaviour.
  • Behaviour change: a bind failure in bootstrap.start(...) now logs and exits with status 1. Previously it propagated out of main, and the JVM could hang on non-daemon threads.

Why

runtime depended on the extension loader, so every variant loaded extensions/, including the local development server. The server init order was only implicit in main.

BREAKING CHANGE: only the apps/cloudnet variant loads the extensions folder; apps/local starts plain Minestom without the extension loader.

Deviations from design.md

  • ServiceLoader instead of a second Avaje scope (D2): as designed.
  • Module check through LoadedModules: each ServerBootstrap found on the loader counts as <name>Platform (so extensionsPlatform). This is used by the boot check and by the test-side verification alike.
  • CloudNetEnvironment stays in common: runtime (profile selection) and ConnectorStartupCheck need it, so moving it would invert the dependency. It gained an isPresent(Path) overload for the tests.
  • TracedDeliver moves to common: not in the design. platform/cloudnet has to wrap its deliver in the same trace, and must not depend on runtime.
  • platform/cloudnet sets its own group: it shares the name cloudnet with apps/cloudnet, and Gradle resolved the platform dependency to the app project (circular task graph). Found while wiring the variant.
  • ServerBootstrap.loadedExtensions() returns an ordered List instead of a Set, so the start log and checks stay stable.
  • Titan's constructor takes the bootstrap: 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 line Server bootstrap: {} is captured through a ListAppender.
  • 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; the ServiceLoader registration.
  • CloudNetDeliverSelectionTest (@TempDir): .wrapper selects MessageChannelDeliver, without it DebugDeliver.
  • MessageChannelDeliverTest (moved, now uses a ListAppender), TracedDeliverTest (moved).
  • DeliverFallbackBeansTest, LuckPermsPermissionServiceStartTest (a LuckPerms extension reported by the bootstrap aborts the start).
  • ExpectedModulesTest: extensionsPlatform is reported missing under plain Minestom.
  • Variant starts: apps/cloudnet VariantStartTest asserts the active bootstrap is extensions and extensionsPlatform is loaded; apps/local asserts minestom.

Verification:

  • ./gradlew spotlessApply build --no-daemon passes.
  • ./gradlew :apps:cloudnet:bootSmokeTest :apps:local:bootSmokeTest --rerun --no-daemon passes.
  • openspec validate --all: see the archive commit.

Scenario mapping (task 4.5, read-only check)

Every server-bootstrap scenario 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 in extensions/ loaded before the first player connects, jars ignored in apps/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 that TitanStartupTest attaches its ListAppender to a shared logger, the same pattern VariantStartupCheckTest already uses; it detaches in finally.

Unticked tasks (manual, need a live server)

  • 2.3: start with ExtensionServerBootstrap and an empty extensions/ in a @TempDir. MinecraftServer.init is process-global and the loader reads extensions/ relative to the working directory, so it cannot run as a unit test. Covered by 4.4 instead.
  • 4.4: manual E2E acceptance. titan-cloudnet.jar with the CloudNet bridge and the Titan bridge in extensions/: both load, the log shows Server bootstrap: extensions, the navigator transfer works inside CloudNet, and the chat message appears outside it. titan-local.jar with a filled extensions/: nothing loads, the log shows Server bootstrap: minestom. forwarding.secret and -Dservice.bind.port work in both.

Task 5.1 (this PR) is ticked after the PR is opened, as in #403.

https://claude.ai/code/session_01GXose9rakuRNZoM3TP3xj1

@TheMeinerLP
TheMeinerLP requested a review from a team as a code owner October 10, 2026 19:28
@TheMeinerLP
TheMeinerLP force-pushed the fix/close-scope-on-failed-start branch from e13d340 to a4b0ebc Compare October 10, 2026 21:08
@TheMeinerLP
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
TheMeinerLP force-pushed the refactor/optional-extensions-bootstrap branch from 21e772d to 0e3ef94 Compare October 10, 2026 22:14
@TheMeinerLP
TheMeinerLP merged commit 306587a into main Oct 10, 2026
2 checks passed
@TheMeinerLP
TheMeinerLP deleted the refactor/optional-extensions-bootstrap branch October 10, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant