# Overview ## Architecture in one diagram ``` StaffPlugin (entry) +-- StaffPermissions wraps Hytale PermissionsModule, checks groups from config +-- PunishmentService issue / revoke / lookup, broadcasts on MessageBus | +-- PunishmentRepository SQL CRUD | +-- MuteCache in-memory map of active mutes for online players +-- StaffChatService cross-server staff chat over MessageBus +-- PlayerSeenTracker writes to players_seen on join/leave | +-- PlayerSeenRepository SQL CRUD +-- BanEnforcer cancels PlayerSetupConnectEvent if banned +-- ChatEnforcer cancels PlayerChatEvent if muted ``` External dependencies: - **NetworkCore.getDatabase()** - MySQL pool (HikariCP). Required. - **NetworkCore.getMessageBus()** - Redis pub/sub. Required. - **NetworkCore.getServerId()** - identifies which server an action was taken on. - **PermissionsModule.get()** - Hytale's built-in group/permission API. ## What lives where | File | Responsibility | |---|---| | [StaffPlugin.java](../src/main/java/net/kewwbec/staff/StaffPlugin.java) | Plugin entry. Reads NetworkCore services, bootstraps schema, registers everything. | | [config/StaffConfig.java](../src/main/java/net/kewwbec/staff/config/StaffConfig.java) | Default config shape. | | [util/StaffPermissions.java](../src/main/java/net/kewwbec/staff/util/StaffPermissions.java) | `isStaff(uuid)` / `isAdmin(uuid)` checks. | | [util/PlayerResolver.java](../src/main/java/net/kewwbec/staff/util/PlayerResolver.java) | Name -> UUID, looks at online players first, then players_seen. | | [util/DurationParser.java](../src/main/java/net/kewwbec/staff/util/DurationParser.java) | Parse "30m", "2h" etc.; format milliseconds back to human strings. | | [model/Punishment.java](../src/main/java/net/kewwbec/staff/model/Punishment.java), [PunishmentType.java](../src/main/java/net/kewwbec/staff/model/PunishmentType.java), [PlayerSeen.java](../src/main/java/net/kewwbec/staff/model/PlayerSeen.java) | Record types. | | [db/SchemaBootstrap.java](../src/main/java/net/kewwbec/staff/db/SchemaBootstrap.java) | `CREATE TABLE IF NOT EXISTS` for both tables. Run on `start()`. | | [db/PunishmentRepository.java](../src/main/java/net/kewwbec/staff/db/PunishmentRepository.java) | SQL for punishments. | | [db/PlayerSeenRepository.java](../src/main/java/net/kewwbec/staff/db/PlayerSeenRepository.java) | SQL for player presence. | | [service/PunishmentService.java](../src/main/java/net/kewwbec/staff/service/PunishmentService.java) | Issuing/revoking + cross-server broadcast + applying remote actions locally. | | [service/MuteCache.java](../src/main/java/net/kewwbec/staff/service/MuteCache.java) | Per-server hot cache of active mutes for online players. | | [service/StaffChatService.java](../src/main/java/net/kewwbec/staff/service/StaffChatService.java) | Cross-server staff chat over MessageBus. | | [service/PunishmentBroadcast.java](../src/main/java/net/kewwbec/staff/service/PunishmentBroadcast.java), [StaffChatPayload.java](../src/main/java/net/kewwbec/staff/service/StaffChatPayload.java) | Wire payloads. | | [listener/BanEnforcer.java](../src/main/java/net/kewwbec/staff/listener/BanEnforcer.java) | `PlayerSetupConnectEvent` -> cancel + set reason if banned. | | [listener/ChatEnforcer.java](../src/main/java/net/kewwbec/staff/listener/ChatEnforcer.java) | `PlayerChatEvent` -> cancel if muted. | | [listener/PlayerSeenTracker.java](../src/main/java/net/kewwbec/staff/listener/PlayerSeenTracker.java) | Connect/disconnect -> update players_seen, load/clear mute cache. | | [command/punish/PunishmentCommands.java](../src/main/java/net/kewwbec/staff/command/punish/PunishmentCommands.java) | All 8 punishment commands as nested classes. | | [command/chat/StaffChatCommand.java](../src/main/java/net/kewwbec/staff/command/chat/StaffChatCommand.java) | `/sc`. | | [command/lookup/LookupCommand.java](../src/main/java/net/kewwbec/staff/command/lookup/LookupCommand.java), [HistoryCommand.java](../src/main/java/net/kewwbec/staff/command/lookup/HistoryCommand.java) | `/lookup`, `/history`. | ## Why the design is this shape - **One service per concern.** Punishments and staff chat are independent. Each owns its own MessageBus subscription and can be modified without touching the other. - **Repositories return models, services orchestrate.** Repositories never call services; services call repositories. This keeps SQL out of the punishment-issuing logic. - **MuteCache is per-server, not network-wide.** Mute state is loaded on join for that one player and cleared on leave. Network-wide propagation happens via the MessageBus, which updates other servers' caches when their player base intersects. - **BanEnforcer talks to MySQL on connect.** No cache. Connects are infrequent enough that a single indexed query is fine; the data is authoritative and we don't want stale-cache surprises letting a banned player in. - **Permission checks live in the command, not in services.** That way a future console source can call the same service without re-checking permissions. ## Lifecycle Standard Hytale plugin lifecycle: - `setup()` - load config, build `StaffPermissions` and `MuteCache`. - `start()` - require NetworkCore's database + bus, bootstrap schema, build services and repositories, mark this server's stale online flags as offline (crash recovery), register events and commands. - `shutdown()` - stop service subscriptions. The database pool is owned by NetworkCore and closes on its own shutdown. If MySQL isn't healthy at `start()`, this plugin logs a severe error and returns. The server keeps running; this plugin just does nothing. Restart once MySQL is back.