Astral Realms Documentation Help

Punishment History GUI

The paper module's whole purpose is /history — a staff GUI over a player's full punishment record, with an in-menu pardon flow. It's built entirely on AstralCore's YAML blueprint menu framework: no hand-written Java menu classes, three blueprints under menus/punishments/, one custom action verb ([pardon-punishment]), and a MenuService that owns the shared MenuContainer and open/fetch/filter logic for both the command and the pardon action.

Flow

/history <player> [duration] │ ▼ punishment-history ── click a row ──▶ punishment-detail ── click Pardon ──▶ punishment-confirm-pardon ▲ │ (back) │ └─────────────────────────────────────┘◀──────────────── cancel ─────────────┘ confirm → [pardon-punishment] → re-opens punishment-history

HistoryCommand#onHistory parses the optional [duration] filter (see Commands → duration syntax) and calls MenuService#openHistory(viewer, target, filter), which:

  1. Fetches every punishment ever recorded against the target (PunishmentService#findHistoryPunishmentRepository#findByTarget) — active, expired, and pardoned alike.

  2. If filter is set, keeps only rows with createdAt >= now - filter (a recency window, not a type filter).

  3. Sorts newest-first and wraps each row in a PunishmentPlaceholder.

  4. If the resulting list is empty, replies no-history and opens nothing.

  5. Otherwise opens the punishment-history menu via MenuContainer#computeAndOpen, passing two open parameters: punishments (the wrapped list, as an ItemProvider) and target (MinecraftPlayerPlaceholder).

punishment-history

54-slot paginated list. The punishments layout (taint: "punishments", provider: "%parameters_punishments%") fills 28 content slots with one punishment item per row, each copy-from the type's display.yml icon with reason/operator/date/duration/status appended as lore. Left-clicking a row opens the detail menu for that specific punishment:

actions: - "[open-menu] punishment-detail:punishment=%parameter_punishment%:target=%parameters_target%:punishments=%parameters_punishments%"

Standard prev/next navigation (slots 46/52) is gated on %layouts_punishments_hasPreviousPage%/%layouts_punishments_hasNextPage%; a close button sits at slot 49.

punishment-detail

54-slot single-punishment view, opened with punishment, target, and punishments (the last carried forward purely so Back can reopen the list without re-fetching). Shows the full info block (reason/operator/date/duration/status) plus two conditional elements:

  • Retrait (revocation) info — reason/pardoner/date — shown only when %parameters_punishment_active% == false.

  • Pardon button — shown only when %parameters_punishment_pardonable% == true (active, not expired, and type is BAN/IPBAN/MUTE) — opens the confirmation menu, forwarding the same three parameters.

Back reopens punishment-history with the original target/punishments parameters (no re-fetch, no filter re-applied — if a duration filter narrowed the original list, going back preserves that narrowed view since the same punishments list is passed through unchanged).

punishment-confirm-pardon

54-slot confirm/cancel step. Cancel returns to punishment-detail unchanged. Confirm fires the custom action:

actions: - "[pardon-punishment] %parameters_punishment_id% %parameters_target_uuid%"

[pardon-punishment] action

Backed by PardonPunishmentAction (paper/action/PardonPunishmentAction), registered as registerAction("pardon-punishment", PardonPunishmentAction.class).

Arg

Type

Description

punishmentId

PlaceholderWrapper<UUID>

The punishment to pardon.

targetId

PlaceholderWrapper<UUID>

The punishment's target, used only to re-open the history menu afterward.

- "[pardon-punishment] %parameters_punishment_id% %parameters_target_uuid%"

Behavior, in order:

  1. Loads the punishment by id (PunishmentService#findById). Missing or a lookup failure → error-occurred, logged via SLF4J.

  2. If it's already inactive (someone else pardoned it in the meantime) → already-pardoned.

  3. Maps its type to the permission node required to lift it: punishments.unban for BAN/IPBAN, punishments.unmute for MUTE. KICK/WARN have no mapping → cannot-pardon (defensive; the button is already hidden for these types via punishment_pardonable).

  4. If the clicking player lacks that node → no-permission.

  5. Otherwise pardons it (PunishmentService#pardonPunishmentRepository#pardon, no reason — GUI pardons always persist removedReason = null, unlike /unban//unmute which accept one), replies pardon-success, and re-opens punishment-history for the target (MenuService#openHistory(player, targetId) — the unfiltered overload, so any duration filter from the original /history call is dropped on this refresh).

Pardoning here reuses the exact same PunishmentRepository#pardon path as /unban//unmute — it flips active, records the revocation fields, and broadcasts a PunishmentIssuedPacket on punishments.updates, so the lift is visible at the velocity login gate immediately. See Overview → Cross-server sync.

See also

Last modified: 25 July 2026