Player Vault
Ten permission-gated item vaults per player, captured slot by slot as they change and kept safe across restarts. 33 lines of Skript, 91 of SwoftLang; exercises GUIs, editable regions, a persistent map<String>, to_nbt/from_nbt item serialization, and permissions.
nothing to show — {vault::%player%::%n%::%slot%} entries pop into existence on first write, invisibly global
// Player_Vault.sk port — per-player numbered vaults behind permission checks.
// A flat-file backend, flushed on a timer: the vault survives restarts.
storage {
backend: files "data/vaults"
flush: every 30 seconds
}
// The authoritative copy of every occupied vault slot, keyed
// "<uuid>:<vault>:<slot>". Skript's {vault::%player%::%n%::%slot%} list is a
// runtime map; a persistent map<String> is the same idea — and because its
// values are the item stacks serialized with to_nbt, the whole store is
// scalar and persists to the backend, exactly like {vault::*} did.
persistent vault: map<String> = new_map()
function vault_key(owner: String, number: Integer, slot: Integer) {
return "${owner}:${number}:${slot}"
}Why it maps this way
The store is a map<String> — a String-keyed dictionary — and persistent, so it serializes to the flat-file backend and comes back after a restart, exactly like Skript's {vault::*}. The trick that makes item stacks fit a scalar map: each value is the stack run through to_nbt, which turns an Item into its NBT string. See the map reference for the type in full.
command pv <number> [<offline player>]:
trigger:
player's inventory contains "Vault":
send "&cYou already have a vault open!"
stop
set {_target} to player
arg-2 is set:
player is op:
set {_target} to arg-2
else:
send "&cYou don't have permission to view other players' vaults."
stop
arg-1 is between 1 and 10:
player has permission "pv.%arg-1%" or "pv.*":
open chest inventory with 6 rows named "Vault %arg-1%" to player
set {openvault::%player%} to arg-1
loop 54 times:
if {vault::%{_target}%::%arg-1%::%loop-number - 1%} is set:
set slot (loop-number - 1) of player's current inventory to {vault::%{_target}%::%arg-1%::%loop-number - 1%}
else:
send "&cYou don't have permission to open this vault."
else:
send "&cPlease choose a number between 1 and 10."command "pv" {
description: "Open one of your ten vaults"
arguments {
number: Integer
target: optional<Player>
}
execute {
set owner to sender
if args.target exists {
if has_permission(sender, "pv.others") {
set owner to args.target
} else {
send "<red>You don't have permission to view other players' vaults." to sender
halt
}
}
if args.number >= 1 and args.number <= 10 {
if has_permission(sender, "pv.${args.number}") or has_permission(sender, "pv.*") {
open gui "vault" to sender with { vaultnum: args.number, owner: owner.uuid }
} else {
send "<red>You don't have permission to open this vault." to sender
}
} else {
send "<red>Please choose a number between 1 and 10." to sender
}
}
}Why it maps this way
player is op becomes a permission (pv.others) — there is no op bit in the language on purpose. arg-1 is between 1 and 10 is a plain range check. Two things vanish: the "already have a vault open" inventory-name sniff (the runtime owns gui sessions; opening another replaces it) and {openvault::%player%} bookkeeping (the vault number and owner ride along as gui state in open ... with { }, so the on_change handler already knows which vault it is writing).
on inventory close:
name of event-inventory contains "Vault":
set {_vaultnum} to {openvault::%player%}
delete {openvault::%player%}
loop 54 times:
slot (loop-number - 1) of event-inventory is not air:
set {vault::%player%::%{_vaultnum}%::%loop-number - 1%} to slot (loop-number - 1) of event-inventory
else:
delete {vault::%player%::%{_vaultnum}%::%loop-number - 1%}gui "vault" {
rows: 6
title: "Vault ${state.vaultnum}"
editable [0..53] {
on_change {
set key to vault_key("${state.owner}", state.vaultnum, slot)
if new_item.material is "AIR" {
map_delete(vault, key)
} else {
map_set(vault, key, to_nbt(new_item))
}
}
}
on_close {
send "<gray>Vault ${state.vaultnum} saved." to player
}
}Why it maps this way
The .sk loops all 54 slots on close, hand-diffing air. An editable region fires on_change per changed slot with slot, old_item, new_item bound — the runtime does the diffing. Each change writes straight through to the persistent map: to_nbt(new_item) serializes the stack, map_delete clears a slot emptied back to AIR. Because the map is persistent, the whole vault is on disk the moment it changes — no close-time sweep required.
Skript reads {vault::...} from anywhere, no proof required — a typo'd index is just <none>
command "pvpeek" {
description: "Inspect one stored vault slot"
arguments {
number: Integer
slot: Integer
}
execute {
set stored to vault[vault_key("${sender.uuid}", args.number, args.slot)]
if stored exists {
set restored to from_nbt(stored)
if restored exists {
send "Vault ${args.number} slot ${args.slot}: ${restored.material} x${restored.amount}" to sender
} else {
send "Vault ${args.number} slot ${args.slot} holds unreadable data." to sender
}
} else {
send "Vault ${args.number} slot ${args.slot} is empty." to sender
}
}
}Why it maps this way
Two optionals, two exists gates — both enforced by the compiler, not by discipline. vault[key] is the index-read sugar for map_get; it yields optional<String> (the slot may be empty). from_nbt turns the stored NBT string back into an Item, and it is also optional — malformed data is provably missing, never a crash. The full round-trip, to_nbt on write and from_nbt on read, is what lets a scalar map hold real item stacks.