Staff Tools¶
Vanish, Freeze, Godmode, No-Collision, and Staff Mode - five staff-facing
capabilities owned by the Moderation module (see
moderation.md), not a separate module: the permission
nodes are explicitly under universaladmin.moderation.*/
universaladmin.bypass.*, and Staff Mode's tool kit reuses Moderation's
punishment history/GUI directly. Built entirely on stable Paper/Bukkit
public API - no NMS, no packet hacks, no ProtocolLib dependency
anywhere in this module.
Freeze is a punishment (targets another player, goes through the
Moderate wizard like Ban/Mute/Warn - see moderation.md).
Vanish/Godmode/No-Collision/Staff Mode are self-directed (a staff member
toggles their own state) and live as buttons on the Moderation home page
(/admin → Moderation) instead.
Vanish¶
universaladmin.moderation.vanish toggles your own vanish status;
universaladmin.bypass.vanish lets a viewer see through anyone else's.
What it does, per toggle:
- Hide/show - Player#hidePlayer(plugin, viewer)/showPlayer(...) for
every online viewer without the bypass permission, plus
Entity#setVisibleByDefault(false) so a player who joins after you
vanish doesn't need a retroactive per-viewer call.
- Tab list - unlistPlayer/listPlayer per viewer, same bypass split.
- Staff visibility - handled by VanishVisibilityPolicy
(canSee(viewer, vanishedPlayerId, level)); the default implementation
checks universaladmin.bypass.vanish. VanishLevel is a one-value enum
today (STANDARD) deliberately shaped so a future tier (e.g. hidden even
from junior staff) is an additive change, not a rewrite - see
moderation.md#policy-hook-staff-hierarchy
for the identical reasoning behind ModerationPolicy. No rank system
exists yet; this only prepares the seam.
- Collision - disabled automatically while vanished (composed with the
standalone No-Collision toggle and Staff Mode's auto-no-collision - see
"No-Collision" below for how those combine).
- Mob targeting - EntityTargetLivingEntityEvent is cancelled when the
target is a vanished player.
- Item pickup - optional (moderation.vanish.block-item-pickup,
default on): EntityPickupItemEvent cancelled for a vanished player.
- Join/quit messages - optional (moderation.vanish.hide-join-message/
hide-quit-message, both default on): nulled via PlayerJoinEvent#joinMessage(null)/
PlayerQuitEvent#quitMessage(null) for the real connect/disconnect of a
persistently-vanished player.
- Reconnect restore - optional (moderation.vanish.restore-on-reconnect,
default on): if you disconnect while vanished, reconnecting re-applies it
automatically.
- GUI status - the Moderation home page's Vanish button shows the
current ON/OFF state.
Fake join/leave¶
Two more settings, both default off (opt-in): moderation.vanish.fake-leave-on-vanish
broadcasts a normal-looking "X left the game" message (to everyone without
the bypass permission) when you vanish; moderation.vanish.fake-join-on-unvanish
does the same with "X joined the game" on unvanish. These are cosmetic chat
messages only - the real audit entry for the vanish toggle is always
recorded via ActionExecutor regardless of this setting, so enabling it
never affects what's auditable, only what non-staff players see in chat.
Vanish limitations¶
Paper/Bukkit's public API does not give a plugin full control over every way a vanished player might be detected, and this module does not reach for NMS to close those gaps. Documented transparently rather than left as a surprise:
- Sound - footsteps, block breaking/placing, and other server-simulated sounds a vanished player causes are still audible (positionally) to nearby non-staff players. Bukkit has no public API to suppress sound emission per-listener for arbitrary player-caused sounds.
- World changes - a vanished player's placed/broken blocks, thrown items, etc. are ordinary world state; nothing about vanish hides the effects of what they do, only their entity.
/listand raw player-list scans - vanilla/list(run from console, or by another plugin readingBukkit.getOnlinePlayers()directly instead of going through visibility-aware APIs) is not filtered by this module. A vanished player still counts as "online" at that level.- Other plugins - any plugin that tracks players via its own listener
or a raw entity/player list (rather than checking
Player#canSee) may still reveal a vanished player through its own UI (e.g. a separate scoreboard/tab-list plugin).
None of this is a bug in this implementation - it's the actual boundary of what a plugin can influence without packet interception, which docs/development/architecture-rules.md rules out for this project.
Freeze¶
universaladmin.moderation.freeze/.unfreeze. A punishment (see
moderation.md) - reason required, no
duration (unfreeze explicitly to end it), reachable from the Moderate
wizard or the Freeze Tool (see "Staff Mode tools" below). If the target is
online, FreezeAction sends them an immediate notice with the reason -
same shape as MuteAction's notice, sent once at freeze time rather than
repeated on every blocked action (movement in particular fires far more
often than a chat message, so a per-attempt reminder there would spam
them instead of informing them). UnfreezeAction does not send a
notice, matching UnmuteAction's precedent.
Five independently configurable blocks, all default on:
| Setting | Blocks |
|---|---|
moderation.freeze.block-movement |
Position changes (PlayerMoveEvent, only when position actually changed - looking around is still allowed) |
moderation.freeze.block-teleport |
PlayerTeleportEvent |
moderation.freeze.block-interaction |
PlayerInteractEvent |
moderation.freeze.block-inventory |
InventoryClickEvent in vanilla inventories - clicks inside a UniversalAdmin GUI page are never blocked, so a frozen staff member isn't locked out of the plugin's own menus |
moderation.freeze.block-commands |
PlayerCommandPreprocessEvent |
What a freeze looks like. The frozen player sees a title ("Frozen -
stay where you are") until they are unfrozen
(moderation.freeze.show-title). Staff with
universaladmin.moderation.freeze see a marker above the frozen player's
head with who froze them and for how long
(moderation.freeze.staff-marker). The marker is a text display that is
hidden from everyone else and never saved with the world. Both default
on.
Every check reads an in-memory cache (FreezeRuntimeState), never the
database - PlayerMoveEvent fires on every tick a player moves, so a
per-event DB query would be a real performance problem, not just a style
concern. The cache is populated from the database once at
AsyncPlayerPreLoginEvent (async-safe, before enforcement is even
possible) and updated directly by Freeze/Unfreeze.
Disconnect while frozen is audited and reported to staff
(NotificationService.notifyStaff, gated on universaladmin.moderation.freeze)
- routed through ActionExecutor with a system actor
(ModerationActionIds.FREEZE_DISCONNECT_NOTICE) rather than the listener
calling AuditService directly, since a module is never allowed to write
audit entries outside that one hook.
Godmode¶
universaladmin.moderation.godmode toggles your own invulnerability via
Entity#setInvulnerable(boolean) directly - no EntityDamageEvent
listener anywhere. This is deliberate: cancelling damage events per-hit
carries side effects (knockback still applying, fire-tick visuals lingering,
per-cause edge cases) that setInvulnerable avoids entirely by blocking
damage at the source. Status shown on the Moderation home page's Godmode
button. Godmode stays on across leaving and restarts: it is kept as a mark
(universaladmin:godmode) in the player's own data, not in the database.
The invulnerability itself is not read to decide whether godmode is on,
because Minecraft also reports the few seconds of protection after joining
or a hit as invulnerable.
No-Collision¶
universaladmin.moderation.collision toggles your own collision via
LivingEntity#setCollidable(boolean). Composed from three independent
inputs (manual toggle, Vanish, Staff Mode's optional auto-no-collision) so
turning any one off doesn't clobber the others - see CollisionState#refresh.
Not persisted, same reasoning as Godmode.
Staff Mode¶
universaladmin.moderation.staffmode enters/exits (same button on the
Moderation home page, label changes based on current state).
Entering¶
- Your current inventory (36 main storage slots + helmet/chestplate/ leggings/boots/offhand), gamemode, XP, level, and flight state are snapshotted and persisted to the database before anything is touched - see "Crash safety" below for why this ordering matters.
- Only once that write has confirmed: your inventory is cleared and
replaced with the eight-tool staff kit (see "Tools"), unconditional
protections turn on (damage via
setInvulnerable, item pickup, block breaking), and the optional auto-toggles apply per settings:
| Setting | Default | Effect on entry |
|---|---|---|
moderation.staffmode.auto-fly |
on | Grants flight |
moderation.staffmode.auto-vanish |
on | Vanishes you |
moderation.staffmode.auto-nocollision |
on | Disables collision |
Exiting¶
Restores the snapshot exactly (inventory, gamemode, XP, level, flight), reverses the auto-toggles, and deletes the snapshot row.
Crash safety and recovery¶
The snapshot write happens before the inventory is ever mutated - not a flag, an ordering property: the code path that clears your inventory literally cannot run until the database save future has already resolved successfully. If the server crashes between those two steps, nothing was mutated - you keep your real inventory, and there is no snapshot for the next login to even find. If it crashes after entry succeeded (you're mid-session with the staff kit), the snapshot survives the crash in the database, and recovery runs automatically at your next login - your real inventory is restored before you can do anything with the leftover staff kit, and you're notified in chat.
If a snapshot already exists for you (entering while one is somehow still
pending - a bug, a race, whatever), entering again is refused
(CONFLICT), never silently overwritten - the existing snapshot could be
your real, unrecovered inventory.
universaladmin.moderation.staffmode.recover lets an admin trigger
recovery manually: /admin staff recover (self) or /admin staff recover <player>
(another online player - recovery needs a live inventory to hand items
back to; an offline target fails with a clear error). This is the explicit
recovery path the automatic login-time recovery is meant to make mostly
unnecessary, not a replacement for it.
Tools¶
Nine items fill your hotbar on entry, each tagged via
PersistentDataContainer/NamespacedKey (not matched by display
name/lore, so a renamed item is still recognized and nothing else can be
mistaken for one). Every tool click cancels the triggering interaction
before dispatching - a Player-Inspector-holding staff member right-clicking
a cow doesn't feed it, right-clicking a block doesn't place/break it.
PlayerInteractEntityEvent fires once per hand for a single physical
click; every entity-targeted tool below only reacts to the main-hand
firing, so a click never dispatches twice.
| Tool | Trigger | Effect |
|---|---|---|
| Player Inspector | Aim at a player, right-click | Opens that player's punishment history. Its icon/name/lore also live-update to whoever is currently in your crosshair - see "Live target display" below - regardless of which tool you're actually holding. |
| Freeze Tool | Aim at a player, right-click | Toggles Freeze/Unfreeze immediately (fixed reason "Staff Freeze Tool" - no prompt, so using it never interrupts world interaction) |
| Inventory Inspector | Aim at a player, right-click | Opens the Players module's live inventory editor (see players.md) - changes show up in both directions within a tick. Editing requires universaladmin.players.inventory.edit; without it, view-only. |
| Ender Chest Inspector | Aim at a player, right-click | Same as the Inventory Inspector, for their ender chest - requires universaladmin.players.enderchest.edit to write. |
| Teleport to/Bring Player | Right-click air | Opens a picker over every online player, then a "Teleport to them" / "Bring them to me" choice - runs through the Players module's own audited teleport action (TeleportKind.ADMIN_TO_PLAYER/BRING_TO_ADMIN), unlike "Teleport to Player" below. |
| Vanish Toggle | Right-click air | Runs the Vanish action on yourself |
| Moderate Tool | Aim at a player, right-click | Opens the full Moderate wizard (Kick/Ban/Mute/Warn) for that player directly - no need to leave Staff Mode and find them again in the Player Browser |
| Teleport to Player | Aim at a player, right-click | Teleports you to that player, through the Players module's audited teleport action (needs universaladmin.players.teleport) |
| Exit Staff Mode | Right-click air | Runs Exit Staff Mode |
While Staff Mode is active (regardless of which tool is held, or none): item pickup and block breaking are unconditionally cancelled, and you are invulnerable.
Live target display¶
StaffModeTargetTracker runs every 10 ticks (twice a second) for every
online Staff-Mode-active player. Unlike a vanilla ray trace, it deliberately
does not stop at walls: it searches every online player within
moderation.staffmode.target-range-blocks (default 128 blocks, ignores line
of sight entirely) for whoever is closest to your exact look direction,
within a narrow angular tolerance. The tools use the same search: a
right-click in the air acts on exactly the player the actionbar names, as
far away as that range - you don't have to walk up to them. Right-clicking
a player directly still works too. If it finds someone:
- The Player Inspector tool always shows their real skin, name, and a quick Frozen/Vanished status line, whichever tool you're actually holding.
- Whichever other status-aware tool you're currently holding (Freeze Tool, Inventory/Ender Chest Inspector, Moderate Tool, Teleport to Player) shows the same status line too, reset back to its default icon the moment you look away or switch tools.
- An actionbar message ("Looking at:
<name>", or "Looking at: nobody") is resent every tick a target exists, keeping it visible continuously instead of fading out after Minecraft's default few seconds.
A tool click searches again at the moment of the click (it never acts on a stale target), and one physical click runs a tool only once - even when Minecraft reports it as both "clicked a player" and "clicked the air" (otherwise the Freeze Tool would freeze and immediately unfreeze).
Tool kit is locked¶
The hotbar (and the rest of your inventory, which Staff Mode leaves
otherwise empty) cannot be rearranged, dropped, or shift-clicked into
another inventory while Staff Mode is active -
StaffModeGuardListener cancels InventoryClickEvent/InventoryDragEvent/
PlayerDropItemEvent outright for a Staff-Mode-active player, except
inside one of UniversalAdmin's own GUI pages (Player Inspector, Inventory
Inspector, Ender Chest Inspector, Moderate), which GuiListener already
owns completely on its own. StaffModeTargetTracker's periodic pass also calls
StaffToolItems#restoreIfTampered, which re-gives the whole kit if any
tool slot is ever found empty or wrong - a backstop for an edge case the
click/drag/drop cancellation didn't anticipate, not the primary defense.
Permissions¶
| Node | Default | Meaning |
|---|---|---|
universaladmin.moderation.vanish |
op | Toggle your own vanish status |
universaladmin.bypass.vanish |
op | See vanished players |
universaladmin.moderation.godmode |
op | Toggle your own godmode status |
universaladmin.moderation.collision |
op | Toggle your own no-collision status |
universaladmin.moderation.staffmode |
op | Enter/exit staff mode |
universaladmin.moderation.staffmode.recover |
op | Manually recover a pending staff-mode snapshot |
universaladmin.moderation.freeze |
op | Freeze a player |
universaladmin.moderation.unfreeze |
op | Unfreeze a player |
See docs/user/permissions.md for the platform-wide list.
Database¶
Two new tables, alongside punishments (Freeze reuses that table - see
moderation.md):
vanish_state(VanishStateMigrationversion 1004) -player_idprimary key,vanished_at. Row existence means "was vanished, restore on reconnect if enabled" - purely for persistence; the in-memoryVanishRuntimeStateis what every hot-path listener actually reads.staff_mode_snapshots(StaffModeSnapshotMigrationversion 1005) -player_idprimary key,inventory_data(ItemStack.serializeItemsAsBytes(...)over the combined 36+5-slot array,MEDIUMBLOBon MySQL/MariaDB since the defaultBLOBcap is too tight for a full inventory of heavy-NBT items, plainBLOBon SQLite),gamemode,exp,level,allow_flight,flying,created_at. One row per player; existence is "has a pending snapshot".
Tests¶
VanishRuntimeStateTest/FreezeRuntimeStateTest/StaffStateTrackersTest
(toggle idempotency for every in-memory tracker), StaffModeServiceTest
(the enter/exit decision branches that short-circuit before ever touching
a live inventory - "snapshot already exists" and "nothing to restore" -
against a fake StaffModeSnapshotRepository; the actual Bukkit inventory
mutation needs a running Paper server, same documented exclusion
PlayerServiceTest uses for its own snapshot()), and
JdbcStaffModeSnapshotRepositoryTest (real temp SQLite, a synthetic
byte[] payload rather than real ItemStack.serializeAsBytes() output -
proves the BLOB column/dialect/migration round-trip independent of live
Bukkit item serialization).
Commands¶
/vanish (/v), /god, /nocollision, /staff, /admin staff recover [player],
and /freeze//unfreeze - see commands.md.