import { world, system, ItemStack } from "@minecraft/server"; import { ActionFormData, MessageFormData } 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 = "postal_state_v1"; // ─── State ────────────────────────────────────────────────── // mailboxes: { "x,y,z,dim": { ownerId, ownerName } } // registry: { [ownerId]: { name, x, y, z, dim } } — reverse lookup for recipient picker // pending: { [ownerId]: [{ from, itemSummary, ts }] } — queued offline notifications let state = { mailboxes: {}, registry: {}, pending: {} }; function loadState() { try { const raw = world.getDynamicProperty(PROP_KEY); if (raw && typeof raw === "string") { const parsed = JSON.parse(raw); state = { mailboxes: parsed.mailboxes || {}, registry: parsed.registry || {}, pending: parsed.pending || {}, }; } } catch (e) { world.sendMessage(`§c[Postal] Failed to load state: ${e.message}`); state = { mailboxes: {}, registry: {}, pending: {} }; } } function saveState() { try { world.setDynamicProperty(PROP_KEY, 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) { const k = keyOf(loc, dimensionId); state.mailboxes[k] = { ownerId: player.id, ownerName: player.name }; state.registry[player.id] = { name: player.name, x: Math.floor(loc.x), y: Math.floor(loc.y), z: Math.floor(loc.z), dim: dimensionId, }; 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]; if (reg.x === Math.floor(loc.x) && reg.y === Math.floor(loc.y) && reg.z === Math.floor(loc.z) && reg.dim === dimensionId) { delete state.registry[entry.ownerId]; } } saveState(); } function hasClaim(playerId) { return !!state.registry[playerId]; } // ─── 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; if (hasClaim(player.id)) { // Revert placement and refund the item 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] §7You already have a mailbox in this world.`); 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 (_) {} } claimMailbox(loc, dim.id, player); player.sendMessage(`§6[Postal] §7Mailbox locked to you. Only you can open or break it.`); }); // ─── Interact: gate mailbox opening for non-owners ────────── world.beforeEvents.playerInteractWithBlock.subscribe((event) => { const block = event.block; if (!block) return; // Post-office block: open send UI (cancel default interact) if (block.typeId === POST_OFFICE_BLOCK) { event.cancel = true; const player = event.player; system.run(() => openSendForm(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; 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); }); }); // ─── Post Office: send form ───────────────────────────────── 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}`; } 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; 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") .body(`Sending §f${itemSummary(item)}§r\n\nChoose a recipient:`); for (const c of candidates) form.button(c.reg.name); form.button("§cCancel"); let response; try { response = await form.show(player); } catch (_) { return; } if (response.canceled || response.selection === undefined) return; if (response.selection >= candidates.length) return; // cancel button const chosen = candidates[response.selection]; const confirm = new MessageFormData() .title("Confirm Send") .body(`Send §f${itemSummary(item)}§r\nto §f${chosen.reg.name}§r?`) .button1("Send") .button2("Cancel"); let conf; try { conf = await confirm.show(player); } catch (_) { return; } if (conf.canceled || conf.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, fresh.item, fresh.slot, fresh.inv); } // ─── Delivery ─────────────────────────────────────────────── function deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry = 0) { const dim = world.getDimension(reg.dim); const loc = { x: reg.x, y: reg.y, z: reg.z }; let block; try { block = dim.getBlock(loc); } catch (_) { block = undefined; } if (!block) { if (retry >= 1) { senderPlayer.sendMessage( `§c[Postal] §7Couldn't reach §f${reg.name}§7's mailbox (chunk not loaded). Try again later.` ); return; } // Force chunk load briefly, then retry const taName = `postal_tmp_${recipientId}`.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 20); try { dim.runCommand( `tickingarea add ${reg.x} ${reg.y} ${reg.z} ${reg.x} ${reg.y} ${reg.z} ${taName}` ); } catch (_) {} system.runTimeout(() => { deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry + 1); try { dim.runCommand(`tickingarea remove ${taName}`); } catch (_) {} }, 20); return; } if (block.typeId !== VANILLA_CHEST) { // Mailbox was destroyed externally — clean up registry releaseMailbox(loc, reg.dim); senderPlayer.sendMessage( `§c[Postal] §f${reg.name}§7's mailbox is missing. Send cancelled.` ); return; } const owner = getMailboxOwner(loc, reg.dim); if (!owner || owner.ownerId !== recipientId) { releaseMailbox(loc, reg.dim); senderPlayer.sendMessage( `§c[Postal] §f${reg.name}§7's mailbox 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); // Remove the full stack from sender's inventory first try { inv.setItem(slot, undefined); } catch (_) {} // Refund leftover (mailbox full) back to sender if (leftover) { const p = senderPlayer.location; try { senderPlayer.dimension.spawnItem(leftover, p); } catch (_) {} senderPlayer.sendMessage( `§c[Postal] §f${reg.name}§7's mailbox is full — partial delivery. Leftover dropped at your feet.` ); } const summary = itemSummary(itemStack); senderPlayer.sendMessage(`§6[Postal] §7Sent §f${summary}§7 to §f${reg.name}§7.`); // Notify recipient const onlineRecipient = world.getAllPlayers().find((p) => p.id === recipientId); if (onlineRecipient) { onlineRecipient.sendMessage(`§6[Postal] §7You've got mail from §f${senderPlayer.name}§7! (${summary})`); } else { if (!state.pending[recipientId]) state.pending[recipientId] = []; state.pending[recipientId].push({ from: senderPlayer.name, itemSummary: summary, ts: Date.now(), }); saveState(); } } // ─── 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); player.sendMessage(`§7[${when}] §7from §f${n.from}§7 — ${n.itemSummary}`); } delete state.pending[player.id]; saveState(); }, 40); }); // ─── Boot ────────────────────────────────────────────────── system.run(() => { loadState(); world.sendMessage("§6[Postal] §7Postal service loaded."); });