# 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()`: 1. Require NetworkCore's DB + bus. 2. Bootstrap schema; seed Hytale's built-in groups (`hytale:Adventurer`, `hytale:Admin`, etc.) if absent. 3. Build cache + provider. 4. If `provider.replace_default`: remove every existing provider from `PermissionsModule`, remember them, add ours. 5. Register the chat prefix listener and all commands. - `shutdown()`: 1. Remove our provider. 2. Re-add any providers we removed at start (so we leave the module in the state we found it). 3. Stop the cache (closes the bus subscription). ## File map | File | Responsibility | |---|---| | [RanksPlugin.java](../src/main/java/net/kewwbec/ranks/RanksPlugin.java) | Entry point. Lifecycle, provider registration. | | [RanksPermissionsNodes.java](../src/main/java/net/kewwbec/ranks/RanksPermissionsNodes.java) | String constants for `networkranks.*` nodes. | | [config/](../src/main/java/net/kewwbec/ranks/config/) | Config record + JSON loader. | | [model/Rank.java](../src/main/java/net/kewwbec/ranks/model/Rank.java) | `(id, parentId, displayName, prefix, priority, builtIn)`. | | [model/Grant.java](../src/main/java/net/kewwbec/ranks/model/Grant.java) | One row of `ranks_grants`. | | [db/SchemaBootstrap.java](../src/main/java/net/kewwbec/ranks/db/SchemaBootstrap.java) | `CREATE TABLE IF NOT EXISTS` + seeds built-in groups. | | [db/RankRepository.java](../src/main/java/net/kewwbec/ranks/db/RankRepository.java) | CRUD on `ranks` + `ranks_perms`, includes inheritance walk. | | [db/GrantRepository.java](../src/main/java/net/kewwbec/ranks/db/GrantRepository.java) | CRUD on `ranks_grants`. | | [db/UserPermRepository.java](../src/main/java/net/kewwbec/ranks/db/UserPermRepository.java) | CRUD on `ranks_user_perms`. | | [provider/RanksPermissionProvider.java](../src/main/java/net/kewwbec/ranks/provider/RanksPermissionProvider.java) | Implements `PermissionProvider`. Reads cache, writes through to MySQL. | | [service/RanksCache.java](../src/main/java/net/kewwbec/ranks/service/RanksCache.java) | In-memory cache + cross-server invalidation publisher/subscriber. | | [service/InvalidationPayload.java](../src/main/java/net/kewwbec/ranks/service/InvalidationPayload.java) | Wire payload on the invalidation channel. | | [service/RankService.java](../src/main/java/net/kewwbec/ranks/service/RankService.java) | High-level business API used by commands. | | [listener/ChatPrefixListener.java](../src/main/java/net/kewwbec/ranks/listener/ChatPrefixListener.java) | Sets `PlayerChatEvent` formatter using primary rank's prefix. | | [command/rank/RankCommand.java](../src/main/java/net/kewwbec/ranks/command/rank/RankCommand.java) | `/rank create|delete|addperm|removeperm|setparent|setprefix|list|info`. | | [command/GrantCommands.java](../src/main/java/net/kewwbec/ranks/command/GrantCommands.java) | `/grant`, `/tempgrant`, `/revoke`, `/primary`, `/ranks`. | | [util/DurationParser.java](../src/main/java/net/kewwbec/ranks/util/DurationParser.java) | Parses `30d`, `1mo`, `1y`, etc. | | [util/TargetResolver.java](../src/main/java/net/kewwbec/ranks/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_in` flag on ranks**: stops `/rank delete hytale:Admin` from breaking the world.