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."); });