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 a507642..f940b27 100644 Binary files a/postal-service-addon/postal_service_RP/pack_icon.png and b/postal-service-addon/postal_service_RP/pack_icon.png differ 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 0000000..506254b Binary files /dev/null and b/postal-service-addon/postal_service_RP/textures/items/mailbox.png differ diff --git a/postal-service-addon/postal_service_RP/textures/items/post_office.png b/postal-service-addon/postal_service_RP/textures/items/post_office.png new file mode 100644 index 0000000..1308ab9 Binary files /dev/null and b/postal-service-addon/postal_service_RP/textures/items/post_office.png differ