From f126eeb95592e7193c320ef95f086febd5b084ee Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Sat, 25 Apr 2026 02:12:36 +0100 Subject: [PATCH] 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) --- .gitignore | 3 + .../postal_service_BP/blocks/mailbox.json | 3 +- .../postal_service_BP/blocks/post_office.json | 3 +- .../postal_service_BP/manifest.json | 2 +- .../postal_service_BP/scripts/main.js | 566 +++++++++++++++--- .../postal_service_RP/pack_icon.png | Bin 971 -> 2062 bytes .../textures/item_texture.json | 12 + .../textures/items/mailbox.png | Bin 0 -> 245 bytes .../textures/items/post_office.png | Bin 0 -> 328 bytes 9 files changed, 519 insertions(+), 70 deletions(-) create mode 100644 postal-service-addon/postal_service_RP/textures/item_texture.json create mode 100644 postal-service-addon/postal_service_RP/textures/items/mailbox.png create mode 100644 postal-service-addon/postal_service_RP/textures/items/post_office.png diff --git a/.gitignore b/.gitignore index 85a42b8..6c0413d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ server-data/ # Claude Code .claude/ + +# Art workspace (edit-in-place mirror; real textures live in each RP) +/art/ diff --git a/postal-service-addon/postal_service_BP/blocks/mailbox.json b/postal-service-addon/postal_service_BP/blocks/mailbox.json index 4a7a3ca..1ddc93d 100644 --- a/postal-service-addon/postal_service_BP/blocks/mailbox.json +++ b/postal-service-addon/postal_service_BP/blocks/mailbox.json @@ -21,7 +21,8 @@ "texture": "mailbox", "render_method": "opaque" } - } + }, + "minecraft:icon": "mailbox_icon" } } } diff --git a/postal-service-addon/postal_service_BP/blocks/post_office.json b/postal-service-addon/postal_service_BP/blocks/post_office.json index e6129aa..a80fecc 100644 --- a/postal-service-addon/postal_service_BP/blocks/post_office.json +++ b/postal-service-addon/postal_service_BP/blocks/post_office.json @@ -21,7 +21,8 @@ "texture": "post_office", "render_method": "opaque" } - } + }, + "minecraft:icon": "post_office_icon" } } } diff --git a/postal-service-addon/postal_service_BP/manifest.json b/postal-service-addon/postal_service_BP/manifest.json index 5d75923..90623e4 100644 --- a/postal-service-addon/postal_service_BP/manifest.json +++ b/postal-service-addon/postal_service_BP/manifest.json @@ -17,7 +17,7 @@ "type": "script", "language": "javascript", "uuid": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b62", - "version": [1, 0, 0], + "version": [1, 1, 0], "entry": "scripts/main.js" } ], diff --git a/postal-service-addon/postal_service_BP/scripts/main.js b/postal-service-addon/postal_service_BP/scripts/main.js index 98f9a69..2c153f8 100644 --- a/postal-service-addon/postal_service_BP/scripts/main.js +++ b/postal-service-addon/postal_service_BP/scripts/main.js @@ -1,38 +1,101 @@ import { world, system, ItemStack } from "@minecraft/server"; -import { ActionFormData, MessageFormData } from "@minecraft/server-ui"; +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 = "postal_state_v1"; +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 } } -// registry: { [ownerId]: { name, x, y, z, dim } } — reverse lookup for recipient picker -// pending: { [ownerId]: [{ from, itemSummary, ts }] } — queued offline notifications +// 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 raw = world.getDynamicProperty(PROP_KEY); - if (raw && typeof raw === "string") { - const parsed = JSON.parse(raw); + 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 state: ${e.message}`); - state = { mailboxes: {}, registry: {}, pending: {} }; + 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, JSON.stringify(state)); + world.setDynamicProperty(PROP_KEY_V2, JSON.stringify(state)); } catch (e) { world.sendMessage(`§c[Postal] Failed to save state: ${e.message}`); } @@ -46,16 +109,41 @@ function getMailboxOwner(loc, dimensionId) { return state.mailboxes[keyOf(loc, dimensionId)] || null; } -function claimMailbox(loc, dimensionId, player) { +function claimMailbox(loc, dimensionId, player, label) { const k = keyOf(loc, dimensionId); - state.mailboxes[k] = { ownerId: player.id, ownerName: player.name }; - state.registry[player.id] = { - name: player.name, + 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(); } @@ -65,15 +153,20 @@ function releaseMailbox(loc, dimensionId) { 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) { + reg.mailboxes = reg.mailboxes.filter((m) => m.mailboxId !== entry.mailboxId); + if (reg.mailboxes.length === 0) { delete state.registry[entry.ownerId]; } } saveState(); } -function hasClaim(playerId) { - return !!state.registry[playerId]; +function countMailboxes(playerId) { + return state.registry[playerId]?.mailboxes?.length || 0; +} + +function getPlayerMailboxes(playerId) { + return state.registry[playerId]?.mailboxes || []; } // ─── Chest facing from player rotation ────────────────────── @@ -96,8 +189,8 @@ world.afterEvents.playerPlaceBlock.subscribe((event) => { const loc = block.location; const dim = block.dimension; - if (hasClaim(player.id)) { - // Revert placement and refund the item + 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), { @@ -106,7 +199,9 @@ world.afterEvents.playerPlaceBlock.subscribe((event) => { z: loc.z + 0.5, }); } catch (_) {} - player.sendMessage(`§c[Postal] §7You already have a mailbox in this world.`); + player.sendMessage( + `§c[Postal] §7Max of ${MAX_MAILBOXES_PER_PLAYER} mailboxes per player — break one first.` + ); return; } @@ -121,20 +216,44 @@ world.afterEvents.playerPlaceBlock.subscribe((event) => { } catch (_) {} } - claimMailbox(loc, dim.id, player); - player.sendMessage(`§6[Postal] §7Mailbox locked to you. Only you can open or break it.`); + 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 send UI (cancel default interact) + // Post-office block: open menu (cancel default interact) if (block.typeId === POST_OFFICE_BLOCK) { event.cancel = true; const player = event.player; - system.run(() => openSendForm(player)); + system.run(() => openPostOfficeMenu(player)); return; } @@ -146,7 +265,9 @@ world.beforeEvents.playerInteractWithBlock.subscribe((event) => { event.cancel = true; const playerRef = event.player; system.run(() => - playerRef.sendMessage(`§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7.`) + playerRef.sendMessage( + `§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7.` + ) ); }); @@ -162,7 +283,9 @@ world.beforeEvents.playerBreakBlock.subscribe((event) => { 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.`) + player.sendMessage( + `§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7. You can't break it.` + ) ); return; } @@ -170,6 +293,7 @@ world.beforeEvents.playerBreakBlock.subscribe((event) => { 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 { @@ -191,10 +315,38 @@ world.beforeEvents.playerBreakBlock.subscribe((event) => { player.sendMessage(`§c[Postal] Error during break: ${e.message}`); } releaseMailbox(loc, dim.id); + player.sendMessage(`§6[Postal] §7Removed §d${label}§7.`); }); }); -// ─── Post Office: send form ───────────────────────────────── +// ─── 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; @@ -212,16 +364,20 @@ function itemSummary(item) { 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.`); + 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 }); } @@ -231,50 +387,111 @@ async function openSendForm(player) { } const form = new ActionFormData() - .title("Post Office") + .title("Post Office — Recipient") .body(`Sending §f${itemSummary(item)}§r\n\nChoose a recipient:`); - for (const c of candidates) form.button(c.reg.name); + 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 response; + let res; try { - response = await form.show(player); + res = await form.show(player); } catch (_) { return; } - if (response.canceled || response.selection === undefined) return; - if (response.selection >= candidates.length) return; // cancel button + if (res.canceled || res.selection === undefined) return; + if (res.selection >= candidates.length) return; - const chosen = candidates[response.selection]; + 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?`) + .body( + `Send §f${itemSummary(item)}§r\nto §f${chosen.reg.name}§r's §d${mailbox.label}§r?` + ) .button1("Send") .button2("Cancel"); - let conf; + let res; try { - conf = await confirm.show(player); + res = await confirm.show(player); } catch (_) { return; } - if (conf.canceled || conf.selection !== 0) 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) { + 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); + deliver( + player, + chosen.ownerId, + chosen.reg.name, + mailbox, + 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 }; +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); @@ -285,19 +502,30 @@ function deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry = 0 if (!block) { if (retry >= 1) { senderPlayer.sendMessage( - `§c[Postal] §7Couldn't reach §f${reg.name}§7's mailbox (chunk not loaded). Try again later.` + `§c[Postal] §7Couldn't reach §f${recipientName}§7's §d${mailbox.label}§7 (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); + const taName = `postal_d_${(mailbox.mailboxId || "").slice(0, 8)}`.replace( + /[^a-zA-Z0-9_]/g, + "_" + ); try { dim.runCommand( - `tickingarea add ${reg.x} ${reg.y} ${reg.z} ${reg.x} ${reg.y} ${reg.z} ${taName}` + `tickingarea add ${loc.x} ${loc.y} ${loc.z} ${loc.x} ${loc.y} ${loc.z} ${taName}` ); } catch (_) {} system.runTimeout(() => { - deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry + 1); + deliver( + senderPlayer, + recipientId, + recipientName, + mailbox, + itemStack, + slot, + inv, + retry + 1 + ); try { dim.runCommand(`tickingarea remove ${taName}`); } catch (_) {} @@ -306,19 +534,18 @@ function deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry = 0 } if (block.typeId !== VANILLA_CHEST) { - // Mailbox was destroyed externally — clean up registry - releaseMailbox(loc, reg.dim); + releaseMailbox(loc, mailbox.dim); senderPlayer.sendMessage( - `§c[Postal] §f${reg.name}§7's mailbox is missing. Send cancelled.` + `§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is missing. Send cancelled.` ); return; } - const owner = getMailboxOwner(loc, reg.dim); + const owner = getMailboxOwner(loc, mailbox.dim); if (!owner || owner.ownerId !== recipientId) { - releaseMailbox(loc, reg.dim); + releaseMailbox(loc, mailbox.dim); senderPlayer.sendMessage( - `§c[Postal] §f${reg.name}§7's mailbox is no longer claimed. Send cancelled.` + `§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is no longer claimed. Send cancelled.` ); return; } @@ -331,40 +558,240 @@ function deliver(senderPlayer, recipientId, reg, itemStack, slot, inv, retry = 0 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); + senderPlayer.dimension.spawnItem(leftover, senderPlayer.location); } catch (_) {} senderPlayer.sendMessage( - `§c[Postal] §f${reg.name}§7's mailbox is full — partial delivery. Leftover dropped at your feet.` + `§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${reg.name}§7.`); + senderPlayer.sendMessage( + `§6[Postal] §7Sent §f${summary}§7 to §f${recipientName}§7's §d${mailbox.label}§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})`); + 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; @@ -373,10 +800,15 @@ world.afterEvents.playerSpawn.subscribe((event) => { if (!queue || queue.length === 0) return; system.runTimeout(() => { - player.sendMessage(`§6[Postal] §7You have §f${queue.length}§7 new mail notification(s):`); + 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}`); + 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(); diff --git a/postal-service-addon/postal_service_RP/pack_icon.png b/postal-service-addon/postal_service_RP/pack_icon.png index a507642d37648f9bf0fb7e5ed2a3229eb2392bb9..f940b274234de2b4b69bda60ebc53c3f070120af 100644 GIT binary patch delta 2049 zcmV+c2>$oW2aXVsBYy|gNklO>7fK6vy8hi$fJVj+3}i6GA8j(&Bv3v=RqI zFC0*Xs#K~Fr>Y0;T&kWp_E1$^dgoB(getY_0aYA0<`7j)fF>o~i!@Bik_n-5Q%p}t}j7_y|nF@IeI#w01z1z^mQ1`YwF zNwfU4;Rrw+P6COOcGv?*!@U6Fa1KBm?gbDh4Gy~iWjF^Q4)+3x!x4ZuoC6StdjZ7Z zCV)6R5JVh~0K`c!I)#Nl25anhUv(#F~d z@O0-_(?w{Su7Av^=^`{u+H?WDBvEuP-ts5K8I1P=Dm!u(Ewy`hHS{mm!`d$`SI}aevJvs!^IN!$+w>-+NjogEl1M z&!0V`u>^XVAFdgDfd=T-hUPsv2z75^k6LtT# zG*?z!8o%CJlE95EbK(bUpA22)nLm3b-~_jzyR*I%C?%k`Q#^DUb(^^LtvTPh4r4bW zdK4zg*MHObuKPM@uj!7w(tA(t8}glN?FC>PS0?=8pV8ej!SI`}5>Rms%(4a}fk{wv zfc33ev}*_P8nhA7y?8$_oMGg~O>IrX`qmuubWWh(XL8@6(0k6vp$|}+fVF`5SjKEc z_&B^#K}XpE0D3zoLUvQx1bi%UfD||mpd1>&5`RB>UoRuy3ovg4GuZgt#ukMNV;mkHtnL=pF;5&!^9jg&`#-rRe2XSE{54=_73>SYB3^Xgg7u%yjRIQicb$^0- z;U54{^K^u9fU0kqv!E661+z0#Fgr8Va$=<#QR^EKUm&xG%x*Q*Ya|XWy@1K(CHVZc zoU-fL5)GQE^)ec%{n%LD4UEHdZcwkSPYUTOQUlYSJrVjoAO^alL94Ysitg+Qh|&T} zBLL(GpwkHe@S_1%t#9N@qF89_27g|8wy87^kbxBqFlzmkXPch0dKzGM1JN#BBcC02 zH-*sU1OV!y0YwV&9#;jOAfjSu!q$@M(ZlC6F1Akx5vtMmt zx-|+B4K{b5*X|+t!liJET*rR&?p;@A6m^298MKcBc%8tD7!(on?yA|`eJ&MVAbxjo zw}Ntifo&T|q6`JsJGwF>(3Kg{^Mn9!xOgE}d@ol!@^H9#0i_(ZXV!1;*r8yPfhd<5 zfz6$HBljL2>~UW&me;l1(|?Yj?*s_qJUvr%MPplVFZ>F zcnOMK@BMa1YcHS??A*Lw4Yt`Dfd6F-d8}I&jvWFP%j?o`uO$(*&DKC50+bf6h5GhE zv4iIqcPmg@xQ1Rf8!*}pl%XpJ(AD+xVwRc^U2^_S@) zpz-g2;k|lIV_h%cA7mqaGQQ)!o?cvWUvIr!EAV&vIO_dv!@hwa(-5;q+YbaW@mMz* zWEyJr;0Qn*?fV1R4}UFtpzjZmG^p8wBR~jR_TU_VIP3t#v2QZSFx2e95r8AFxaT$H99naL2P?mm61*9L&T+8s9e*tjf!Yfwv*3FsSXl;< z90FxYSoz6a7G5!;G7Y}|wo*q>ngg)#_y`-Ta)eYB)cmmcdMQ7-dr5_EJQqM5a?8TM zy1vvtkxGGf$|Asl!ZGUxIocUj_I3se$CQVFhpT_IPc5^I0mT)~87Le@YXcsxwo$k> z%s?T24r8yNI(k45c8JEo5McZQuaR-XS*-#Q5Q%VB%Q2B#hAmM*!mRvT)+?{s6?` f2tXXp0f_TIVY8^o|7=cT00000NkvXXu0mjfuUyFg literal 971 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV4mdZ;uumf=j~m?JmElr_K%#W z1cNTSgzR&$QsJ9mCBC5F;boALN{hqGm&&hX6oh2OABbfJw=>P#9Cx$g?7mv#&;RbP z_fmNrG4K55b7{hTa{2!Ac{asW*fac4VkkU**#F$Ru(*C^-oJnTWdAjkc93E0P-ZA( zQRrhhBG_QTB;d~Qhy%Oeosa)!e@U${^;uY>IwgkTwuZYSgY^m*M+Wv6hFnZDG^<5e zHZ=GI3OFbSE>>W0f2?8C^uQ%RAntYp?}IbH4GypsJh81YHf!kSXx6{M(CaF-Pf~ip z%nT5R*F|=pB%_Yrrw?Z>Saj5WJ>WJwz_f|yeh+Uz=(2<2ISpP$eD`&3uvyseyi+}U zcl+$4^2Zf7@$KUfJTT>8f4;P0%bUaI+yxvKDL+jA#p>Gf+&@#vd`9-s&y%U2eJ`AT zz`dO};I-(TBH=G)HJcUQGcI~v_@VVQ+ls%dVvD%H*wk!h$Y*M|tEqqdSKt~jeD3n! z*9l-q{UN9nAW45J^l@#@t)iuahWT*&%y zd*8b6nJ+D!avQ|5cU*wpn{?uE083X^;Ucd%Y~EvB)_<-sn-jsx?5 zD=zrXv8b5;z#D&-y=?-u+%3P%KEw;U)GKY~+Q|6rq}`tr{r0X55qy|2>NJ=0&eL}f z)xMmZvN3%1bm{r^KR=#(Iq}VOrG_{6_W#@U&mxK^lj+1Z*0N49=UEI@COrqwsCR@g ze%ZuY_VEVaN+yHX_DzdIdEEQAFZX{s*=N7FxMkDFQr^d*_Sf$ywEYkEyv5Mvq3E-7 z)ygoQVkHT-3GW09#GXzt_&RCZ!v|UUZT~0QF>HRWeqXu8^14x9{82f_52c)I$ztaD0e F0stcJlKlVx diff --git a/postal-service-addon/postal_service_RP/textures/item_texture.json b/postal-service-addon/postal_service_RP/textures/item_texture.json new file mode 100644 index 0000000..f10461c --- /dev/null +++ b/postal-service-addon/postal_service_RP/textures/item_texture.json @@ -0,0 +1,12 @@ +{ + "resource_pack_name": "postal_service_RP", + "texture_name": "atlas.items", + "texture_data": { + "mailbox_icon": { + "textures": "textures/items/mailbox" + }, + "post_office_icon": { + "textures": "textures/items/post_office" + } + } +} diff --git a/postal-service-addon/postal_service_RP/textures/items/mailbox.png b/postal-service-addon/postal_service_RP/textures/items/mailbox.png new file mode 100644 index 0000000000000000000000000000000000000000..506254b90657fd2b95bb0777b75d84572cb5f719 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`dpunnLn`JZCrD%*=s5Fo;>4f+ zJ-X6wwC|0n-YFloofx6+3g*x1;1Jlkz9_t50Q ziPhmesxeFjcEY<}9$}iT|6Zcz-k!vigG(Kf?~Czll=iceNb-?TvM@0=FfjO_Ki78- z;|a4&jg^i^^6n)3keJ)GP)tsCLxciL-=3maCn8 zZNiNYFPQ_`3^pjv&`6x{e6jnKf1c5LiRKUP%nj$!G-a(YJ)b$VA@-14>-+lF{#*6D zmJ9z&6*>LiZ+E+C>behszw;L#V(*V~OJUA2QR(<}Xri>fMBkmCEm9pkK;Wj8q`~!B zEH(iIW{9LWBr@a)AVc;a@?y z<;kZKB@P%U^R?bK31nO@Q^q{uz*G165=_kc4}+UJSUDBt_Uz=7-BagTe~DWM4fc=m%- literal 0 HcmV?d00001