Skip to content

refactor(permission)!: resolve permissions through an optional di module - #313

Merged
TheMeinerLP merged 19 commits into
mainfrom
refactor/permission-spi
Sep 28, 2026
Merged

TheMeinerLP merged 19 commits into
mainfrom
refactor/permission-spi

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Contributor

Summary

New PermissionService contract in core (JDK-only, ALLOWED/DENIED/NOT_SET); DenyAllPermissionService as @Secondary fallback in runtime; new module platform/luckperms (LuckPermsPermissionService, @Named("luckperms"), starts the LuckPerms minestom-loader in @PostConstruct, stops via the loader's JVM shutdown hook, aborts if a LuckPerms extension is loaded too); TitanPlayer and the CloudNet bridge resolver (TitanPermissionBridge) both resolve through the same service; startup logs Permissions resolved by <name>; titanVariant { platform("…") } in the app-variant convention adds platform modules and makes them expected modules (luckpermsPlatform); apps/cloudnet always includes LuckPerms, apps/local only with -Ptitan.luckperms; Butterfly removed.

Behaviour changes

CloudNet permission queries now honour LuckPerms contexts (online player → the player's query options, otherwise static query options; previously no options); without a permission platform players have no permissions, console unaffected.

Deploy

Remove extensions/luckperms.jar and extensions/butterfly.jar from the CloudNet template; keep data/ (LuckPerms data location unchanged); retrain the AOT cache (training now boots LuckPerms against a disposable data dir).

Acceptance (local, titan-cloudnet.jar with existing data/)

  • Startup log names luckperms, module luckpermsPlatform loaded
  • Existing data/ picked up (group default present)
  • /stop with permission not set → refused
  • /stop with permission false → refused
  • /stop with permission only in server=survival → refused
  • /stop with permission in server=lobby (lobby's own context) → server stops
  • apps/local without the switch → Permissions resolved by deny-all, no data/ created
  • Duplicate LuckPerms: startup aborts with "LuckPerms is loaded twice" — needs an API-compatible LuckPerms extension jar (the old 6.0.1 extension fails earlier with NoSuchMethodError); check covered by unit test
  • CloudNet permission query returns the lobby's result — needs a CloudNet service; resolver covered by unit tests

Depends on

#312 (merged).

BREAKING CHANGE: Butterfly is removed and LuckPerms is no longer loaded from main() or the extensions folder but only through platform/luckperms; without it players have no permissions.

https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH

Titan needs a permission source that is replaceable by DI instead of
hard-wired to LuckPerms; this JDK-only contract lets any platform
module answer a player permission check without leaking its own types
across the classloader boundary.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
…rovides one

Without a permission platform in the DI scope, Titan must still start
and behave safely; this @secondary fallback answers every check as not
set so players get no permissions until a platform module overrides it.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Adds platform/luckperms (java-library, Avaje inject) and a platform/*
directory scan in settings.gradle.kts, mirroring the features/* scan.
The module declares @InjectModule(name = "luckpermsPlatform", provides
= PermissionService.class) so a variant can pick it up.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
LuckPermsResults translates a LuckPerms Tristate into the
platform-neutral PermissionResult, tested for all three values.
Runtime's CompatibilityUtil stays where it is for now; the runtime
agent removes it once the platform module is wired in.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
LuckPermsExtensionCheck rejects a duplicate extensions/luckperms.jar
by name, case-insensitively, tested standalone as a pure function.
LuckPermsPermissionService wires it into @PostConstruct before
starting LuckPerms through the loader, then answers a permission check
from the player's live or static query options, keyed by UUID so the
CloudNet bridge can use it too.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
…on service

TitanPlayer now checks the injected PermissionService instead of calling
LuckPerms directly, and Titan sets the Minestom player provider only after
the BeanScope is built, once that service is available. The old LuckPerms
mapping in runtime's own CompatibilityUtil moves to platform/luckperms.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
…rvice

PermissionBridgeResolver turns a PermissionService into the BiPredicate
TitanPermissionBridge needs, and a new @singleton wires/unwires it around
the BeanScope's lifecycle. TitanApplication.main no longer starts LuckPerms
or builds a resolver by hand.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Titan logs "Permissions resolved by {}" with the resolved service's name
once the BeanScope is built, so the start log always names deny-all or a
platform's own service.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Butterfly is unused beyond Titan.initialize() and its shutdown task; drop the
dependency, the butterfly/butterfly-minestom catalog entries, and both call
sites. runtime no longer bundles LuckPerms or Guava, and apps/cloudnet and
apps/local drop the minestom-loader Gson exclude that existed only because
runtime shipped that loader.

BREAKING CHANGE: runtime no longer starts LuckPerms or loads Butterfly; a
platform module now owns the permission platform's lifecycle.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Add titanVariant { platform(...) } so an app variant can depend on a
permission platform module and expect it at startup, the same way it
already does for feature columns.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
apps/cloudnet always bundles platform/luckperms; apps/local only with
-Ptitan.luckperms, keeping the deny-all fallback as its default. Every
scope-building test in apps/cloudnet now mocks PermissionService under
Avaje's auto-derived "LuckPerms" qualifier so it never starts real
LuckPerms, and apps/local gets a start test asserting the deny-all
fallback is active without the switch.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Document titanVariant { platform(...) }, -Ptitan.luckperms for local
builds, the deny-all fallback, and the deploy-time removal of
extensions/luckperms.jar and extensions/butterfly.jar now that the
lobby starts LuckPerms itself and Butterfly is gone.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
Seven test call sites mocked PermissionService under Avaje's
class-name-derived qualifier "LuckPerms", each re-explaining in a
multi-line comment why the unnamed mock(Type) overload isn't enough.
Giving LuckPermsPermissionService an explicit @nAmed(QUALIFIER) makes
the name intentional and lets every test reference the constant
instead of a magic string, with the comment shrunk to a doc pointer.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
The generateAotCache training run boots the full BeanScope, including
LuckPermsPermissionService's @PostConstruct, so it starts real
LuckPerms against a data/ directory of its own rather than the
deployment's.

Claude-Session: https://claude.ai/code/session_01A7aAe2E6wAEbi6jjnyPTWH
@TheMeinerLP
TheMeinerLP requested a review from a team as a code owner September 28, 2026 20:17
@github-actions

Copy link
Copy Markdown
Contributor

Test results

210 files  210 suites   13m 49s ⏱️
298 tests 297 ✅ 1 💤 0 ❌
894 runs  891 ✅ 3 💤 0 ❌

Results for commit 80322a9.

@TheMeinerLP
TheMeinerLP merged commit a32544b into main Sep 28, 2026
8 checks passed
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