Astral Realms Documentation Help

AstralPrivateChest Overview

AstralPrivateChest lets a player lock a chest (or trapped chest) inside their own town's claims, restricting who can open it, and marks it with a floating "lock" item hovering on one of its faces. It is a single Paper module (com.astralrealms.privatechest.AstralPrivateChest) that hard-depends on AstralCore, AstralTown, and PacketFramework (for the lock display), and soft-depends on AstralPlayerShops.

Locking a chest

Right-clicking a CHEST or TRAPPED_CHEST while holding the configured lock item (see Configuration) triggers ChestListener#onChestInteract, which:

  1. Does nothing if the chest is already locked — replies already-locked.

  2. Requires the player to be a member of the town whose claim covers the block (TownAPI.isWithinOwnTownClaims) — otherwise replies not-within-town-claims.

  3. If AstralPlayerShops is installed, refuses to lock a block that is a registered player shop — replies cannot-lock-shop.

  4. Otherwise creates the lock (LockService#create): persists a new ChestData(owner, ownerName, empty authorizedUsers, face) to the block's PersistentDataContainer, indexes it, and spawns the lock display. The clicked face is resolved by FaceGeometry#resolve — a horizontal clicked face (NORTH/SOUTH/EAST/WEST) is used as-is; a top/bottom click instead uses the chest side nearest the player's facing direction.

  5. On success, one lock item is consumed from the player's hand (the whole stack if amount <= 1, otherwise amount - 1) and chest-locked is sent, and only then is the interaction event cancelled. In every other outcome above (already locked, outside claims, shop block, or LockService#create failing) the event is left uncancelled, so the chest still opens normally on that click.

Access control

ChestData#canAccess(player) is true for the chest's ownerId, any UUID in authorizedUsers, or any player holding privatechests.bypass. ChestListener#onChestAccess (PlayerInteractEvent, HIGH priority, ignoreCancelled = true) cancels the interaction and sends cannot-access (with the privatechest_* placeholders — see Placeholders) for anyone who fails that check.

Hopper automation into/out of a locked chest is also blocked regardless of who placed it: ChestListener#onItemMove cancels every InventoryMoveItemEvent whose source inventory holder is a locked Chest, and (via the AstralPlayerShops hook) PlayerShopListener#onHopperPlace cancels placing a HOPPER directly below a locked chest (i.e. the block one above the newly-placed hopper is a locked chest) unless the placer is the owner or holds privatechests.bypass.

The same hook also guards the other direction: PlayerShopListener#onShopCreate listens for AstralPlayerShops' PlayerShopPreCreateEvent and cancels it whenever the target block is already a locked chest, so a locked chest can never be turned into a player shop after the fact (mirroring the cannot-lock-shop check above, which prevents locking a block that is already a shop).

Unlocking

Breaking a locked chest (BlockBreakEvent) is handled by ChestListener#onBlockBreak:

  • The owner, or a player with privatechests.bypass, breaking it removes the lock data and display, cancels the vanilla break (the chest block itself is never destroyed), drops a freshly built lock item at the block's location, and sends chest-unlocked.

  • Anyone else breaking it has the break cancelled and gets cannot-access.

Members

Beyond the owner, a chest can have any number of authorizedUsers (a Set<UUID>) who also pass canAccess. These are managed with /pc add|remove|list — see Commands. Only the owner may add/remove members; anyone else attempting to gets not-owner.

Double chests

A double chest is locked/unlocked/indexed/rendered as one unit. PCUtils#getOtherChestBlockIfDouble resolves the adjacent half via the shared DoubleChest inventory holder:

  • Storage (LockRepository): data is written to whichever half is passed to setData/create, and reads fall back to the other half if the queried block has none — so both halves resolve to the same ChestData. dataBlockOf finds whichever of the two blocks actually carries the PDC entry.

  • Indexing (LockService#indexBothHalves): both block positions are put into the in-memory LockIndex pointing at the same ChestData instance.

  • Display (DisplayManager#displayBase): for a face perpendicular to the join axis (a front/back face — see FaceGeometry#spansBothHalves), the lock item is centered between the two halves rather than anchored to a single block; a face along the join axis (an end face), or a single chest, anchors to the block.

Persistence & the in-memory index

ChestData is stored on the chest's (or trapped chest's) TileState PersistentDataContainer under astralprivatechest:chest_data, serialized by the custom PersistentDataType<PersistentDataContainer, ChestData> implementation ChestDataType (owner id via a shared UUIDDataType, owner name as a string, each authorized UUID packed as 16 raw bytes in a byte-array list, and the face — if non-null — as its enum name).

Because reading a TileState's PDC force-loads the chunk, LockService also keeps an authoritative in-memory index (LockIndex, chunk-bucketed Long2ObjectMaps of packed block-position longs, main-thread only) covering every locked chest in a currently-loaded chunk. findByBlock — used on every protection-path listener — serves from this index only; a miss means "not locked," without touching the PDC. The index is populated per-chunk on ChunkLoadEvent (re-reading the PDC of every chest tile entity in that chunk) and cleared per-chunk on ChunkUnloadEvent. A separate findByBlockUncached reads straight from PDC (force- loading the chunk if needed) and is used only by the coordinate-based /pc remove variant, never on the hot protection path.

Lock display

DisplayManager renders the lock icon as a PacketItemDisplay (a client-side-only, packet-driven item display from PacketFramework) rather than a real entity, cached per Location. On lock creation the display is spawned with only the locking player as a viewer; on server (re)start it is respawned for every loaded chunk's locked chests with no restricted viewer (visible to everyone). Position/orientation come from FaceGeometry#offset/#rotation for the stored face, FIXED billboard. Displays are torn down per-chunk on ChunkUnloadEvent and individually on unlock.

The lock item

The lock item template is config.yml's lock-item (an ItemStackWrapper; see Configuration). LockService#buildItem builds a fresh copy and stamps a marker (astralprivatechest:lock_item, PersistentDataType.INTEGER = 1) onto its PDC; isLockItem checks for that marker, so only items built this way (dropped on unlock, or acquired via the item supplier below) count as a valid lock item for locking a chest — a manually-crafted look-alike item does not.

AstralPrivateChest also registers an ItemStackSupplier under the namespace privatechest (AstralPaperAPI.registerItemStackSupplier("privatechest", new PCItemSupplier(this))) with one key, lock — so other plugins/menus can reference the lock item as material: "privatechest-lock".

Where to go next

Last modified: 25 July 2026