@@ -0,0 +1,390 @@
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 \n Choose 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 \n to §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." ) ;
} ) ;