Files
SysAdmin f7aa71e9eb
All checks were successful
Deploy Addons / deploy (push) Successful in 16s
feat(postal): add postal service addon and bundle pending addon work
- New postal-service-addon: per-player mailboxes + post-office send block
  (ActionForm recipient picker, offline notification queue, chunk-load
  retry via tickingarea)
- Commit previously untracked private-chest, home-sign, keep-inventory
  addons and their docker-compose mounts
- Deploy workflow: add postal + previously unwired addons to path filter
  and checkout list; drop easter-egg from deployment
- enabled_packs.json: register postal UUIDs for Lyla + Mya

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:07:39 +01:00

159 lines
5.5 KiB
JavaScript

import { world, system, ItemStack } from "@minecraft/server";
// ─── Constants ──────────────────────────────────────────────
const PRIVATE_BLOCK = "silverlabs:private_chest";
const VANILLA_CHEST = "minecraft:chest";
const PROP_KEY = "private_chests_v1";
// ─── State ──────────────────────────────────────────────────
// In-memory mirror of dynamic property: { "x,y,z,dim": { ownerId, ownerName } }
let chests = {};
function loadState() {
try {
const raw = world.getDynamicProperty(PROP_KEY);
if (raw && typeof raw === "string") {
chests = JSON.parse(raw);
}
} catch (e) {
world.sendMessage(`§c[Private Chest] Failed to load state: ${e.message}`);
chests = {};
}
}
function saveState() {
try {
world.setDynamicProperty(PROP_KEY, JSON.stringify(chests));
} catch (e) {
world.sendMessage(`§c[Private Chest] 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 claim(loc, dimensionId, player) {
chests[keyOf(loc, dimensionId)] = { ownerId: player.id, ownerName: player.name };
saveState();
}
function release(loc, dimensionId) {
delete chests[keyOf(loc, dimensionId)];
saveState();
}
function getOwner(loc, dimensionId) {
return chests[keyOf(loc, dimensionId)] || null;
}
// ─── Chest facing from player rotation ──────────────────────
// Bedrock yaw: 0=south, 90=west, 180=north, -90=east. Chest front faces the player.
function chestFacing(yaw) {
let y = yaw;
while (y > 180) y -= 360;
while (y < -180) y += 360;
if (y >= -45 && y < 45) return "north"; // player facing south
if (y >= 45 && y < 135) return "east"; // player facing west
if (y >= -135 && y < -45) return "west"; // player facing east
return "south"; // player facing north
}
// ─── Placement: swap custom block → vanilla chest + claim ──
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== PRIVATE_BLOCK) return;
const player = event.player;
const loc = block.location;
const dim = block.dimension;
const facing = chestFacing(player.getRotation().y);
try {
dim.runCommand(
`setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
);
} catch (e) {
// Older block-state syntax fallback (rare)
try {
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
} catch (_) {}
}
claim(loc, dim.id, player);
player.sendMessage(`§6[Private Chest] §7Locked to you. Only you can open or break it.`);
});
// ─── Interact: block opening for non-owners ─────────────────
try {
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getOwner(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[Private Chest] §7This chest belongs to §f${owner.ownerName}§7.`)
);
});
} catch (e) {
console.warn(`[Private Chest] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
}
// ─── Break: protect for non-owners; owner break drops the custom item back ──
try {
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (block.typeId !== VANILLA_CHEST) return;
const owner = getOwner(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[Private Chest] §7This chest belongs to §f${owner.ownerName}§7. You can't break it.`)
);
return;
}
// Owner breaking — cancel default drop, manually eject contents + return the custom item.
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(PRIVATE_BLOCK, 1), dropPos);
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
} catch (e) {
player.sendMessage(`§c[Private Chest] Error during break: ${e.message}`);
}
release(loc, dim.id);
});
});
} catch (e) {
console.warn(`[Private Chest] beforeEvents.playerBreakBlock unavailable: ${e}`);
}
// ─── Boot ──────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Private Chest] §7Owner-locked chest system loaded.");
});