init
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Configuration
|
||||
|
||||
## Location
|
||||
|
||||
`<plugin data dir>/config.json`. Written with defaults on first boot.
|
||||
|
||||
## Full config
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"staff_groups": ["OP", "ADMIN", "MOD", "HELPER"],
|
||||
"admin_groups": ["OP", "ADMIN"]
|
||||
},
|
||||
"chat": {
|
||||
"channel": "staff.chat",
|
||||
"prefix": "[Staff]"
|
||||
},
|
||||
"tables": {
|
||||
"punishments": "staff_punishments",
|
||||
"players_seen": "staff_players_seen"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fields
|
||||
|
||||
### permissions
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `staff_groups` | `["OP", "ADMIN", "MOD", "HELPER"]` | Any Hytale group name in this list grants access to staff commands. |
|
||||
| `admin_groups` | `["OP", "ADMIN"]` | Subset that the plugin treats as "admin". v1 doesn't gate anything on this, but it's available for future features that want to require admin (e.g. unban after a permaban). |
|
||||
|
||||
Group names come from `PermissionsModule.get().getGroupsForUser(uuid)`. Set up your groups in the Hytale server's own configuration first; this plugin just reads them.
|
||||
|
||||
### chat
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `channel` | `"staff.chat"` | MessageBus channel used for cross-server staff chat. Don't share this with other plugins. |
|
||||
| `prefix` | `"[Staff]"` | Visual prefix on every staff chat line. |
|
||||
|
||||
### tables
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `punishments` | `"staff_punishments"` | Table name. Change only if you really need to and you're prepared to migrate data. |
|
||||
| `players_seen` | `"staff_players_seen"` | Same. |
|
||||
|
||||
## Setting up staff groups
|
||||
|
||||
In your Hytale server config, define groups and assign players:
|
||||
|
||||
1. Make sure the group names you assign match one of `staff_groups` exactly. The check is case-sensitive (groups in Hytale tend to be uppercase, e.g. `OP`).
|
||||
2. After assigning, the player needs to relog for the change to take effect (`PermissionsModule.get().getGroupsForUser` is queried on each command, but Hytale generally requires a reconnect for group changes to apply on the player side).
|
||||
3. Test with `/lookup <staff-member-name>` from a staff account. If you see the lookup output, you have staff perms.
|
||||
|
||||
## Environment variables
|
||||
|
||||
None specific to Staff. The relevant secrets (`MYSQL_PASSWORD`, `NETWORK_AUTH_TOKEN`) live in NetworkCore's env.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Commands
|
||||
|
||||
All commands require the caller to be in one of the `staff_groups` from config. Player-only (no console support yet).
|
||||
|
||||
## Punishments
|
||||
|
||||
### /ban <player> <reason>
|
||||
|
||||
Permanent ban. Active across all servers in the network within a few hundred ms. Online target is disconnected immediately with the reason.
|
||||
|
||||
### /tempban <player> <duration> <reason>
|
||||
|
||||
Time-limited ban. Duration syntax: `30s`, `5m`, `2h`, `7d`, `1w`. Expires automatically (the active-ban query filters by `expires_at`).
|
||||
|
||||
### /mute <player> <reason>
|
||||
|
||||
Permanent mute. Cancels `PlayerChatEvent` for the target on every server. Loaded into a per-server in-memory cache on the target's connect so chat enforcement doesn't hit MySQL on every message.
|
||||
|
||||
### /tempmute <player> <duration> <reason>
|
||||
|
||||
Time-limited mute. Same duration syntax as tempban.
|
||||
|
||||
### /kick <player> <reason>
|
||||
|
||||
One-shot disconnect. Recorded in history but doesn't persist beyond that (the target can rejoin immediately).
|
||||
|
||||
### /warn <player> <reason>
|
||||
|
||||
Recorded in history and shown to the target as a message if they're online. No connection or chat impact.
|
||||
|
||||
### /unban <player>
|
||||
|
||||
Revokes any active ban or tempban for the player. Marks the punishment row as `active=0`, fills `revoked_at` / `revoked_by_*`. Original row stays in history.
|
||||
|
||||
### /unmute <player>
|
||||
|
||||
Same as unban but for mutes. Also clears the entry from the mute cache on any server where the player is online.
|
||||
|
||||
## Staff chat
|
||||
|
||||
### /sc <message>
|
||||
|
||||
Broadcasts on the cross-server staff chat channel. Visible only to other players whose `getGroupsForUser` overlaps with `staff_groups`.
|
||||
|
||||
System events (a ban being issued, an unban) also post a line on this channel automatically.
|
||||
|
||||
## Lookup
|
||||
|
||||
### /lookup <player>
|
||||
|
||||
Shows:
|
||||
- Player's last-known name and UUID
|
||||
- Online status (and which server they're on, if any)
|
||||
- First-seen and last-seen timestamps
|
||||
- Any active ban
|
||||
- Any active mute
|
||||
|
||||
If the player has never connected, only the name/UUID lookup happens via online players and may return nothing.
|
||||
|
||||
### /history <player>
|
||||
|
||||
Last 20 punishment entries for the player, most recent first. Each entry shows id, state (`ACTIVE` / `EXPIRED` / `REVOKED`), type, who issued it, when, and the reason.
|
||||
|
||||
## Common error cases
|
||||
|
||||
| Error message | Cause |
|
||||
|---|---|
|
||||
| "You do not have permission to use this command." | Caller isn't in any of `staff_groups`. |
|
||||
| "No player found with that name (online or in history)." | Name doesn't match any online player or any name in `staff_players_seen`. |
|
||||
| "Database error: ..." | MySQL query failed. Check NetworkCore connection (`/networkcore status`). |
|
||||
| "Bad duration. Use 30s, 5m, 2h, 7d, 1w." | Duration string didn't parse. Last char must be `s`/`m`/`h`/`d`/`w`, prefix must be a positive integer. |
|
||||
@@ -0,0 +1,94 @@
|
||||
# Schema
|
||||
|
||||
Two MySQL tables, created automatically on `start()` via `CREATE TABLE IF NOT EXISTS`. Both prefixed `staff_` so they don't collide with other plugins.
|
||||
|
||||
## staff_punishments
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS staff_punishments (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_uuid CHAR(36) NOT NULL,
|
||||
target_name VARCHAR(64) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, -- BAN, TEMPBAN, MUTE, TEMPMUTE, KICK, WARN
|
||||
reason VARCHAR(512) NOT NULL,
|
||||
issued_by_uuid CHAR(36) NOT NULL,
|
||||
issued_by_name VARCHAR(64) NOT NULL,
|
||||
issued_at BIGINT NOT NULL, -- epoch millis
|
||||
expires_at BIGINT NULL, -- epoch millis; NULL = permanent
|
||||
issued_on_server VARCHAR(64) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
revoked_at BIGINT NULL,
|
||||
revoked_by_uuid CHAR(36) NULL,
|
||||
revoked_by_name VARCHAR(64) NULL,
|
||||
revoke_reason VARCHAR(512) NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_target_uuid (target_uuid),
|
||||
KEY idx_target_active (target_uuid, type, active),
|
||||
KEY idx_issued_at (issued_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Rows are **never deleted**. Revokes flip `active=0` and fill the `revoked_*` columns. Wipe via SQL only if you really mean to lose history.
|
||||
- `target_name` is captured at issue time and never updated. If a player renames, history shows the old name; lookup by UUID is canonical.
|
||||
- `issued_on_server` is the server id that issued the action. Useful for "who issued this from where" forensics; not used for enforcement.
|
||||
|
||||
### Common queries
|
||||
|
||||
```sql
|
||||
-- Active ban for a player
|
||||
SELECT * FROM staff_punishments
|
||||
WHERE target_uuid = ? AND type IN ('BAN', 'TEMPBAN')
|
||||
AND active = 1 AND revoked_at IS NULL
|
||||
AND (expires_at IS NULL OR expires_at > UNIX_TIMESTAMP() * 1000)
|
||||
ORDER BY issued_at DESC LIMIT 1;
|
||||
|
||||
-- All actions a moderator has taken in the last 7 days
|
||||
SELECT * FROM staff_punishments
|
||||
WHERE issued_by_uuid = ?
|
||||
AND issued_at > (UNIX_TIMESTAMP() * 1000) - 7 * 86400000
|
||||
ORDER BY issued_at DESC;
|
||||
|
||||
-- Most-punished players in the last month
|
||||
SELECT target_name, COUNT(*) c
|
||||
FROM staff_punishments
|
||||
WHERE issued_at > (UNIX_TIMESTAMP() * 1000) - 30 * 86400000
|
||||
GROUP BY target_uuid, target_name
|
||||
ORDER BY c DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
## staff_players_seen
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS staff_players_seen (
|
||||
uuid CHAR(36) NOT NULL,
|
||||
last_name VARCHAR(64) NOT NULL,
|
||||
first_seen BIGINT NOT NULL,
|
||||
last_seen BIGINT NOT NULL,
|
||||
last_server_id VARCHAR(64) NULL,
|
||||
online TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (uuid),
|
||||
KEY idx_last_name (last_name),
|
||||
KEY idx_last_seen (last_seen)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- One row per player UUID. Upserted on join.
|
||||
- `online` is best-effort: a hard server crash leaves the row stuck at `online=1` until that server boots again and runs `markServerOffline(serverId)` (which Staff does in `start()`).
|
||||
- `last_server_id` is the last server the player joined. If `online=1` it's the server they're on now.
|
||||
|
||||
## Migrations
|
||||
|
||||
There aren't any yet. When a schema change is needed:
|
||||
|
||||
1. Add a `migrations/00X_what.sql` file
|
||||
2. Add a tiny runner in `SchemaBootstrap` that:
|
||||
- Reads applied migrations from a `staff_schema_version` table (create on first run)
|
||||
- Applies the next pending one, commits, records it
|
||||
3. Ship together. Don't backport schema changes to old plugin versions without a migration.
|
||||
|
||||
Don't drop into Flyway/Liquibase until you have at least 3 real migrations - they earn their weight on real projects, not on the second migration.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Cross-Server Propagation
|
||||
|
||||
How a `/ban` on one server affects players on all other servers.
|
||||
|
||||
## The path of a ban
|
||||
|
||||
1. **Staff types `/ban Bob griefing` on lobby-1.**
|
||||
2. `PunishmentCommands.Ban.execute` runs:
|
||||
- Calls `PunishmentService.issue(...)`.
|
||||
3. `PunishmentService.issue(...)`:
|
||||
- INSERTs the row into `staff_punishments` (active=1, revoked_at=null).
|
||||
- Calls `applyLocally(saved)` - if Bob is online on lobby-1, kicks him.
|
||||
- Calls `publishIssued(saved)` - sends a `PunishmentBroadcast` on MessageBus channel `staff.punishments`.
|
||||
4. **arena-3 receives the broadcast** on its `staff.punishments` subscription.
|
||||
5. `PunishmentService.onRemote` runs:
|
||||
- Ignores its own broadcasts (`issuedOnServer == self`).
|
||||
- Reconstructs a transient `Punishment` from the payload.
|
||||
- Calls `applyLocally(p)` - same logic as the issuer used.
|
||||
- On arena-3 specifically: if Bob is currently online there, he gets kicked. If he's not, nothing happens at this stage.
|
||||
6. **Bob tries to reconnect somewhere later.** The receiving server runs `BanEnforcer.onSetupConnect`, which queries `staff_punishments` directly. The active ban is found, the connect is cancelled with the reason.
|
||||
|
||||
## Why both paths matter
|
||||
|
||||
- **The broadcast** handles "Bob is online right now somewhere else, kick him *now*".
|
||||
- **The DB query on connect** handles "Bob tries to join 30 minutes after his ban; nobody's holding state for him in memory."
|
||||
|
||||
If you only had the broadcast, a player who reconnected after the broadcast fired would walk back in until the next broadcast. If you only had the DB query, a banned player who's already in-game stays in-game until they disconnect.
|
||||
|
||||
## Mutes
|
||||
|
||||
Same flow, except:
|
||||
- `applyLocally` for a mute updates the local `MuteCache` (no kick).
|
||||
- `ChatEnforcer` reads from `MuteCache` on every `PlayerChatEvent` - no DB query per chat message.
|
||||
- On player connect (any server), `PlayerSeenTracker.onConnect` calls `PunishmentService.loadMuteIntoCache(uuid)` which queries `staff_punishments` and populates the cache. This handles "player joins after a mute was issued while they were offline."
|
||||
|
||||
## Unban / unmute
|
||||
|
||||
`PunishmentService.revoke` UPDATEs the row to `active=0`, then publishes a `PunishmentBroadcast` with action `revoked`. Other servers receive, clear the MuteCache entry if relevant. Future connects no longer find an active ban.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| What | What happens | Mitigation |
|
||||
|---|---|---|
|
||||
| Redis is down on the issuing server | Local action happens, broadcast fails silently. Other servers don't know until the player reconnects (then DB query catches it). | Run multiple Redis instances, or accept the short window. |
|
||||
| Receiver's MessageBus is down | Receiver misses the broadcast. Same fallback: next connect attempt queries the DB. | Same. |
|
||||
| MySQL is down | Issue command itself fails with a database error. Nothing propagates. | Don't issue punishments during MySQL outages. |
|
||||
| Auth token mismatch | The receiving server's bus rejects the broadcast as bad-auth. You'll see `messagebus_rejected_total` climb on NetworkCore. | Fix the token across the fleet. |
|
||||
| Receiver has the same player online and the broadcast is lost | Player stays online (and unmuted/unbanned) until the issuing server's heartbeat re-syncs by RPC... which doesn't exist for punishments yet. | If this matters for you, add a periodic reconciliation: every N minutes, each server cross-checks its online players against the active-bans table. Not in v1. |
|
||||
|
||||
The "broadcast lost" case is the only one where you'd see persistent inconsistency. In practice it's rare (Redis pub/sub doesn't lose messages between healthy connections) and self-corrects on the player's next reconnect.
|
||||
|
||||
## What happens when a server crashes mid-broadcast
|
||||
|
||||
- Issuer server crashes before publishing: the row is in MySQL, the broadcast never went. Other servers learn on next connect.
|
||||
- Receiver server crashes mid-receive: the broadcast was published; this server happens to miss it. Same fallback applies for this server.
|
||||
|
||||
There's no transactional guarantee across MySQL and Redis. That's a tradeoff we made deliberately - the simpler "DB is truth, bus is fast notification" model is robust enough.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Operations
|
||||
|
||||
## Smoke test after deploy
|
||||
|
||||
1. `/networkcore status` - confirm "Database: ok" and "Network: connected".
|
||||
2. Connect a staff alt. Run `/lookup <your-main>`. You should see your own UUID and online status.
|
||||
3. Connect a non-staff alt. Have a staff member run `/warn <alt> test`. The alt receives a chat message; nothing crashes.
|
||||
4. `/sc test` from staff. Every other online staff member sees it. No non-staff sees it.
|
||||
5. Look in MySQL:
|
||||
```sql
|
||||
SELECT * FROM staff_punishments ORDER BY issued_at DESC LIMIT 5;
|
||||
```
|
||||
The warn should be the most recent row.
|
||||
|
||||
## A staff member can't run staff commands
|
||||
|
||||
1. Check what groups Hytale shows for them. There's no direct command for this in Staff; have them run `/lookup <theirname>` - if it works, they're staff. If it doesn't, the group is wrong.
|
||||
2. `PermissionsModule.get().getGroupsForUser(uuid)` is the source. Confirm the group name they're assigned matches one in `permissions.staff_groups` in `config.json` (case-sensitive).
|
||||
3. Group changes typically require the player to reconnect.
|
||||
|
||||
## A ban isn't propagating
|
||||
|
||||
1. On the issuing server: `/networkcore servers` - is the destination server in the list? If no, the destination isn't actually on the network.
|
||||
2. On the issuing server: confirm `messagebus_published_total` is incrementing in `/metrics`.
|
||||
3. On the destination server: confirm `messagebus_received_total` is incrementing. If it's rising but `messagebus_rejected_total` is also rising fast, you have an auth token mismatch.
|
||||
4. Confirm the destination server's `network.auth_token` matches the issuer's. Different tokens cause silent message drops.
|
||||
5. As a last resort: the destination server queries MySQL on connect via `BanEnforcer`. Have the target try to reconnect. If the ban still doesn't apply, the row may not actually be in the DB - check `SELECT * FROM staff_punishments WHERE target_uuid = ?`.
|
||||
|
||||
## A muted player can still chat
|
||||
|
||||
1. The mute might have expired. Check `expires_at` against now.
|
||||
2. `MuteCache` on the chat server might be stale. The cache loads on connect; if the mute was issued *while the player was already online*, the broadcast path is what populates the cache.
|
||||
3. If broadcasts aren't arriving, see the previous section.
|
||||
4. As a fallback, kick + rejoin the player. `PlayerSeenTracker.onConnect` calls `loadMuteIntoCache` which forces a fresh DB read.
|
||||
|
||||
## `players_seen.online` is stuck at 1 after a crash
|
||||
|
||||
The plugin runs `markServerOffline(serverId)` in `start()`, which sets `online=0` for all rows whose `last_server_id` matches this server. So a normal restart fixes it.
|
||||
|
||||
If you see online=1 for a player whose `last_server_id` belongs to a server that no longer exists (decommissioned, renamed): clear manually with `UPDATE staff_players_seen SET online=0 WHERE last_server_id = 'old-server-id'`.
|
||||
|
||||
## Auditing
|
||||
|
||||
Recent actions taken by a moderator:
|
||||
|
||||
```sql
|
||||
SELECT issued_at, type, target_name, reason
|
||||
FROM staff_punishments
|
||||
WHERE issued_by_name = 'StaffName'
|
||||
ORDER BY issued_at DESC
|
||||
LIMIT 50;
|
||||
```
|
||||
|
||||
All actions in the last hour:
|
||||
|
||||
```sql
|
||||
SELECT * FROM staff_punishments
|
||||
WHERE issued_at > (UNIX_TIMESTAMP() * 1000) - 3600000
|
||||
ORDER BY issued_at DESC;
|
||||
```
|
||||
|
||||
Players who have been banned but later unbanned:
|
||||
|
||||
```sql
|
||||
SELECT target_name, COUNT(*) AS unbanned_bans
|
||||
FROM staff_punishments
|
||||
WHERE type IN ('BAN', 'TEMPBAN') AND revoked_at IS NOT NULL
|
||||
GROUP BY target_uuid, target_name
|
||||
ORDER BY unbanned_bans DESC;
|
||||
```
|
||||
|
||||
## Things this plugin deliberately doesn't do
|
||||
|
||||
- **IP bans.** Privacy implications and uncertainty about the Hytale auth model. Use the punishment system on UUIDs.
|
||||
- **Appeals workflow.** Staff just unban. There's no in-game appeal form. Build that externally if you want it.
|
||||
- **Multiple active bans on the same player.** The repository's "active ban" query returns the most recent one. Stacking is unsupported by design - it complicates logic without changing outcomes.
|
||||
- **Per-world or per-server bans.** Punishments are network-wide. If you need per-world, that's a different feature (likely a separate plugin built on top of this).
|
||||
@@ -0,0 +1,8 @@
|
||||
# Staff Docs
|
||||
|
||||
1. [01_Overview.md](01_Overview.md) - architecture and pieces
|
||||
2. [02_Configuration.md](02_Configuration.md) - config.json + permission groups
|
||||
3. [03_Commands.md](03_Commands.md) - every command, syntax, behavior
|
||||
4. [04_Schema.md](04_Schema.md) - MySQL tables
|
||||
5. [05_Cross_Server_Propagation.md](05_Cross_Server_Propagation.md) - how bans/mutes propagate
|
||||
6. [06_Operations.md](06_Operations.md) - ops scenarios, debugging
|
||||
Reference in New Issue
Block a user