5.1 KiB
5.1 KiB
Overview
Architecture
RanksPlugin (entry)
+-- RanksPermissionProvider implements Hytale's PermissionProvider SPI
| +-- RankRepository SQL: ranks, ranks_perms
| +-- GrantRepository SQL: ranks_grants
| +-- UserPermRepository SQL: ranks_user_perms
| +-- RanksCache in-memory per-user effective perms + group memberships
+-- RankService high-level business API used by commands
+-- ChatPrefixListener PlayerChatEvent -> rewrites formatter with primary rank prefix
+-- Commands: /rank, /grant, /tempgrant, /revoke, /primary, /ranks
External:
- NetworkCore.getDatabase() for the MySQL pool
- NetworkCore.getMessageBus() for cross-server cache invalidation
- PermissionsModule.get() for
addProvider/removeProvider/getProviders
Mental model: rank vs group
In Hytale's permission API the unit is a group. We call ours a rank in user-facing speak but they're the same thing under the hood. A Rank row in the DB IS a Hytale group. Every method on PermissionProvider operates on group IDs - we just pass Rank.id() through.
A player can hold multiple ranks at once via Grant rows. Hytale's getGroupsForUser(uuid) returns the set of active rank IDs (not revoked, not expired). The "primary" flag is OUR extension - Hytale doesn't have a notion of primary. We use it for chat prefix selection.
Lifecycle
setup()- load config.start():- Require NetworkCore's DB + bus.
- Bootstrap schema; seed Hytale's built-in groups (
hytale:Adventurer,hytale:Admin, etc.) if absent. - Build cache + provider.
- If
provider.replace_default: remove every existing provider fromPermissionsModule, remember them, add ours. - Register the chat prefix listener and all commands.
shutdown():- Remove our provider.
- Re-add any providers we removed at start (so we leave the module in the state we found it).
- Stop the cache (closes the bus subscription).
File map
| File | Responsibility |
|---|---|
| RanksPlugin.java | Entry point. Lifecycle, provider registration. |
| RanksPermissionsNodes.java | String constants for networkranks.* nodes. |
| config/ | Config record + JSON loader. |
| model/Rank.java | (id, parentId, displayName, prefix, priority, builtIn). |
| model/Grant.java | One row of ranks_grants. |
| db/SchemaBootstrap.java | CREATE TABLE IF NOT EXISTS + seeds built-in groups. |
| db/RankRepository.java | CRUD on ranks + ranks_perms, includes inheritance walk. |
| db/GrantRepository.java | CRUD on ranks_grants. |
| db/UserPermRepository.java | CRUD on ranks_user_perms. |
| provider/RanksPermissionProvider.java | Implements PermissionProvider. Reads cache, writes through to MySQL. |
| service/RanksCache.java | In-memory cache + cross-server invalidation publisher/subscriber. |
| service/InvalidationPayload.java | Wire payload on the invalidation channel. |
| service/RankService.java | High-level business API used by commands. |
| listener/ChatPrefixListener.java | Sets PlayerChatEvent formatter using primary rank's prefix. |
| command/rank/RankCommand.java | `/rank create |
| command/GrantCommands.java | /grant, /tempgrant, /revoke, /primary, /ranks. |
| util/DurationParser.java | Parses 30d, 1mo, 1y, etc. |
| util/TargetResolver.java | Resolves a name (online players only) or UUID string to a UUID. |
Why this shape
- One provider per server, single source of truth: replacing Hytale's default removes the risk of two providers giving conflicting answers.
- Cache around the provider: hasPermission gets called many times per tick (chat enforcement, command parsing, world actions). Hitting MySQL each time would be a disaster; caching by UUID with bus-based invalidation is the standard pattern.
- Repositories return models, the service orchestrates: keeps SQL out of the provider and command handlers.
built_inflag on ranks: stops/rank delete hytale:Adminfrom breaking the world.