feat(redstone-link): wireless redstone addon; track family-goals
Adds redstone-link-addon: a transmitter/receiver pair bound by channel name, giving redstone that runs anywhere without wire, support blocks or line of sight. Bedrock custom blocks can read redstone power but cannot emit it, so a live receiver is swapped to a vanilla redstone_block and back; receiver positions persist in a world dynamic property, with a reconcile pass at boot to repair anything a crash left inconsistent. Deployed and proven end-to-end on lobby (Hub World). Also commits family-goals-addon, which was running on jamie -- mounted in docker-compose.yml and pinned in the world -- while existing in no commit on any branch. It was unbacked-up production code. That omission was load-bearing for deploys: deploy.yml checks out docker-compose.yml from origin/main, so a deploy would have replaced the host compose with a version lacking the family-goals mount and silently killed the addon. Both new directories are added to the workflow's PATHS and push-trigger list, so nothing is mounted that CI does not deliver. The mount set in this file now matches the live host exactly. Note for a follow-up: dynamite-, hemp-, naturalist-lite-, smart-crafting-, tow-boat- and trees-features-addon are mounted but absent from PATHS, so CI never refreshes them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC
This commit is contained in:
co-authored by
Claude Opus 5
parent
b28e4d0610
commit
8ded29cbcc
@@ -0,0 +1,118 @@
|
||||
// Persistent node registry for Redstone Link.
|
||||
//
|
||||
// A "node" is one placed transmitter or receiver. We have to remember them
|
||||
// because a powered receiver is physically replaced by a vanilla redstone
|
||||
// block (Bedrock custom blocks cannot emit redstone power), so the block
|
||||
// itself stops being evidence that a receiver is there.
|
||||
//
|
||||
// State lives in a world dynamic property, the same approach family-goals and
|
||||
// private-chest use, so it survives container restarts.
|
||||
|
||||
import { world } from "@minecraft/server";
|
||||
|
||||
const DP_KEY = "rlink:nodes";
|
||||
|
||||
// Bounded so the serialised registry stays well inside the dynamic-property
|
||||
// size budget and the tick loop stays cheap.
|
||||
export const MAX_NODES = 256;
|
||||
|
||||
// node shape: { d: dimensionId, x, y, z, r: "tx" | "rx", c: channel }
|
||||
let cache = null;
|
||||
|
||||
export function load() {
|
||||
if (cache) return cache;
|
||||
let raw;
|
||||
try {
|
||||
raw = world.getDynamicProperty(DP_KEY);
|
||||
} catch (_) {
|
||||
raw = undefined;
|
||||
}
|
||||
if (typeof raw !== "string" || raw.length === 0) {
|
||||
cache = [];
|
||||
return cache;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
cache = Array.isArray(parsed) ? parsed : [];
|
||||
} catch (_) {
|
||||
console.warn("[RedstoneLink] registry was corrupt, starting empty");
|
||||
cache = [];
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
export function save() {
|
||||
if (!cache) return;
|
||||
try {
|
||||
world.setDynamicProperty(DP_KEY, JSON.stringify(cache));
|
||||
} catch (e) {
|
||||
console.warn(`[RedstoneLink] registry save failed: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function keyOf(dimensionId, loc) {
|
||||
return `${dimensionId}|${Math.floor(loc.x)}|${Math.floor(loc.y)}|${Math.floor(loc.z)}`;
|
||||
}
|
||||
|
||||
export function nodeKey(n) {
|
||||
return `${n.d}|${n.x}|${n.y}|${n.z}`;
|
||||
}
|
||||
|
||||
export function find(dimensionId, loc) {
|
||||
const k = keyOf(dimensionId, loc);
|
||||
return load().find((n) => nodeKey(n) === k) ?? null;
|
||||
}
|
||||
|
||||
/** Returns the node, or null if the registry is full. */
|
||||
export function add(dimensionId, loc, role, channel) {
|
||||
const list = load();
|
||||
const k = keyOf(dimensionId, loc);
|
||||
const existing = list.find((n) => nodeKey(n) === k);
|
||||
if (existing) {
|
||||
existing.r = role;
|
||||
existing.c = channel;
|
||||
save();
|
||||
return existing;
|
||||
}
|
||||
if (list.length >= MAX_NODES) return null;
|
||||
const node = {
|
||||
d: dimensionId,
|
||||
x: Math.floor(loc.x),
|
||||
y: Math.floor(loc.y),
|
||||
z: Math.floor(loc.z),
|
||||
r: role,
|
||||
c: channel,
|
||||
};
|
||||
list.push(node);
|
||||
save();
|
||||
return node;
|
||||
}
|
||||
|
||||
export function remove(dimensionId, loc) {
|
||||
const list = load();
|
||||
const k = keyOf(dimensionId, loc);
|
||||
const i = list.findIndex((n) => nodeKey(n) === k);
|
||||
if (i < 0) return false;
|
||||
list.splice(i, 1);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Bulk removal by node key; one save for the whole batch. */
|
||||
export function removeKeys(keys) {
|
||||
if (!keys || keys.size === 0) return 0;
|
||||
const list = load();
|
||||
let removed = 0;
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
if (keys.has(nodeKey(list[i]))) {
|
||||
list.splice(i, 1);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) save();
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return load();
|
||||
}
|
||||
Reference in New Issue
Block a user