Files
minecraft-aiworld/postal-service-addon/postal_service_BP/scripts/main.js
SysAdmin f126eeb955
All checks were successful
Deploy Addons / deploy (push) Successful in 14s
feat(postal): multi-mailbox per player with labels, redirect flow, icon refresh
Players can now place up to 5 mailboxes, each labelled like a lodestone
waypoint. Sending mail picks recipient then mailbox; redirect collapses
one of your own mailboxes into another and removes the source.

- v1 -> v2 schema migration runs once on boot; existing claims default to
  label "Mailbox".
- Two-step send picker (skipped when recipient has only one mailbox).
- Post office root menu adds Redirect option.
- Per-entry break handling so removing one mailbox keeps the others claimed.
- minecraft:icon component + 16x16 inventory icons for both blocks.
- Refreshed pack_icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:12:36 +01:00

823 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { world, system, ItemStack } from "@minecraft/server";
import { ActionFormData, MessageFormData, ModalFormData } from "@minecraft/server-ui";
// ─── Constants ──────────────────────────────────────────────
const MAILBOX_BLOCK = "silverlabs:mailbox";
const POST_OFFICE_BLOCK = "silverlabs:post_office";
const VANILLA_CHEST = "minecraft:chest";
const PROP_KEY_V1 = "postal_state_v1";
const PROP_KEY_V2 = "postal_state_v2";
const MAX_MAILBOXES_PER_PLAYER = 5;
const LABEL_MAX_LEN = 24;
// ─── State ──────────────────────────────────────────────────
// mailboxes: { "x,y,z,dim": { ownerId, ownerName, mailboxId, label } }
// registry: { [ownerId]: { name, mailboxes: [{ mailboxId, label, x, y, z, dim }] } }
// pending: { [ownerId]: [{ from, itemSummary, mailboxLabel, ts }] }
let state = { mailboxes: {}, registry: {}, pending: {} };
function uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function loadState() {
// Try v2 first
try {
const rawV2 = world.getDynamicProperty(PROP_KEY_V2);
if (rawV2 && typeof rawV2 === "string") {
const parsed = JSON.parse(rawV2);
state = {
mailboxes: parsed.mailboxes || {},
registry: parsed.registry || {},
pending: parsed.pending || {},
};
return;
}
} catch (e) {
world.sendMessage(`§c[Postal] Failed to load v2 state: ${e.message}`);
}
// One-shot v1 → v2 migration
try {
const rawV1 = world.getDynamicProperty(PROP_KEY_V1);
if (rawV1 && typeof rawV1 === "string") {
const v1 = JSON.parse(rawV1);
const next = { mailboxes: {}, registry: {}, pending: v1.pending || {} };
// Each v1 mailbox entry → v2 entry with synthetic mailboxId + default label
for (const [k, entry] of Object.entries(v1.mailboxes || {})) {
next.mailboxes[k] = {
ownerId: entry.ownerId,
ownerName: entry.ownerName,
mailboxId: uuid(),
label: "Mailbox",
};
}
// v1 registry shape: { [ownerId]: { name, x, y, z, dim } }
for (const [ownerId, reg] of Object.entries(v1.registry || {})) {
const k = `${reg.x},${reg.y},${reg.z},${reg.dim}`;
const mb = next.mailboxes[k];
if (!mb) continue; // orphaned
next.registry[ownerId] = {
name: reg.name,
mailboxes: [
{
mailboxId: mb.mailboxId,
label: "Mailbox",
x: reg.x,
y: reg.y,
z: reg.z,
dim: reg.dim,
},
],
};
}
state = next;
saveState();
try {
world.setDynamicProperty(PROP_KEY_V1, undefined);
} catch (_) {}
world.sendMessage(`§6[Postal] §7Migrated mail data to v2 (multi-mailbox).`);
return;
}
} catch (e) {
world.sendMessage(`§c[Postal] Migration failed: ${e.message}`);
}
state = { mailboxes: {}, registry: {}, pending: {} };
}
function saveState() {
try {
world.setDynamicProperty(PROP_KEY_V2, JSON.stringify(state));
} catch (e) {
world.sendMessage(`§c[Postal] Failed to save state: ${e.message}`);
}
}
function keyOf(loc, dimensionId) {
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
}
function getMailboxOwner(loc, dimensionId) {
return state.mailboxes[keyOf(loc, dimensionId)] || null;
}
function claimMailbox(loc, dimensionId, player, label) {
const k = keyOf(loc, dimensionId);
const mailboxId = uuid();
state.mailboxes[k] = {
ownerId: player.id,
ownerName: player.name,
mailboxId,
label,
};
if (!state.registry[player.id]) {
state.registry[player.id] = { name: player.name, mailboxes: [] };
}
state.registry[player.id].name = player.name;
state.registry[player.id].mailboxes.push({
mailboxId,
label,
x: Math.floor(loc.x),
y: Math.floor(loc.y),
z: Math.floor(loc.z),
dim: dimensionId,
});
saveState();
return mailboxId;
}
function renameMailbox(loc, dimensionId, label) {
const k = keyOf(loc, dimensionId);
const entry = state.mailboxes[k];
if (!entry) return;
entry.label = label;
const reg = state.registry[entry.ownerId];
if (reg) {
const m = reg.mailboxes.find((m) => m.mailboxId === entry.mailboxId);
if (m) m.label = label;
}
saveState();
}
function releaseMailbox(loc, dimensionId) {
const k = keyOf(loc, dimensionId);
const entry = state.mailboxes[k];
delete state.mailboxes[k];
if (entry && state.registry[entry.ownerId]) {
const reg = state.registry[entry.ownerId];
reg.mailboxes = reg.mailboxes.filter((m) => m.mailboxId !== entry.mailboxId);
if (reg.mailboxes.length === 0) {
delete state.registry[entry.ownerId];
}
}
saveState();
}
function countMailboxes(playerId) {
return state.registry[playerId]?.mailboxes?.length || 0;
}
function getPlayerMailboxes(playerId) {
return state.registry[playerId]?.mailboxes || [];
}
// ─── Chest facing from player rotation ──────────────────────
function chestFacing(yaw) {
let y = yaw;
while (y > 180) y -= 360;
while (y < -180) y += 360;
if (y >= -45 && y < 45) return "north";
if (y >= 45 && y < 135) return "east";
if (y >= -135 && y < -45) return "west";
return "south";
}
// ─── Mailbox placement ──────────────────────────────────────
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== MAILBOX_BLOCK) return;
const player = event.player;
const loc = block.location;
const dim = block.dimension;
const currentCount = countMailboxes(player.id);
if (currentCount >= MAX_MAILBOXES_PER_PLAYER) {
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), {
x: loc.x + 0.5,
y: loc.y + 0.5,
z: loc.z + 0.5,
});
} catch (_) {}
player.sendMessage(
`§c[Postal] §7Max of ${MAX_MAILBOXES_PER_PLAYER} mailboxes per player — break one first.`
);
return;
}
const facing = chestFacing(player.getRotation().y);
try {
dim.runCommand(
`setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
);
} catch (_) {
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
} catch (_) {}
}
const defaultLabel = `Mailbox ${currentCount + 1}`;
claimMailbox(loc, dim.id, player, defaultLabel);
system.run(() => promptForLabel(player, loc, dim.id, defaultLabel));
});
async function promptForLabel(player, loc, dimId, defaultLabel) {
const form = new ModalFormData()
.title("Name this Mailbox")
.textField("Label", "e.g. Home, Mine, Base", defaultLabel);
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled) {
player.sendMessage(`§6[Postal] §7Mailbox locked to you as §d${defaultLabel}§7.`);
return;
}
const raw = (res.formValues && res.formValues[0]) || "";
const label = String(raw).trim().slice(0, LABEL_MAX_LEN) || defaultLabel;
renameMailbox(loc, dimId, label);
player.sendMessage(`§6[Postal] §7Mailbox locked to you as §d${label}§7.`);
}
// ─── Interact: gate mailbox opening for non-owners ──────────
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block) return;
// Post-office block: open menu (cancel default interact)
if (block.typeId === POST_OFFICE_BLOCK) {
event.cancel = true;
const player = event.player;
system.run(() => openPostOfficeMenu(player));
return;
}
// Vanilla chest: gate if it's a claimed mailbox
if (block.typeId !== VANILLA_CHEST) return;
const owner = getMailboxOwner(block.location, block.dimension.id);
if (!owner) return;
if (owner.ownerId === event.player.id) return;
event.cancel = true;
const playerRef = event.player;
system.run(() =>
playerRef.sendMessage(
`§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7.`
)
);
});
// ─── Break: protect mailboxes; owner break returns custom item ──
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getMailboxOwner(block.location, block.dimension.id);
if (!owner) return;
const player = event.player;
if (owner.ownerId !== player.id) {
event.cancel = true;
system.run(() =>
player.sendMessage(
`§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7. You can't break it.`
)
);
return;
}
event.cancel = true;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dim = block.dimension;
const label = owner.label;
system.run(() => {
try {
const inv = dim.getBlock(loc)?.getComponent("inventory");
const container = inv?.container;
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
if (container) {
for (let i = 0; i < container.size; i++) {
const item = container.getItem(i);
if (item) {
dim.spawnItem(item, dropPos);
container.setItem(i, undefined);
}
}
}
dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), dropPos);
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
} catch (e) {
player.sendMessage(`§c[Postal] Error during break: ${e.message}`);
}
releaseMailbox(loc, dim.id);
player.sendMessage(`§6[Postal] §7Removed §d${label}§7.`);
});
});
// ─── Post Office: root menu (router) ────────────────────────
async function openPostOfficeMenu(player) {
const myCount = countMailboxes(player.id);
// No mailboxes of their own → no redirect option, jump straight to send
if (myCount === 0) {
return openSendForm(player);
}
const form = new ActionFormData()
.title("Post Office")
.body("What would you like to do?");
form.button("§fSend Mail\n§7Deliver an item to another player");
form.button("§fRedirect Mailbox\n§7Move one of your mailboxes' contents into another");
form.button("§cClose");
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection === undefined) return;
if (res.selection === 0) return openSendForm(player);
if (res.selection === 1) return openRedirectForm(player);
}
// ─── Held item helpers ──────────────────────────────────────
function getHeldItem(player) {
try {
const inv = player.getComponent("inventory")?.container;
if (!inv) return { item: undefined, slot: -1 };
const slot = player.selectedSlotIndex;
const item = inv.getItem(slot);
return { item, slot, inv };
} catch (_) {
return { item: undefined, slot: -1 };
}
}
function itemSummary(item) {
const niceName = item.typeId.replace(/^minecraft:/, "").replace(/_/g, " ");
return `${item.amount} × ${niceName}`;
}
// ─── Send form: Stage A (recipient) ─────────────────────────
async function openSendForm(player) {
const { item, slot, inv } = getHeldItem(player);
if (!item) {
player.sendMessage(
`§c[Postal] §7Hold the item you want to send in your hotbar first.`
);
return;
}
const candidates = [];
for (const [ownerId, reg] of Object.entries(state.registry)) {
if (ownerId === player.id) continue;
if (!reg.mailboxes || reg.mailboxes.length === 0) continue;
candidates.push({ ownerId, reg });
}
if (candidates.length === 0) {
player.sendMessage(`§c[Postal] §7No other players have claimed a mailbox yet.`);
return;
}
const form = new ActionFormData()
.title("Post Office — Recipient")
.body(`Sending §f${itemSummary(item)}§r\n\nChoose a recipient:`);
for (const c of candidates) {
const n = c.reg.mailboxes.length;
form.button(`§f${c.reg.name}\n§7${n} mailbox${n === 1 ? "" : "es"}`);
}
form.button("§cCancel");
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection === undefined) return;
if (res.selection >= candidates.length) return;
const chosen = candidates[res.selection];
// Single mailbox → skip Stage B
if (chosen.reg.mailboxes.length === 1) {
return confirmAndSend(player, chosen, chosen.reg.mailboxes[0], item, slot, inv);
}
return openMailboxPicker(player, chosen, item, slot, inv);
}
// ─── Send form: Stage B (mailbox) ───────────────────────────
async function openMailboxPicker(player, chosen, item, slot, inv) {
const form = new ActionFormData()
.title(`Send to ${chosen.reg.name}`)
.body(`Sending §f${itemSummary(item)}§r\n\nChoose which mailbox:`);
for (const m of chosen.reg.mailboxes) {
form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
}
form.button("§cBack");
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection === undefined) return;
if (res.selection >= chosen.reg.mailboxes.length) {
return openSendForm(player);
}
const mailbox = chosen.reg.mailboxes[res.selection];
return confirmAndSend(player, chosen, mailbox, item, slot, inv);
}
// ─── Send form: Stage C (confirm) ───────────────────────────
async function confirmAndSend(player, chosen, mailbox, item, slot, inv) {
const confirm = new MessageFormData()
.title("Confirm Send")
.body(
`Send §f${itemSummary(item)}§r\nto §f${chosen.reg.name}§r's §d${mailbox.label}§r?`
)
.button1("Send")
.button2("Cancel");
let res;
try {
res = await confirm.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection !== 0) return;
// Re-fetch held item in case it changed while form was open
const fresh = getHeldItem(player);
if (
!fresh.item ||
fresh.item.typeId !== item.typeId ||
fresh.item.amount !== item.amount
) {
player.sendMessage(`§c[Postal] §7Held item changed — send cancelled.`);
return;
}
deliver(
player,
chosen.ownerId,
chosen.reg.name,
mailbox,
fresh.item,
fresh.slot,
fresh.inv
);
}
// ─── Delivery ───────────────────────────────────────────────
function deliver(
senderPlayer,
recipientId,
recipientName,
mailbox,
itemStack,
slot,
inv,
retry = 0
) {
const dim = world.getDimension(mailbox.dim);
const loc = { x: mailbox.x, y: mailbox.y, z: mailbox.z };
let block;
try {
block = dim.getBlock(loc);
} catch (_) {
block = undefined;
}
if (!block) {
if (retry >= 1) {
senderPlayer.sendMessage(
`§c[Postal] §7Couldn't reach §f${recipientName}§7's §d${mailbox.label}§7 (chunk not loaded). Try again later.`
);
return;
}
const taName = `postal_d_${(mailbox.mailboxId || "").slice(0, 8)}`.replace(
/[^a-zA-Z0-9_]/g,
"_"
);
try {
dim.runCommand(
`tickingarea add ${loc.x} ${loc.y} ${loc.z} ${loc.x} ${loc.y} ${loc.z} ${taName}`
);
} catch (_) {}
system.runTimeout(() => {
deliver(
senderPlayer,
recipientId,
recipientName,
mailbox,
itemStack,
slot,
inv,
retry + 1
);
try {
dim.runCommand(`tickingarea remove ${taName}`);
} catch (_) {}
}, 20);
return;
}
if (block.typeId !== VANILLA_CHEST) {
releaseMailbox(loc, mailbox.dim);
senderPlayer.sendMessage(
`§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is missing. Send cancelled.`
);
return;
}
const owner = getMailboxOwner(loc, mailbox.dim);
if (!owner || owner.ownerId !== recipientId) {
releaseMailbox(loc, mailbox.dim);
senderPlayer.sendMessage(
`§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is no longer claimed. Send cancelled.`
);
return;
}
const container = block.getComponent("inventory")?.container;
if (!container) {
senderPlayer.sendMessage(`§c[Postal] §7Couldn't access the mailbox contents.`);
return;
}
const leftover = container.addItem(itemStack);
try {
inv.setItem(slot, undefined);
} catch (_) {}
if (leftover) {
try {
senderPlayer.dimension.spawnItem(leftover, senderPlayer.location);
} catch (_) {}
senderPlayer.sendMessage(
`§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is full — partial delivery. Leftover dropped at your feet.`
);
}
const summary = itemSummary(itemStack);
senderPlayer.sendMessage(
`§6[Postal] §7Sent §f${summary}§7 to §f${recipientName}§7's §d${mailbox.label}§7.`
);
const onlineRecipient = world.getAllPlayers().find((p) => p.id === recipientId);
if (onlineRecipient) {
onlineRecipient.sendMessage(
`§6[Postal] §7You've got mail from §f${senderPlayer.name}§7 in §d${mailbox.label}§7! (${summary})`
);
} else {
if (!state.pending[recipientId]) state.pending[recipientId] = [];
state.pending[recipientId].push({
from: senderPlayer.name,
itemSummary: summary,
mailboxLabel: mailbox.label,
ts: Date.now(),
});
saveState();
}
}
// ─── Redirect: Stage 1 (source) ─────────────────────────────
async function openRedirectForm(player) {
const myMailboxes = getPlayerMailboxes(player.id);
if (myMailboxes.length < 2) {
player.sendMessage(`§c[Postal] §7You need at least two mailboxes to redirect.`);
return;
}
const form = new ActionFormData()
.title("Redirect — Source")
.body("Choose the mailbox to empty and remove:");
for (const m of myMailboxes) {
form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
}
form.button("§cCancel");
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection === undefined) return;
if (res.selection >= myMailboxes.length) return;
const source = myMailboxes[res.selection];
return redirectStep2(player, source);
}
// ─── Redirect: Stage 2 (destination) ────────────────────────
async function redirectStep2(player, source) {
const dests = getPlayerMailboxes(player.id).filter(
(m) => m.mailboxId !== source.mailboxId
);
if (dests.length === 0) {
player.sendMessage(`§c[Postal] §7No other mailboxes to redirect into.`);
return;
}
const form = new ActionFormData()
.title("Redirect — Destination")
.body(`Source: §d${source.label}§r\n\nChoose where to move its contents:`);
for (const m of dests) {
form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
}
form.button("§cBack");
let res;
try {
res = await form.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection === undefined) return;
if (res.selection >= dests.length) return openRedirectForm(player);
const dest = dests[res.selection];
return redirectConfirm(player, source, dest);
}
// ─── Redirect: Stage 3 (confirm) ────────────────────────────
async function redirectConfirm(player, source, dest) {
const confirm = new MessageFormData()
.title("Confirm Redirect")
.body(
`Redirect §d${source.label}§r → §d${dest.label}§r?\n\n§7The source mailbox will be destroyed and all its contents moved to the destination.`
)
.button1("Redirect")
.button2("Cancel");
let res;
try {
res = await confirm.show(player);
} catch (_) {
return;
}
if (res.canceled || res.selection !== 0) return;
executeRedirect(player, source, dest);
}
// ─── Redirect: execute ──────────────────────────────────────
function executeRedirect(player, source, dest, retry = 0) {
const sDim = world.getDimension(source.dim);
const dDim = world.getDimension(dest.dim);
const sLoc = { x: source.x, y: source.y, z: source.z };
const dLoc = { x: dest.x, y: dest.y, z: dest.z };
let sBlock, dBlock;
try {
sBlock = sDim.getBlock(sLoc);
} catch (_) {}
try {
dBlock = dDim.getBlock(dLoc);
} catch (_) {}
if (!sBlock || !dBlock) {
if (retry >= 1) {
player.sendMessage(
`§c[Postal] §7Couldn't reach mailbox chunks. Try again later.`
);
return;
}
const taS = `postal_rs_${(source.mailboxId || "").slice(0, 6)}`.replace(
/[^a-zA-Z0-9_]/g,
"_"
);
const taD = `postal_rd_${(dest.mailboxId || "").slice(0, 6)}`.replace(
/[^a-zA-Z0-9_]/g,
"_"
);
try {
sDim.runCommand(
`tickingarea add ${sLoc.x} ${sLoc.y} ${sLoc.z} ${sLoc.x} ${sLoc.y} ${sLoc.z} ${taS}`
);
} catch (_) {}
try {
dDim.runCommand(
`tickingarea add ${dLoc.x} ${dLoc.y} ${dLoc.z} ${dLoc.x} ${dLoc.y} ${dLoc.z} ${taD}`
);
} catch (_) {}
system.runTimeout(() => {
executeRedirect(player, source, dest, retry + 1);
try {
sDim.runCommand(`tickingarea remove ${taS}`);
} catch (_) {}
try {
dDim.runCommand(`tickingarea remove ${taD}`);
} catch (_) {}
}, 20);
return;
}
if (sBlock.typeId !== VANILLA_CHEST) {
releaseMailbox(sLoc, source.dim);
player.sendMessage(
`§c[Postal] §7Source mailbox §d${source.label}§7 is missing. Aborted.`
);
return;
}
if (dBlock.typeId !== VANILLA_CHEST) {
releaseMailbox(dLoc, dest.dim);
player.sendMessage(
`§c[Postal] §7Destination mailbox §d${dest.label}§7 is missing. Aborted.`
);
return;
}
const sOwner = getMailboxOwner(sLoc, source.dim);
const dOwner = getMailboxOwner(dLoc, dest.dim);
if (!sOwner || sOwner.ownerId !== player.id) {
player.sendMessage(`§c[Postal] §7You no longer own the source mailbox. Aborted.`);
return;
}
if (!dOwner || dOwner.ownerId !== player.id) {
player.sendMessage(
`§c[Postal] §7You no longer own the destination mailbox. Aborted.`
);
return;
}
const sContainer = sBlock.getComponent("inventory")?.container;
const dContainer = dBlock.getComponent("inventory")?.container;
if (!sContainer || !dContainer) {
player.sendMessage(`§c[Postal] §7Couldn't access mailbox contents.`);
return;
}
const leftovers = [];
for (let i = 0; i < sContainer.size; i++) {
const it = sContainer.getItem(i);
if (!it) continue;
const lo = dContainer.addItem(it);
if (lo) leftovers.push(lo);
sContainer.setItem(i, undefined);
}
// Destroy source block (no item drop — pure consolidation)
try {
sDim.runCommand(`setblock ${sLoc.x} ${sLoc.y} ${sLoc.z} air`);
} catch (_) {}
releaseMailbox(sLoc, source.dim);
if (leftovers.length) {
for (const lo of leftovers) {
try {
player.dimension.spawnItem(lo, player.location);
} catch (_) {}
}
player.sendMessage(
`§e[Postal] §7Destination was full — leftovers dropped at your feet.`
);
}
player.sendMessage(
`§6[Postal] §7Redirected §d${source.label}§7 → §d${dest.label}§7.`
);
}
// ─── Flush pending notifications on login ───────────────────
world.afterEvents.playerSpawn.subscribe((event) => {
if (!event.initialSpawn) return;
const player = event.player;
const queue = state.pending[player.id];
if (!queue || queue.length === 0) return;
system.runTimeout(() => {
player.sendMessage(
`§6[Postal] §7You have §f${queue.length}§7 new mail notification(s):`
);
for (const n of queue) {
const when = new Date(n.ts).toISOString().replace("T", " ").slice(0, 16);
const where = n.mailboxLabel ? ` in §d${n.mailboxLabel}§7` : "";
player.sendMessage(
`§7[${when}] §7from §f${n.from}§7${where}${n.itemSummary}`
);
}
delete state.pending[player.id];
saveState();
}, 40);
});
// ─── Boot ──────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Postal] §7Postal service loaded.");
});