AstralPunishments Overview
AstralPunishments is the network's punishment system — bans, IP bans, mutes, warns and kicks. It ships as two modules that share one MariaDB schema and one Redis-backed messaging channel:
velocity— a Velocity plugin (depends onastralcore) that owns every punishment command (/ban,/ipban,/mute,/warn,/kick,/unban,/unmute) and enforces bans at proxy login.paper— a Paper plugin that owns the staff-facing punishment history GUI (/history) and thepunishment_*placeholder used to render it. It does not issue punishments itself.
Both modules read and write the same punishments table through their own independent PunishmentRepository/PunishmentService pair (common/repository/PunishmentRepository, velocity/service/PunishmentService, paper/service/PunishmentService) — there is no RPC between the two for CRUD; they're kept in sync purely by sharing the database and by broadcasting cache-invalidation packets.
Punishment types
PunishmentType (common/model/PunishmentType.java) has five values: BAN, IPBAN, MUTE, KICK, WARN. They fall into two behavioral groups:
Stateful (
BAN,MUTE,IPBAN) — enforce an ongoing state, so at most one active punishment of that type exists per target at a time. Issuing the same type again while one is already active reissues the existing row (PunishmentEntity#reissue) instead of creating a duplicate — the id and its pardon history are preserved, only the issuer, reason, start time, expiry (and, for IP bans, the address) are refreshed.Point-in-time (
KICK,WARN) — a one-off record with nothing to lift; a new row is always created.
Only BAN, IPBAN and MUTE can be pardoned (/unban, /unmute, or the history GUI's pardon button) — KICK and WARN are historical only.
Data model
Every punishment — active, expired, or pardoned — is one row in the shared punishments table (velocity/src/main/resources/schema.sql), mapped by PunishmentEntity:
Field | Column | Notes |
|---|---|---|
|
| Primary key. |
|
|
|
|
| Nullable — null on a pure IP ban with no known account. |
|
| The banned address ( |
|
| Null id = console; name defaults to |
|
| Nullable free text. |
|
| Set on issue; refreshed on reissue. |
|
|
|
|
| Flipped to |
|
| Populated by |
A second table, ip_history, records one row per (player_uuid, ip) pair, upserted by IpHistoryListener on every PostLoginEvent. It backs /ipban when the target isn't currently connected (the ban falls back to their last-known address) and has no other consumer today.
Enforcement is evaluated live at query time (active = true AND (expires_at IS NULL OR expires_at > now)) — there's no background sweeper; a pardon or an expiry takes effect on the very next check.
Cross-server sync
Both modules construct MessagingService with a PunishmentsPacketRegistry (common/messaging/PunishmentsPacketRegistry), which registers two request/response packets:
Packet | Opcode | Purpose |
|---|---|---|
|
| Sent by |
|
| Reply carrying whether the disconnect succeeded. |
PunishmentRepository (the common module, used independently by both velocity and paper) additionally registers an exchange on punishments.updates (PunishmentConstants.UPDATES_CHANNEL) that carries a PunishmentIssuedPacket(UUID) — sent after every save()/pardon() — and, on receipt, refreshes that punishment's entry in the local Caffeine cache. This is what makes a pardon issued from the paper history GUI take effect immediately at the velocity login gate (BanListener) without either side polling the other.
velocity's PunishmentService additionally listens on punishments.global (PunishmentConstants.PUNISHMENTS_CHANNEL) for KickPlayerRequestPackets and answers them via localKick(...).
Login enforcement
BanListener (velocity, PreLoginEvent) is the only enforcement point. For every connection attempt that another listener hasn't already denied, it calls PunishmentService#findActiveBan(playerId, ip) (PunishmentRepository#findActiveLoginBan), which returns whichever of an active account BAN for the player or an active IPBAN on the connecting address was issued more recently — pardoned or expired rows never block. A lookup failure (e.g. a transient database error) fails open: the connection is allowed rather than the whole network being locked out.
The denial screen is PLAYER_IPBANNED for an IP ban or PLAYER_BANNED for an account ban, rendered with %reason% and %duration% ("Permanent" or a human-readable duration).
Where to go next
Configuration —
database.properties,messages.yml(both modules),display.yml.Commands — every velocity punishment command plus paper's
/history.Placeholders — the
punishment_*namespace.Punishment History GUI — the
/historymenu flow and the pardon action.