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:
Does nothing if the chest is already locked — replies
already-locked.Requires the player to be a member of the town whose claim covers the block (
TownAPI.isWithinOwnTownClaims) — otherwise repliesnot-within-town-claims.If AstralPlayerShops is installed, refuses to lock a block that is a registered player shop — replies
cannot-lock-shop.Otherwise creates the lock (
LockService#create): persists a newChestData(owner, ownerName, empty authorizedUsers, face)to the block'sPersistentDataContainer, indexes it, and spawns the lock display. The clicked face is resolved byFaceGeometry#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.On success, one lock item is consumed from the player's hand (the whole stack if
amount <= 1, otherwiseamount - 1) andchest-lockedis sent, and only then is the interaction event cancelled. In every other outcome above (already locked, outside claims, shop block, orLockService#createfailing) 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 sendschest-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 tosetData/create, and reads fall back to the other half if the queried block has none — so both halves resolve to the sameChestData.dataBlockOffinds whichever of the two blocks actually carries the PDC entry.Indexing (
LockService#indexBothHalves): both block positions are put into the in-memoryLockIndexpointing at the sameChestDatainstance.Display (
DisplayManager#displayBase): for a face perpendicular to the join axis (a front/back face — seeFaceGeometry#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
Configuration —
config.yml(lock item) andmessages.yml.Commands —
/pc add|remove|list|reload.Placeholders — the
privatechest_*namespace.