Astral Realms Documentation Help

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 on astralcore) 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 the punishment_* 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

uniqueId

id

Primary key.

type

type

PunishmentType, persisted as its ordinal (INT).

playerId/playerName

player_uuid/player_name

Nullable — null on a pure IP ban with no known account.

ip

ip

The banned address (IPBAN) or the address recorded at punish-time (other types).

operatorId/operatorName

operator_uuid/operator_name

Null id = console; name defaults to "Console".

reason

reason

Nullable free text.

createdAt

created_at

Set on issue; refreshed on reissue.

expiresAt

expires_at

null or -1 = permanent (isPermanent()); otherwise a millisecond timestamp (isExpired() compares against "now").

active

active

Flipped to false by revoke() /pardon; history rows are never deleted.

removedById/removedByName/removedAt/removedReason

removed_*

Populated by revoke() when pardoned.

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

KickPlayerRequestPacket

0x00

Sent by velocity's PunishmentService#kick when the target isn't connected to the issuing proxy, asking whoever holds that connection to disconnect them.

KickPlayerResponsePacket

0x01

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

Last modified: 25 July 2026