From 8ded29cbccf553582a635e1926ea50c4de3bbe3a Mon Sep 17 00:00:00 2001 From: sysadmin Date: Fri, 28 Aug 2026 11:06:46 +0100 Subject: [PATCH 1/4] 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) Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC --- .gitea/workflows/deploy.yml | 9 +- docker-compose.yml | 3 + .../family_goals_BP/manifest.json | 33 ++ .../family_goals_BP/scripts/main.js | 479 ++++++++++++++++++ redstone-link-addon/README.md | 76 +++ .../blocks/redstone_link_rx.json | 27 + .../blocks/redstone_link_tx.json | 27 + .../redstone_link_BP/manifest.json | 38 ++ .../redstone_link_BP/pack_icon.png | Bin 0 -> 1758 bytes .../recipes/redstone_link_rx.json | 19 + .../recipes/redstone_link_tx.json | 19 + .../redstone_link_BP/scripts/main.js | 354 +++++++++++++ .../redstone_link_BP/scripts/power.js | 84 +++ .../redstone_link_BP/scripts/registry.js | 118 +++++ .../redstone_link_RP/blocks.json | 5 + .../redstone_link_RP/manifest.json | 17 + .../redstone_link_RP/pack_icon.png | Bin 0 -> 1758 bytes .../redstone_link_RP/texts/en_US.lang | 2 + .../textures/blocks/redstone_link_rx.png | Bin 0 -> 586 bytes .../textures/blocks/redstone_link_tx.png | Bin 0 -> 595 bytes .../textures/terrain_texture.json | 14 + 21 files changed, 1323 insertions(+), 1 deletion(-) create mode 100644 family-goals-addon/family_goals_BP/manifest.json create mode 100644 family-goals-addon/family_goals_BP/scripts/main.js create mode 100644 redstone-link-addon/README.md create mode 100644 redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json create mode 100644 redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json create mode 100644 redstone-link-addon/redstone_link_BP/manifest.json create mode 100644 redstone-link-addon/redstone_link_BP/pack_icon.png create mode 100644 redstone-link-addon/redstone_link_BP/recipes/redstone_link_rx.json create mode 100644 redstone-link-addon/redstone_link_BP/recipes/redstone_link_tx.json create mode 100644 redstone-link-addon/redstone_link_BP/scripts/main.js create mode 100644 redstone-link-addon/redstone_link_BP/scripts/power.js create mode 100644 redstone-link-addon/redstone_link_BP/scripts/registry.js create mode 100644 redstone-link-addon/redstone_link_RP/blocks.json create mode 100644 redstone-link-addon/redstone_link_RP/manifest.json create mode 100644 redstone-link-addon/redstone_link_RP/pack_icon.png create mode 100644 redstone-link-addon/redstone_link_RP/texts/en_US.lang create mode 100644 redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png create mode 100644 redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_tx.png create mode 100644 redstone-link-addon/redstone_link_RP/textures/terrain_texture.json diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 88808fb..3a4eec2 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -14,6 +14,8 @@ on: - 'keep-inventory-addon/**' - 'postal-service-addon/**' - 'camping-supplies-addon/**' + - 'family-goals-addon/**' + - 'redstone-link-addon/**' - 'docker-compose.yml' - 'scripts/**' @@ -31,7 +33,12 @@ jobs: script: | set -e APP_DIR="$HOME/minecraft-multiworld" - PATHS="addon/ lobby-addon/ hub-return-addon/ village-evolution-addon/ monkey-addon/ private-chest-addon/ home-sign-addon/ keep-inventory-addon/ postal-service-addon/ camping-supplies-addon/ docker-compose.yml" + # Anything mounted in docker-compose.yml but missing here is simply + # never refreshed by CI. family-goals-addon and redstone-link-addon + # are listed because docker-compose.yml mounts them; omitting either + # while checking out docker-compose.yml would leave a mount pointing + # at a directory this job never delivers. + PATHS="addon/ lobby-addon/ hub-return-addon/ village-evolution-addon/ monkey-addon/ private-chest-addon/ home-sign-addon/ keep-inventory-addon/ postal-service-addon/ camping-supplies-addon/ family-goals-addon/ redstone-link-addon/ docker-compose.yml" # First run: clone. Subsequent: pull. if [ ! -d "$APP_DIR/.git" ]; then diff --git a/docker-compose.yml b/docker-compose.yml index 04bda63..c72b019 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,8 @@ services: - ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP - ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP - ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP + - ./redstone-link-addon/redstone_link_BP:/data/behavior_packs/redstone_link_BP + - ./redstone-link-addon/redstone_link_RP:/data/resource_packs/redstone_link_RP restart: unless-stopped # Cap each Bedrock service so a runaway/hung server can't OOM-kill its # neighbours. Host has 8 GB; 4 × 1500 MB leaves headroom for the OS and @@ -81,6 +83,7 @@ services: - ./trees-features-addon/trees_features_RP:/data/resource_packs/trees_features_RP - ./hemp-addon/hemp_BP:/data/behavior_packs/hemp_BP - ./hemp-addon/hemp_RP:/data/resource_packs/hemp_RP + - ./family-goals-addon/family_goals_BP:/data/behavior_packs/family_goals_BP restart: unless-stopped mem_limit: 1500m memswap_limit: 2500m diff --git a/family-goals-addon/family_goals_BP/manifest.json b/family-goals-addon/family_goals_BP/manifest.json new file mode 100644 index 0000000..a4097dd --- /dev/null +++ b/family-goals-addon/family_goals_BP/manifest.json @@ -0,0 +1,33 @@ +{ + "format_version": 2, + "header": { + "name": "Family Goals", + "description": "Shared goals, family bank, loot share, pity timer, and PvP-off for the family survival world", + "uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345001", + "version": [1, 0, 6], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "script", + "language": "javascript", + "uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345002", + "version": [1, 0, 6], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "module_name": "@minecraft/server", + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-ui", + "version": "1.3.0" + }, + { + "module_name": "@minecraft/server-admin", + "version": "1.0.0-beta" + } + ] +} diff --git a/family-goals-addon/family_goals_BP/scripts/main.js b/family-goals-addon/family_goals_BP/scripts/main.js new file mode 100644 index 0000000..8d03576 --- /dev/null +++ b/family-goals-addon/family_goals_BP/scripts/main.js @@ -0,0 +1,479 @@ +// Family Goals Addon +// +// Lives in jamie's world (the shared family survival world). Provides: +// • PvP-off (siblings can't hurt each other) +// • Family roster ( !family setup adds online players ) +// • Family Bank ( !family bank registers a vanilla chest as shared ) +// • Five shared goals with per-player contribution tracking +// • Loot share within 32 blocks (sibling helpers get a kill bonus) +// • Pity timer (idle siblings get an encouragement bundle every 60 min) +// +// All persistent state lives in world dynamic properties so it survives +// container restarts. No resource pack — vanilla items + titleraw + actionforms. + +import { world, system, ItemStack } from "@minecraft/server"; +import { ActionFormData } from "@minecraft/server-ui"; +// We don't actually use @minecraft/server-admin, but importing from it flips +// the beta-API gate for this pack so world.beforeEvents.chatSend resolves. +// If we ever drop this import, fall back to afterEvents.chatSend (no cancel). +import { variables } from "@minecraft/server-admin"; +void variables; // silence unused warning + +// ─── Tunables ────────────────────────────────────────────────── +const FAMILY_PROP = "family_roster_v1"; // { members: [, ...] } +const GOALS_PROP = "family_goals_v1"; // see initGoals() for shape +const BANK_PROP = "family_bank_v1"; // { x, y, z, dim } | null +const PROGRESS_PROP = "family_last_progress_at"; // per-player dyn prop, Date.now() + +const VANILLA_CHEST = "minecraft:chest"; +const LOOT_SHARE_RADIUS = 32; +const PITY_THRESHOLD_MS = 60 * 60 * 1000; // 60 min of no progress +const PITY_TICK_INTERVAL = 6000; // poll every 5 min (20 tps) +const NIGHTS_TICK_INTERVAL = 200; // every 10s + +const GOAL_DEFS = [ + { id: "logs", label: "Logs Chopped", target: 5000 }, + { id: "diamonds", label: "Diamonds Mined", target: 50 }, + { id: "mobs", label: "Monsters Defeated", target: 500 }, + { id: "crops", label: "Crops Harvested", target: 1000 }, + { id: "nights", label: "Nights Survived", target: 30 }, +]; + +const LOG_IDS = new Set([ + "minecraft:oak_log", "minecraft:spruce_log", "minecraft:birch_log", + "minecraft:jungle_log", "minecraft:acacia_log", "minecraft:dark_oak_log", + "minecraft:cherry_log", "minecraft:mangrove_log", + "minecraft:stripped_oak_log", "minecraft:stripped_spruce_log", + "minecraft:stripped_birch_log", "minecraft:stripped_jungle_log", + "minecraft:stripped_acacia_log", "minecraft:stripped_dark_oak_log", + "minecraft:stripped_cherry_log", "minecraft:stripped_mangrove_log", +]); +const DIAMOND_IDS = new Set([ + "minecraft:diamond_ore", "minecraft:deepslate_diamond_ore", +]); +// Block id → "growth"-like state value at full maturity +const CROP_AGES = { + "minecraft:wheat": 7, + "minecraft:carrots": 7, + "minecraft:potatoes": 7, + "minecraft:beetroot": 3, +}; + +// ─── In-memory mirror of dynamic properties ──────────────────── +let family = { members: [] }; +let goals = null; +let bank = null; +let lastDay = -1; // tracks day-rollover for the "nights survived" goal + +// ─── Persistence ─────────────────────────────────────────────── +function loadState() { + try { + const f = world.getDynamicProperty(FAMILY_PROP); + if (typeof f === "string") family = JSON.parse(f); + } catch (_) { family = { members: [] }; } + + try { + const g = world.getDynamicProperty(GOALS_PROP); + if (typeof g === "string") goals = JSON.parse(g); + } catch (_) { goals = null; } + if (!goals || !goals.goals) goals = initGoals(); + // Backfill any newly added goal definitions on upgrade + for (const def of GOAL_DEFS) { + if (!goals.goals[def.id]) { + goals.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} }; + } + } + + try { + const b = world.getDynamicProperty(BANK_PROP); + if (typeof b === "string") bank = JSON.parse(b); + } catch (_) { bank = null; } +} + +function initGoals() { + const g = { goals: {} }; + for (const def of GOAL_DEFS) { + g.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} }; + } + return g; +} + +function saveFamily() { + try { world.setDynamicProperty(FAMILY_PROP, JSON.stringify(family)); } catch (_) {} +} +function saveGoals() { + try { + const json = JSON.stringify(goals); + world.setDynamicProperty(GOALS_PROP, json); + const verify = world.getDynamicProperty(GOALS_PROP); + console.warn(`[Family] saveGoals: wrote ${json.length} chars, readback typeof=${typeof verify} len=${typeof verify === "string" ? verify.length : "n/a"}`); + } catch (e) { console.warn(`[Family] saveGoals FAILED: ${e}`); } +} +function saveBank() { + try { world.setDynamicProperty(BANK_PROP, bank ? JSON.stringify(bank) : undefined); } catch (_) {} +} + +function isFamily(player) { + if (!player || !family.members) return false; + return family.members.includes(player.id); +} + +// ─── Goal mutation ───────────────────────────────────────────── +function incrGoal(goalId, player, n = 1) { + if (!isFamily(player)) return; + const g = goals.goals[goalId]; + if (!g || g.completed) return; + g.current += n; + g.contributions[player.id] = (g.contributions[player.id] || 0) + n; + try { player.setDynamicProperty(PROGRESS_PROP, Date.now()); } catch (_) {} + + if (g.current >= g.target) { + g.completed = true; + g.current = g.target; + fireGoalReward(goalId); + } + saveGoals(); +} + +function fireGoalReward(goalId) { + const def = GOAL_DEFS.find(d => d.id === goalId); + const label = def ? def.label : goalId; + for (const p of world.getAllPlayers()) { + try { + p.onScreenDisplay.setTitle(`§6✦ Family Goal Complete!`, { + subtitle: `§e${label}`, + fadeInDuration: 10, + stayDuration: 80, + fadeOutDuration: 20, + }); + p.dimension.runCommand(`summon firework_rocket ${p.location.x} ${p.location.y + 2} ${p.location.z}`); + // v1 trophy: a named gold ingot. Easy to tell apart in inventory. + p.runCommand(`give @s gold_ingot 1`); + } catch (_) {} + } + world.sendMessage(`§6[Family] §eGoal complete: §f${label}§e! §7Trophy gold awarded.`); +} + +// ─── Family roster command ───────────────────────────────────── +function familySetup(player) { + const online = world.getAllPlayers(); + const before = new Set(family.members); + let added = 0; + for (const p of online) { + if (!before.has(p.id)) { + family.members.push(p.id); + added += 1; + } + } + saveFamily(); + + const names = online.map(p => p.name).join(", "); + if (added === 0) { + player.sendMessage(`§7[Family] §fAll currently-online players are already in the roster (${family.members.length} members).`); + } else { + player.sendMessage(`§a[Family] §fAdded ${added} new member(s). Roster (${family.members.length}): §e${names}§f.`); + world.sendMessage(`§a[Family] §fRoster updated by §e${player.name}§f.`); + } +} + +function familyReset(player) { + family = { members: [] }; + saveFamily(); + player.sendMessage("§c[Family] §fRoster cleared. Run §e!family setup§f again to repopulate."); +} + +// ─── Family Bank command ─────────────────────────────────────── +function familyBank(player) { + if (!isFamily(player)) { + player.sendMessage("§c[Family] §7Only family members can register the bank — run §e!family setup§7 first while everyone's online."); + return; + } + let view; + try { + view = player.getBlockFromViewDirection({ maxDistance: 6 }); + } catch (e) { + player.sendMessage(`§c[Family] §7Couldn't look at a block: ${e.message}`); + return; + } + const target = view?.block; + if (!target || target.typeId !== VANILLA_CHEST) { + player.sendMessage("§c[Family] §7Look at a vanilla chest within 6 blocks, then re-run §e!family bank§7."); + return; + } + bank = { + x: target.location.x, + y: target.location.y, + z: target.location.z, + dim: target.dimension.id, + }; + saveBank(); + player.sendMessage(`§a[Family] §fBank registered at §e${bank.x}, ${bank.y}, ${bank.z}§f. All family members can now open it.`); +} + +// ─── Bank gating (interact + break) ──────────────────────────── +function isBankAt(loc, dimId) { + if (!bank) return false; + return Math.floor(loc.x) === bank.x + && Math.floor(loc.y) === bank.y + && Math.floor(loc.z) === bank.z + && bank.dim === dimId; +} + +try { + world.beforeEvents.playerInteractWithBlock.subscribe((event) => { + const block = event.block; + if (block.typeId !== VANILLA_CHEST) return; + if (!isBankAt(block.location, block.dimension.id)) return; + if (isFamily(event.player)) return; + event.cancel = true; + const playerRef = event.player; + system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can open this chest.")); + }); +} catch (e) { + console.warn(`[Family] beforeEvents.playerInteractWithBlock unavailable: ${e}`); +} + +try { + world.beforeEvents.playerBreakBlock.subscribe((event) => { + const block = event.block; + if (block.typeId !== VANILLA_CHEST) return; + if (!isBankAt(block.location, block.dimension.id)) return; + + if (isFamily(event.player)) { + // Family member breaking the bank → un-register so it stops being protected + bank = null; + saveBank(); + const p = event.player; + system.run(() => p.sendMessage("§7[Family Bank] §fBank chest broken — un-registered. Place a new chest and run §e!family bank§f to re-register.")); + return; + } + event.cancel = true; + const playerRef = event.player; + system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can break this chest.")); + }); +} catch (e) { + console.warn(`[Family] beforeEvents.playerBreakBlock unavailable: ${e}`); +} + +// ─── PvP off (heal damage between siblings) ──────────────────── +// Bedrock has no server.properties pvp toggle — we hook the after-event +// and immediately re-heal. Net visible effect: zero damage between players. +world.afterEvents.entityHurt.subscribe((event) => { + const source = event.damageSource?.damagingEntity; + const victim = event.hurtEntity; + if (!source || !victim) return; + if (source.typeId !== "minecraft:player" || victim.typeId !== "minecraft:player") return; + try { + const health = victim.getComponent("minecraft:health"); + if (!health) return; + const restored = Math.min(health.effectiveMax, health.currentValue + event.damage); + health.setCurrentValue(restored); + } catch (_) {} +}); + +// ─── Goal triggers ───────────────────────────────────────────── +world.afterEvents.playerBreakBlock.subscribe((event) => { + const player = event.player; + if (!isFamily(player)) return; + const id = event.brokenBlockPermutation?.type?.id; + if (!id) return; + + if (LOG_IDS.has(id)) { + incrGoal("logs", player); + return; + } + if (DIAMOND_IDS.has(id)) { + incrGoal("diamonds", player); + return; + } + if (id in CROP_AGES) { + // Only fully grown crops count + let states = null; + try { states = event.brokenBlockPermutation.getAllStates(); } catch (_) {} + const age = states && (states["growth"] ?? states["age"]); + if (typeof age === "number" && age >= CROP_AGES[id]) { + incrGoal("crops", player); + } + } +}); + +world.afterEvents.entityDie.subscribe((event) => { + const dead = event.deadEntity; + if (!dead || dead.typeId === "minecraft:player") return; + const source = event.damageSource?.damagingEntity; + if (!source || source.typeId !== "minecraft:player") return; + if (!isFamily(source)) return; + + // Goal: monsters only + let isMonster = false; + try { + const fam = dead.getComponent("minecraft:type_family"); + if (fam && typeof fam.hasTypeFamily === "function" && fam.hasTypeFamily("monster")) { + isMonster = true; + } + } catch (_) {} + if (isMonster) incrGoal("mobs", source); + + // Loot share: every family member in 32 blocks (other than killer) gets cooked beef. + // Sharing helpers, not real drop mirroring — keeps the drop pipeline untouched. + try { + const k = source.location; + const r2 = LOOT_SHARE_RADIUS * LOOT_SHARE_RADIUS; + for (const other of world.getAllPlayers()) { + if (other.id === source.id) continue; + if (!isFamily(other)) continue; + if (other.dimension.id !== source.dimension.id) continue; + const dx = other.location.x - k.x; + const dy = other.location.y - k.y; + const dz = other.location.z - k.z; + if (dx*dx + dy*dy + dz*dz <= r2) { + other.runCommand("give @s cooked_beef 2"); + } + } + } catch (_) {} +}); + +// ─── Nights survived ─────────────────────────────────────────── +system.runInterval(() => { + let day; + try { day = world.getDay(); } catch (_) { return; } + if (typeof day !== "number") return; + if (lastDay === -1) { lastDay = day; return; } + if (day > lastDay) { + const diff = day - lastDay; + lastDay = day; + for (const p of world.getAllPlayers()) { + if (isFamily(p)) incrGoal("nights", p, diff); + } + } +}, NIGHTS_TICK_INTERVAL); + +// ─── Pity timer ──────────────────────────────────────────────── +system.runInterval(() => { + const now = Date.now(); + for (const p of world.getAllPlayers()) { + if (!isFamily(p)) continue; + + let last; + try { last = p.getDynamicProperty(PROGRESS_PROP); } catch (_) { last = undefined; } + if (typeof last !== "number") { + // First time we've seen this family member contribute (or first session) — initialize, don't pity yet + try { p.setDynamicProperty(PROGRESS_PROP, now); } catch (_) {} + continue; + } + if (now - last < PITY_THRESHOLD_MS) continue; + + // Drop encouragement bundle directly into inventory + try { + p.runCommand("give @s diamond 1"); + p.runCommand("give @s cooked_beef 16"); + p.runCommand("give @s iron_pickaxe 1"); + p.sendMessage("§6[Family] §fA little boost to keep you going! §7(family pity bundle)"); + p.setDynamicProperty(PROGRESS_PROP, now); // reset so we don't spam every 5 min + } catch (_) {} + } +}, PITY_TICK_INTERVAL); + +// ─── Goal display ────────────────────────────────────────────── +function makeBar(pct) { + const filled = Math.max(0, Math.min(10, Math.floor(pct / 10))); + return "§a" + "▰".repeat(filled) + "§8" + "▰".repeat(10 - filled); +} + +async function showGoals(player, perPlayerMode) { + const lines = []; + for (const def of GOAL_DEFS) { + const g = goals.goals[def.id]; + if (!g) continue; + const cur = Math.min(g.current, g.target); + const pct = Math.floor((cur / g.target) * 100); + const bar = makeBar(pct); + const status = g.completed ? " §a✔" : ""; + + if (perPlayerMode) { + const my = g.contributions[player.id] || 0; + const myPct = cur > 0 ? Math.floor((my / cur) * 100) : 0; + lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}\n§7 You: §f${my}§7 (${myPct}% of progress)`); + } else { + lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}`); + } + } + if (!lines.length) lines.push("§7No goals configured."); + + const form = new ActionFormData() + .title(perPlayerMode ? "My Goal Contributions" : "Family Goals") + .body(lines.join("\n\n")) + .button("§7OK"); + try { await form.show(player); } catch (_) {} +} + +// ─── Family menu (entered via scriptevent from hub-return compass) ───── +function showRoster(player) { + if (family.members.length === 0) { + player.sendMessage("§7[Family] §fNo roster set yet — open §e🏆 Family§f from your compass and pick §eAdd me to the family§f."); + return; + } + const names = world.getAllPlayers() + .filter(p => family.members.includes(p.id)) + .map(p => p.name); + player.sendMessage(`§a[Family] §fRoster: §e${family.members.length}§f members. Online: §e${names.join(", ") || "(none)"}§f.`); +} + +async function openFamilyMenu(player) { + const inFamily = isFamily(player); + const form = new ActionFormData() + .title("🏆 Family") + .body(`§7Roster: §f${family.members.length}§7 members${inFamily ? " §a(you're in)" : " §c(you're not)"}\n§7Bank: ${bank ? `§f${bank.x}, ${bank.y}, ${bank.z}` : "§cnot set"}`); + + const actions = []; + form.button("🎯 Family Goals"); actions.push("goals"); + form.button("📝 My Contributions"); actions.push("mygoals"); + form.button("👥 Show Roster"); actions.push("roster"); + form.button("➕ Add me / Add online players"); actions.push("setup"); + form.button("🏦 Register Family Bank (look at chest)"); actions.push("bank"); + form.button("♻ Reset Roster (danger)"); actions.push("reset"); + form.button("§7Cancel"); actions.push("cancel"); + + let res; + try { res = await form.show(player); } catch (_) { return; } + if (res.canceled || res.selection === undefined) return; + const a = actions[res.selection]; + + if (a === "goals") return showGoals(player, false); + if (a === "mygoals") return showGoals(player, true); + if (a === "roster") return showRoster(player); + if (a === "setup") return familySetup(player); + if (a === "bank") return familyBank(player); + if (a === "reset") return familyReset(player); +} + +// scriptEventReceive is the cross-pack dispatch surface. Hub-return's compass +// menu fires `scriptevent family:menu` to enter our UI. Direct ids are also +// handled so an opped player can run e.g. `/scriptevent family:setup`. +try { + system.afterEvents.scriptEventReceive.subscribe((event) => { + const id = event.id; + if (!id || !id.startsWith("family:")) return; + const player = event.sourceEntity; + if (!player || player.typeId !== "minecraft:player") return; + switch (id) { + case "family:menu": system.run(() => openFamilyMenu(player)); break; + case "family:goals": system.run(() => showGoals(player, false)); break; + case "family:mygoals": system.run(() => showGoals(player, true)); break; + case "family:roster": showRoster(player); break; + case "family:setup": familySetup(player); break; + case "family:bank": familyBank(player); break; + case "family:reset": familyReset(player); break; + } + }); +} catch (e) { + console.warn(`[Family] scriptEventReceive unavailable: ${e}`); +} + +// ─── Boot ────────────────────────────────────────────────────── +system.run(() => { + loadState(); + saveGoals(); // persist any backfilled goal defs from upgrade + + world.sendMessage("§6[Family] §fLoaded. Right-click your §dHub Compass§f to find the §6🏆 Family§f menu."); +}); diff --git a/redstone-link-addon/README.md b/redstone-link-addon/README.md new file mode 100644 index 0000000..d6a1b5a --- /dev/null +++ b/redstone-link-addon/README.md @@ -0,0 +1,76 @@ +# Redstone Link + +Wireless redstone. Vanilla dust needs a solid block under it and gets cut off by +terrain; this replaces the wire entirely with a transmitter/receiver pair bound +by a channel name. + +| Block | What it does | +|---|---| +| `silverlabs:redstone_link_tx` — Redstone Link Transmitter | Reads redstone power from any of its six neighbours. While powered, its channel is live. | +| `silverlabs:redstone_link_rx` — Redstone Link Receiver | While its channel is live, emits full-strength (15) power in every direction. | + +Craft either from a redstone block + ender pearl (+ copper for the transmitter, +iron for the receiver). Both give 2. + +**Setting a channel:** right-click a block and type a name, e.g. `front-door`. +Every transmitter and receiver sharing that name is linked — many-to-many, any +distance, through any terrain, in any direction. Newly placed blocks start on +channel `default`. + +## How it works, and why + +Bedrock custom blocks can *read* redstone power but cannot *emit* it — there is +no script API for that. So a live receiver is physically swapped for a vanilla +`minecraft:redstone_block` (strength 15, omnidirectional, needs no support) and +swapped back when the channel goes quiet. + +That swap is why `scripts/registry.js` exists: while a receiver is powered the +block itself no longer says it is a receiver, so the positions are kept in a +world dynamic property. It survives container restarts, and a reconcile pass at +boot repairs anything left inconsistent by a crash. + +Breaking a powered receiver is intercepted so it returns the receiver block +rather than a free redstone block. + +## Limits + +These are inherent to the approach, not bugs: + +- **Both ends must be in loaded chunks.** A link whose receiver sits in unloaded + terrain simply does not fire. Cover it with a ticking area if it must always work. +- **~2 tick latency.** Fine for doors, lamps and farms; not for sub-tick clocks. +- **Binary, not graded.** Output is always 15, never a decayed signal. +- **Same dimension only.** +- **256 nodes per world.** + +A transmitter ignores power coming from its own channel's receivers, so putting +a matched pair side by side will not latch itself on. + +## Console / bridge control + +Blocks placed by `/setblock`, `/fill`, or the mc-ai-bridge MCP build tools never +fire `playerPlaceBlock`, so they are not registered and stay inert. Register them +explicitly (`sourceEntity` is null over RCON/the bridge, so no player is needed): + +``` +/scriptevent rlink:register +/scriptevent rlink:unregister +/scriptevent rlink:status +``` + +`rlink:status` reports node counts and active channels — the quickest way to +confirm the pack is alive on a headless server. + +## Deployment + +Bind-mount both packs into the target service in `docker-compose.yml`, then pin +the pack UUIDs into that world's `world_behavior_packs.json` / +`world_resource_packs.json`. **The version arrays must match the manifest header +versions exactly** or the pack is silently dropped from the stack. + +| Pack | UUID | Version | +|---|---|---| +| BP header | `e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b001` | `[1, 0, 0]` | +| RP header | `e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b004` | `[1, 0, 0]` | + +Currently deployed to **lobby** (`Hub World`) only. diff --git a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json new file mode 100644 index 0000000..14f461a --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:redstone_link_rx", + "menu_category": { + "category": "items", + "group": "itemGroup.name.redstone" + } + }, + "components": { + "minecraft:destructible_by_mining": { + "seconds_to_destroy": 1.5 + }, + "minecraft:destructible_by_explosion": { + "explosion_resistance": 20.0 + }, + "minecraft:map_color": "#1A4C8C", + "minecraft:material_instances": { + "*": { + "texture": "redstone_link_rx", + "render_method": "opaque" + } + } + } + } +} diff --git a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json new file mode 100644 index 0000000..bd20b30 --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json @@ -0,0 +1,27 @@ +{ + "format_version": "1.21.0", + "minecraft:block": { + "description": { + "identifier": "silverlabs:redstone_link_tx", + "menu_category": { + "category": "items", + "group": "itemGroup.name.redstone" + } + }, + "components": { + "minecraft:destructible_by_mining": { + "seconds_to_destroy": 1.5 + }, + "minecraft:destructible_by_explosion": { + "explosion_resistance": 20.0 + }, + "minecraft:map_color": "#8C1A1A", + "minecraft:material_instances": { + "*": { + "texture": "redstone_link_tx", + "render_method": "opaque" + } + } + } + } +} diff --git a/redstone-link-addon/redstone_link_BP/manifest.json b/redstone-link-addon/redstone_link_BP/manifest.json new file mode 100644 index 0000000..cf8795d --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/manifest.json @@ -0,0 +1,38 @@ +{ + "format_version": 2, + "header": { + "name": "Redstone Link", + "description": "Wireless redstone: pair a transmitter and a receiver by channel name, no wire in between", + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b001", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "data", + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b002", + "version": [1, 0, 0] + }, + { + "type": "script", + "language": "javascript", + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b003", + "version": [1, 0, 0], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "module_name": "@minecraft/server", + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-ui", + "version": "1.3.0" + }, + { + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b004", + "version": [1, 0, 0] + } + ] +} diff --git a/redstone-link-addon/redstone_link_BP/pack_icon.png b/redstone-link-addon/redstone_link_BP/pack_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f030e4af5e9509f7acdf6d46407392eaade92b35 GIT binary patch literal 1758 zcmc&#`#Tc~7@kYzau~Tp?vm@W5VPEwqD7KM5otM`I4+}^Rw(3BVjRh~u*#9U3ClHQ zZOKfjV^~{-xfCO}Rf`>U&Uwy%aL)6*-}n9aJaExar4Pt+;X+@(4pjU2wXmJ!Hw|W33{})h;zh-ip3dHFb5?Xv0DaG#q}7OsCOm zu|;Kx1Xl5lX%*%Ow(|gPNljfk9x6JfY4GZgEwWiIM5j4@O}! zV!>ywW&;g(=gHkHxR=rS_F@Om+(H_bDDa!Q(05DI5*fp)5I?1Bm|j%=uGGo!{!HmB zfv7~G)@#L? z@!&GGMxZ6H!*a6ZYzL1E%oAt`e`2h^nr(ids;4U*8qtWPkZV$e)RElWTv}%?VL;D~G*H!CL`D!Gmm-G`$h{TZHuqsBJ7hU}JzI=p=_fy7U)BGaeIJj z9aBToiVZOB)KU-a9_&)@A=1Nh#r?e6^~5ghbJ6?P8Z`S9a?Hym^BW|=3E)}IMn+OB z;)Zk?JsixpT+8;2bjQht4+i#GBs*jBaKppHfHT;W9T(ph{t1F#m0JStNU{QE*QD4p9N<~DvUn#$8_U){BGL+Ia**e}t(@>0uGVSC z5FLmK8iz~0KG$#f44?11F+sfg!q?jkraZ=m511;kx^0kdY@L1R)#8KSoE#VN7D`UH zo0&WO{XQk{7s7W>w#6r(NLs4Tyv`XB=91@qUk%~5=^7PmTPz?q8UYKN?oy(y)&eD> zt58zIFL9)!{eiEMT4LG?K>!)7BC@5w`&0f9sJr@jEEX`;*Ky4y6rA=>AIGxu2?>dz zEw-r5iTP|d*1xOZ{M{IBI>ch6(PAMNuC?q85Adl3x5I;=_V!D8b~8F3CI%8U?6$;@ zm#a|zI7>T~4TD^iHAq+YkisD#($p81J@;~p-!LUA_RQ%BISZ2BuKo$o^9o4-lmP(s z{G=Rp$e_Cn-JXm+JwYRtCvJTo2Au3ol^<7+Xzvls>^)X58P8vx(Cn_O8*rlZJ6xW= z*&W|-sgZp}bP)bF5rOG<%jzi(ZL|pg`$mo-q?`rm=IX+U*rLU>*Q#dhryDK_VOt<} z2X^Fa$%E-D2D#P&;`vFK-S0C5;tlGu2<~CA2CHWB)7vKLmxKqQ;56OMtnmAFe(vPZ z8I)d3qM$w;@%5U%tj5Y$-e0;l6v4Yj?{of%#m!{=@4*zg%Ng!j+-^R&`L_U`?%uF^ I*V9-21nMzD)c^nh literal 0 HcmV?d00001 diff --git a/redstone-link-addon/redstone_link_BP/recipes/redstone_link_rx.json b/redstone-link-addon/redstone_link_BP/recipes/redstone_link_rx.json new file mode 100644 index 0000000..1d6e0f7 --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/recipes/redstone_link_rx.json @@ -0,0 +1,19 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shapeless": { + "description": { + "identifier": "silverlabs:redstone_link_rx_recipe" + }, + "tags": ["crafting_table"], + "unlock": { "context": "AlwaysUnlocked" }, + "ingredients": [ + { "item": "minecraft:redstone_block" }, + { "item": "minecraft:ender_pearl" }, + { "item": "minecraft:iron_ingot" } + ], + "result": { + "item": "silverlabs:redstone_link_rx", + "count": 2 + } + } +} diff --git a/redstone-link-addon/redstone_link_BP/recipes/redstone_link_tx.json b/redstone-link-addon/redstone_link_BP/recipes/redstone_link_tx.json new file mode 100644 index 0000000..82e0dfa --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/recipes/redstone_link_tx.json @@ -0,0 +1,19 @@ +{ + "format_version": "1.21.0", + "minecraft:recipe_shapeless": { + "description": { + "identifier": "silverlabs:redstone_link_tx_recipe" + }, + "tags": ["crafting_table"], + "unlock": { "context": "AlwaysUnlocked" }, + "ingredients": [ + { "item": "minecraft:redstone_block" }, + { "item": "minecraft:ender_pearl" }, + { "item": "minecraft:copper_ingot" } + ], + "result": { + "item": "silverlabs:redstone_link_tx", + "count": 2 + } + } +} diff --git a/redstone-link-addon/redstone_link_BP/scripts/main.js b/redstone-link-addon/redstone_link_BP/scripts/main.js new file mode 100644 index 0000000..cf8291f --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/scripts/main.js @@ -0,0 +1,354 @@ +// Redstone Link Addon +// +// Wireless redstone for the SilverLABS multiworld. Place a transmitter next to +// any redstone source and a receiver anywhere else, give both the same channel +// name, and the receiver emits full-strength power whenever the transmitter is +// powered. No wire, no line of sight, no support block, any distance. +// +// Why a block swap: Bedrock custom blocks can READ redstone power but cannot +// EMIT it -- there is no script API for that. So a live receiver is physically +// replaced by a vanilla redstone block (strength 15, omnidirectional, needs no +// support) and swapped back when the channel goes quiet. registry.js remembers +// where the receivers are, because while powered the block no longer says so. +// +// Known limits, by design: +// * Both ends must be in loaded chunks. Out-of-range links simply do nothing. +// * ~2 tick latency. Not for sub-tick contraptions. +// * Binary, not graded: output is always 15. + +import { world, system, ItemStack, BlockPermutation } from "@minecraft/server"; +import { ModalFormData } from "@minecraft/server-ui"; +import { isTransmitterPowered } from "./power.js"; +import * as registry from "./registry.js"; + +const TX_ID = "silverlabs:redstone_link_tx"; +const RX_ID = "silverlabs:redstone_link_rx"; +const LIVE_ID = "minecraft:redstone_block"; + +const TICK_INTERVAL = 2; +const MAX_CHANNEL_LEN = 24; +const DEFAULT_CHANNEL = "default"; + +function roleOf(typeId) { + if (typeId === TX_ID) return "tx"; + if (typeId === RX_ID) return "rx"; + return null; +} + +function sanitiseChannel(raw) { + if (typeof raw !== "string") return ""; + return raw.trim().toLowerCase().slice(0, MAX_CHANNEL_LEN); +} + +function blockAt(node) { + // Returns undefined when the chunk is not loaded -- callers must treat that + // as "unknown", never as "gone", or we would delete live nodes. + let dim; + try { dim = world.getDimension(node.d); } catch (_) { return undefined; } + if (!dim) return undefined; + try { return dim.getBlock({ x: node.x, y: node.y, z: node.z }); } catch (_) { return undefined; } +} + +function setBlockTo(block, typeId) { + try { + block.setPermutation(BlockPermutation.resolve(typeId)); + return true; + } catch (e) { + console.warn(`[RedstoneLink] failed to set ${typeId}: ${e}`); + return false; + } +} + +// --- The link engine ------------------------------------------------------ + +function tick() { + const nodes = registry.all(); + if (nodes.length === 0) return; + + // Receiver positions grouped by channel, so a transmitter can ignore power + // coming from its own channel's receivers (self-latch guard). + const rxKeysByChannel = new Map(); + for (const n of nodes) { + if (n.r !== "rx" || !n.c) continue; + let set = rxKeysByChannel.get(n.c); + if (!set) { set = new Set(); rxKeysByChannel.set(n.c, set); } + set.add(registry.nodeKey(n)); + } + + const stale = new Set(); + const live = new Set(); + + // Pass 1 -- which channels are being driven? + for (const n of nodes) { + if (n.r !== "tx") continue; + const b = blockAt(n); + if (b === undefined) continue; // chunk unloaded: unknown + if (b.typeId !== TX_ID) { stale.add(registry.nodeKey(n)); continue; } + if (!n.c || live.has(n.c)) continue; // unset, or already known live + if (isTransmitterPowered(b, rxKeysByChannel.get(n.c))) live.add(n.c); + } + + // Pass 2 -- make every receiver match its channel. + for (const n of nodes) { + if (n.r !== "rx") continue; + const b = blockAt(n); + if (b === undefined) continue; + const isOn = b.typeId === LIVE_ID; + const isOff = b.typeId === RX_ID; + if (!isOn && !isOff) { stale.add(registry.nodeKey(n)); continue; } + const want = Boolean(n.c) && live.has(n.c); + if (want && !isOn) setBlockTo(b, LIVE_ID); + else if (!want && !isOff) setBlockTo(b, RX_ID); + } + + if (stale.size > 0) registry.removeKeys(stale); +} + +/** + * Startup pass. Anything that got out of sync while the world was unloaded -- + * a receiver left as a redstone block after a crash, a node whose block was + * removed by an explosion -- is corrected or forgotten here. + */ +function reconcile() { + const nodes = registry.all(); + if (nodes.length === 0) return; + const stale = new Set(); + let restored = 0; + for (const n of nodes) { + const b = blockAt(n); + if (b === undefined) continue; + if (n.r === "tx") { + if (b.typeId !== TX_ID) stale.add(registry.nodeKey(n)); + continue; + } + if (b.typeId === LIVE_ID) { + // No transmitter has been evaluated yet, so nothing is live: park it off. + if (setBlockTo(b, RX_ID)) restored++; + } else if (b.typeId !== RX_ID) { + stale.add(registry.nodeKey(n)); + } + } + const dropped = registry.removeKeys(stale); + if (restored || dropped) { + console.warn(`[RedstoneLink] reconcile: ${restored} receiver(s) reset, ${dropped} stale node(s) dropped`); + } +} + +// --- Channel UI ----------------------------------------------------------- + +async function openChannelForm(player, block, node) { + const role = roleOf(block.typeId); + const label = role === "tx" ? "Transmitter" : "Receiver"; + const current = node?.c ?? ""; + + const form = new ModalFormData() + .title(`§6Redstone Link §7${label}`) + .textField( + "Channel name\n§7Any transmitter and receiver sharing a channel are linked.", + "e.g. front-door", + current + ); + + let res; + try { res = await form.show(player); } catch (_) { return; } + if (res.canceled) return; + + const channel = sanitiseChannel(res.formValues?.[0]); + if (!channel) { + player.sendMessage("§c[Redstone Link] §7Channel name cannot be empty."); + return; + } + + const saved = registry.add(block.dimension.id, block.location, role, channel); + if (!saved) { + player.sendMessage(`§c[Redstone Link] §7Too many links in this world (max ${registry.MAX_NODES}).`); + return; + } + player.sendMessage(`§a[Redstone Link] §7${label} set to channel §f${channel}§7.`); +} + +// --- Events --------------------------------------------------------------- + +try { + world.beforeEvents.playerInteractWithBlock.subscribe((event) => { + const block = event.block; + if (!block) return; + const role = roleOf(block.typeId); + if (!role) return; + event.cancel = true; + const playerRef = event.player; + const dimId = block.dimension.id; + const loc = { x: block.location.x, y: block.location.y, z: block.location.z }; + system.run(() => { + const node = registry.find(dimId, loc); + openChannelForm(playerRef, block, node); + }); + }); +} catch (e) { + console.warn(`[RedstoneLink] beforeEvents.playerInteractWithBlock unavailable: ${e}`); +} + +try { + world.afterEvents.playerPlaceBlock.subscribe((event) => { + const block = event.block; + if (!block) return; + const role = roleOf(block.typeId); + if (!role) return; + const added = registry.add(block.dimension.id, block.location, role, DEFAULT_CHANNEL); + if (!added) { + event.player?.sendMessage( + `§c[Redstone Link] §7Too many links in this world (max ${registry.MAX_NODES}). This one will not work.` + ); + return; + } + event.player?.sendMessage( + `§7[Redstone Link] On channel §f${DEFAULT_CHANNEL}§7. Interact to rename it.` + ); + }); +} catch (e) { + console.warn(`[RedstoneLink] afterEvents.playerPlaceBlock unavailable: ${e}`); +} + +try { + world.beforeEvents.playerBreakBlock.subscribe((event) => { + const block = event.block; + if (!block) return; + const dimId = block.dimension.id; + const loc = { x: block.location.x, y: block.location.y, z: block.location.z }; + const node = registry.find(dimId, loc); + if (!node) return; + + if (block.typeId === LIVE_ID) { + // A live receiver. Left alone, vanilla would hand out a free redstone + // block, so intercept: drop the receiver item instead. + event.cancel = true; + const playerRef = event.player; + system.run(() => { + registry.remove(dimId, loc); + let dim; + try { dim = world.getDimension(dimId); } catch (_) { return; } + try { dim.getBlock(loc)?.setPermutation(BlockPermutation.resolve("minecraft:air")); } catch (_) {} + let creative = false; + try { creative = playerRef?.getGameMode?.() === "creative"; } catch (_) { creative = false; } + if (creative) return; + try { + dim.spawnItem(new ItemStack(RX_ID, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 }); + } catch (_) {} + }); + return; + } + + // Unpowered node: vanilla break already returns the right item. + system.run(() => registry.remove(dimId, loc)); + }); +} catch (e) { + console.warn(`[RedstoneLink] beforeEvents.playerBreakBlock unavailable: ${e}`); +} + +// --- Console / bridge control -------------------------------------------- +// +// Blocks placed by command rather than by hand -- /setblock, /fill, or the +// mc-ai-bridge MCP build tools -- never fire playerPlaceBlock, so they are not +// in the registry and stay inert. These script events give the console and the +// bridge a way to register them. sourceEntity is null when run via RCON or the +// bridge, so we never require a player here. +// +// /scriptevent rlink:register +// /scriptevent rlink:unregister +// /scriptevent rlink:status + +function announce(text) { + try { world.getDimension("overworld").runCommand(`say ${text}`); } catch (_) {} + console.warn(text); +} + +function handleRegister(args) { + if (args.length < 4) { + announce("[RedstoneLink] usage: rlink:register "); + return; + } + const x = Number(args[0]), y = Number(args[1]), z = Number(args[2]); + if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) { + announce("[RedstoneLink] register: coordinates must be numbers"); + return; + } + const channel = sanitiseChannel(args.slice(3).join(" ")); + if (!channel) { + announce("[RedstoneLink] register: channel name required"); + return; + } + const loc = { x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) }; + let block; + try { block = world.getDimension("overworld").getBlock(loc); } catch (_) { block = undefined; } + if (!block) { + announce(`[RedstoneLink] register: no loaded block at ${loc.x},${loc.y},${loc.z}`); + return; + } + const role = roleOf(block.typeId); + if (!role) { + announce(`[RedstoneLink] register: ${block.typeId} is not a link block`); + return; + } + const saved = registry.add("minecraft:overworld", loc, role, channel); + if (!saved) { + announce(`[RedstoneLink] register: registry full (max ${registry.MAX_NODES})`); + return; + } + announce(`[RedstoneLink] registered ${role} at ${loc.x},${loc.y},${loc.z} on channel ${channel}`); +} + +function handleUnregister(args) { + if (args.length < 3) { + announce("[RedstoneLink] usage: rlink:unregister "); + return; + } + const loc = { + x: Math.floor(Number(args[0])), + y: Math.floor(Number(args[1])), + z: Math.floor(Number(args[2])), + }; + const gone = registry.remove("minecraft:overworld", loc); + announce(`[RedstoneLink] unregister ${loc.x},${loc.y},${loc.z}: ${gone ? "removed" : "not found"}`); +} + +function handleStatus() { + const nodes = registry.all(); + const tx = nodes.filter((n) => n.r === "tx").length; + const rx = nodes.filter((n) => n.r === "rx").length; + const channels = [...new Set(nodes.map((n) => n.c).filter(Boolean))]; + announce(`[RedstoneLink] alive: ${nodes.length} node(s), ${tx} tx, ${rx} rx, channels=[${channels.join(",")}]`); +} + +try { + system.afterEvents.scriptEventReceive.subscribe((event) => { + const id = event.id; + if (!id || !id.startsWith("rlink:")) return; + const args = (event.message || "").trim().split(/\s+/).filter(Boolean); + try { + switch (id) { + case "rlink:register": handleRegister(args); break; + case "rlink:unregister": handleUnregister(args); break; + case "rlink:status": handleStatus(); break; + default: announce(`[RedstoneLink] unknown command ${id}`); + } + } catch (e) { + announce(`[RedstoneLink] ${id} failed: ${e}`); + } + }); +} catch (e) { + console.warn(`[RedstoneLink] scriptEventReceive unavailable: ${e}`); +} + +// --- Boot ----------------------------------------------------------------- + +system.run(() => { + try { reconcile(); } catch (e) { console.warn(`[RedstoneLink] reconcile failed: ${e}`); } + try { + system.runInterval(() => { + try { tick(); } catch (e) { console.warn(`[RedstoneLink] tick failed: ${e}`); } + }, TICK_INTERVAL); + } catch (e) { + console.warn(`[RedstoneLink] could not start tick loop: ${e}`); + } + world.sendMessage("§c[Redstone Link] §7Wireless redstone loaded."); +}); diff --git a/redstone-link-addon/redstone_link_BP/scripts/power.js b/redstone-link-addon/redstone_link_BP/scripts/power.js new file mode 100644 index 0000000..bd8b8cb --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/scripts/power.js @@ -0,0 +1,84 @@ +// Redstone input detection for the transmitter. +// +// Mirrors the approach already proven in hemp-addon's sun lamp: try the script +// API's getRedstonePower() first, and fall back to inspecting the six +// neighbouring blocks when it is unavailable or reports nothing. + +export const REDSTONE_FACES = [ + [1, 0, 0], [-1, 0, 0], + [0, 1, 0], [0, -1, 0], + [0, 0, 1], [0, 0, -1], +]; + +export function isPowerSource(b) { + if (!b) return false; + const t = b.typeId; + if (t === "minecraft:redstone_block") return true; + if (t === "minecraft:lit_redstone_torch" || t === "minecraft:redstone_torch") { + try { return b.permutation.getState("toggle_bit") !== false; } catch (_) {} + return t === "minecraft:lit_redstone_torch"; + } + if (t === "minecraft:powered_repeater") return true; + if (t === "minecraft:powered_comparator") return true; + if (t === "minecraft:lever") { + try { return b.permutation.getState("open_bit") === true; } catch (_) { return false; } + } + if (t.endsWith("_button")) { + try { return b.permutation.getState("button_pressed_bit") === true; } catch (_) { return false; } + } + if (t.endsWith("_pressure_plate")) { + try { + const r = b.permutation.getState("redstone_signal") ?? 0; + return r > 0; + } catch (_) { return false; } + } + if (t === "minecraft:redstone_wire") { + try { + const p = b.permutation.getState("redstone_signal") ?? 0; + return p > 0; + } catch (_) { return false; } + } + if (t === "minecraft:daylight_detector_inverted") return true; + return false; +} + +/** + * Is this transmitter receiving power? + * + * `ignoreKeys` holds the position keys of receivers on the SAME channel. A + * powered receiver is a real redstone block, so without this guard a + * transmitter sitting next to its own channel's receiver would latch itself on + * forever. We skip those positions, and when one is adjacent we also skip the + * getRedstonePower() fast path, because that call cannot exclude a position. + */ +export function isTransmitterPowered(block, ignoreKeys) { + const dim = block.dimension; + const dimId = dim.id; + const { x, y, z } = block.location; + + let adjacentSameChannelReceiver = false; + if (ignoreKeys && ignoreKeys.size > 0) { + for (const [dx, dy, dz] of REDSTONE_FACES) { + if (ignoreKeys.has(`${dimId}|${x + dx}|${y + dy}|${z + dz}`)) { + adjacentSameChannelReceiver = true; + break; + } + } + } + + if (!adjacentSameChannelReceiver) { + try { + const p = block.getRedstonePower(); + if (typeof p === "number" && p > 0) return true; + } catch (_) {} + } + + for (const [dx, dy, dz] of REDSTONE_FACES) { + const pos = { x: x + dx, y: y + dy, z: z + dz }; + if (ignoreKeys && ignoreKeys.has(`${dimId}|${pos.x}|${pos.y}|${pos.z}`)) continue; + let nb; + try { nb = dim.getBlock(pos); } catch (_) { continue; } + if (isPowerSource(nb)) return true; + } + return false; +} diff --git a/redstone-link-addon/redstone_link_BP/scripts/registry.js b/redstone-link-addon/redstone_link_BP/scripts/registry.js new file mode 100644 index 0000000..1676056 --- /dev/null +++ b/redstone-link-addon/redstone_link_BP/scripts/registry.js @@ -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(); +} diff --git a/redstone-link-addon/redstone_link_RP/blocks.json b/redstone-link-addon/redstone_link_RP/blocks.json new file mode 100644 index 0000000..2b4511f --- /dev/null +++ b/redstone-link-addon/redstone_link_RP/blocks.json @@ -0,0 +1,5 @@ +{ + "format_version": [1, 1, 0], + "silverlabs:redstone_link_tx": { "sound": "stone" }, + "silverlabs:redstone_link_rx": { "sound": "stone" } +} diff --git a/redstone-link-addon/redstone_link_RP/manifest.json b/redstone-link-addon/redstone_link_RP/manifest.json new file mode 100644 index 0000000..bed6a34 --- /dev/null +++ b/redstone-link-addon/redstone_link_RP/manifest.json @@ -0,0 +1,17 @@ +{ + "format_version": 2, + "header": { + "name": "Redstone Link Resources", + "description": "Textures and lang for the silverlabs:redstone_link_tx / _rx blocks", + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b004", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "resources", + "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b005", + "version": [1, 0, 0] + } + ] +} diff --git a/redstone-link-addon/redstone_link_RP/pack_icon.png b/redstone-link-addon/redstone_link_RP/pack_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f030e4af5e9509f7acdf6d46407392eaade92b35 GIT binary patch literal 1758 zcmc&#`#Tc~7@kYzau~Tp?vm@W5VPEwqD7KM5otM`I4+}^Rw(3BVjRh~u*#9U3ClHQ zZOKfjV^~{-xfCO}Rf`>U&Uwy%aL)6*-}n9aJaExar4Pt+;X+@(4pjU2wXmJ!Hw|W33{})h;zh-ip3dHFb5?Xv0DaG#q}7OsCOm zu|;Kx1Xl5lX%*%Ow(|gPNljfk9x6JfY4GZgEwWiIM5j4@O}! zV!>ywW&;g(=gHkHxR=rS_F@Om+(H_bDDa!Q(05DI5*fp)5I?1Bm|j%=uGGo!{!HmB zfv7~G)@#L? z@!&GGMxZ6H!*a6ZYzL1E%oAt`e`2h^nr(ids;4U*8qtWPkZV$e)RElWTv}%?VL;D~G*H!CL`D!Gmm-G`$h{TZHuqsBJ7hU}JzI=p=_fy7U)BGaeIJj z9aBToiVZOB)KU-a9_&)@A=1Nh#r?e6^~5ghbJ6?P8Z`S9a?Hym^BW|=3E)}IMn+OB z;)Zk?JsixpT+8;2bjQht4+i#GBs*jBaKppHfHT;W9T(ph{t1F#m0JStNU{QE*QD4p9N<~DvUn#$8_U){BGL+Ia**e}t(@>0uGVSC z5FLmK8iz~0KG$#f44?11F+sfg!q?jkraZ=m511;kx^0kdY@L1R)#8KSoE#VN7D`UH zo0&WO{XQk{7s7W>w#6r(NLs4Tyv`XB=91@qUk%~5=^7PmTPz?q8UYKN?oy(y)&eD> zt58zIFL9)!{eiEMT4LG?K>!)7BC@5w`&0f9sJr@jEEX`;*Ky4y6rA=>AIGxu2?>dz zEw-r5iTP|d*1xOZ{M{IBI>ch6(PAMNuC?q85Adl3x5I;=_V!D8b~8F3CI%8U?6$;@ zm#a|zI7>T~4TD^iHAq+YkisD#($p81J@;~p-!LUA_RQ%BISZ2BuKo$o^9o4-lmP(s z{G=Rp$e_Cn-JXm+JwYRtCvJTo2Au3ol^<7+Xzvls>^)X58P8vx(Cn_O8*rlZJ6xW= z*&W|-sgZp}bP)bF5rOG<%jzi(ZL|pg`$mo-q?`rm=IX+U*rLU>*Q#dhryDK_VOt<} z2X^Fa$%E-D2D#P&;`vFK-S0C5;tlGu2<~CA2CHWB)7vKLmxKqQ;56OMtnmAFe(vPZ z8I)d3qM$w;@%5U%tj5Y$-e0;l6v4Yj?{of%#m!{=@4*zg%Ng!j+-^R&`L_U`?%uF^ I*V9-21nMzD)c^nh literal 0 HcmV?d00001 diff --git a/redstone-link-addon/redstone_link_RP/texts/en_US.lang b/redstone-link-addon/redstone_link_RP/texts/en_US.lang new file mode 100644 index 0000000..a820b43 --- /dev/null +++ b/redstone-link-addon/redstone_link_RP/texts/en_US.lang @@ -0,0 +1,2 @@ +tile.silverlabs:redstone_link_tx.name=Redstone Link Transmitter +tile.silverlabs:redstone_link_rx.name=Redstone Link Receiver diff --git a/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png b/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png new file mode 100644 index 0000000000000000000000000000000000000000..e2cd18b094db86a135a9f91d6b5d6626297341cb GIT binary patch literal 586 zcmV-Q0=4~#P)K~|;h#FCUjWw0pXrZ)FMA0`N z{6bJ4L~7!0Hg0w|*{saT-CKO^Kyv5K+&MFIX4bfV<7SFu%Jx}oFdPo)>-Bmt41;($`EwixK@hKX<3WzR$<;{3`G1$ETP~CQPUSeBWnTllfH?zdS=epT}_Fu71Er zJ546(&aGhZsf{E_jsPl^3Q>CiTL>@SLh0ZkfyE~hF5W}3-=Z%32u~WI-|r)yUB<~@ zd-$t@hmAElL&XDamu|y)zJ|lO736zQXwMNvvG2Mr&#b(SI}Kd8+Mpgq5zlJ1s;kfd zg+hTBVVA}n&1k(ont8_hU@*|+>K4YzDa;zGC4qdqeV_pZd>T3x*$E90MG>6uFVH`K z4Ktl}6qX(#>8ztzEOH`}U~st!YwtdS*=1C}b&#s6&8%mc0?CpM+ixMh@&F@%KaujQ z#|TuVvYr1`kjv!=V1M0$dv1vid#B0F0yr9taH#CEV5#FU#`~=RGLcqhK5w#gPX)xv zXMWfq*Va2LR<^|1Fi(I zA~R!J&!ztzSr;kwS@OA$SYPA|BlL<`I zgb)Iw(FnF}BMd{RstUs}_)?zd@O__sBr3gTvw}palO(}r@&};jMD~PWaPnD}h2d~G z2e54$vMeKtB3wEA!H&1f7dblxQ^w7B0K@~lURq^;y!XvSJ`}Oqz1N1TvjYp(qMF*PriV{c?xT zG^LVl9LLP60!Wgi3ZPQj?RJrU_er5CP?k9>^`RVjRf3`@Ji_sK%tqP)lKpSLD*ytf z(Gf_a(Ll4=oHH;C1DdAcT6oCLXClG_ca7`5>vVAM>;>!$kC@rU=$Mny$SVd}mT?&* zYH;#L3v002ovPDHLkV1f+p6aD}I literal 0 HcmV?d00001 diff --git a/redstone-link-addon/redstone_link_RP/textures/terrain_texture.json b/redstone-link-addon/redstone_link_RP/textures/terrain_texture.json new file mode 100644 index 0000000..77784ab --- /dev/null +++ b/redstone-link-addon/redstone_link_RP/textures/terrain_texture.json @@ -0,0 +1,14 @@ +{ + "resource_pack_name": "redstone_link_RP", + "texture_name": "atlas.terrain", + "padding": 8, + "num_mip_levels": 4, + "texture_data": { + "redstone_link_tx": { + "textures": "textures/blocks/redstone_link_tx" + }, + "redstone_link_rx": { + "textures": "textures/blocks/redstone_link_rx" + } + } +} From db8931784e838f91d3fbc72517fdc0f8a35d7dd2 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Fri, 28 Aug 2026 11:40:16 +0100 Subject: [PATCH 2/4] feat(redstone-link): real block textures via build-textures.py, add build.sh The first textures were a motif floating on a flat slab. That hits the lesson already documented on mailbox_block(): an icon-like sprite "reads as a placeholder when applied to all 6 faces of a cube". Replaced with machine faces whose traces run out to the edges, so they connect across tile boundaries and a wall of them reads as continuous circuitry -- transmitter radiating out, receiver feeding in. Generated through scripts/build-textures.py using the existing palette and rect/px/save helpers rather than an ad-hoc script, so they regenerate with everything else. Running the pipeline also rewrites the other five textures, but pixel-identically -- that churn is only a newer Pillow re-encoding, so those files are left alone. RP header bumped to 1.0.1 (with the BP dependency to match) because clients cache packs by uuid+version and would otherwise keep serving the old art. The lobby world pin was updated to match; a mismatch silently drops the pack from the stack. Adds build.sh to package the .mcaddon, mirroring addon/build.sh. It omits that script's `-x` exclude: these packs have no dotfiles, and dropping it keeps the script working with the 7-Zip `zip` shim on the workstation as well as Info-ZIP on the CI runner. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC --- redstone-link-addon/build.sh | 38 ++++++++ .../redstone_link_BP/manifest.json | 30 +++++- .../redstone_link_RP/manifest.json | 2 +- .../textures/blocks/redstone_link_rx.png | Bin 586 -> 291 bytes .../textures/blocks/redstone_link_tx.png | Bin 595 -> 275 bytes scripts/build-textures.py | 87 ++++++++++++++++++ 6 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 redstone-link-addon/build.sh diff --git a/redstone-link-addon/build.sh b/redstone-link-addon/build.sh new file mode 100644 index 0000000..56830b5 --- /dev/null +++ b/redstone-link-addon/build.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="$SCRIPT_DIR/build" + +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +# Each pack is zipped from INSIDE its own directory so that manifest.json sits +# at the archive root. An extra nesting level makes Minecraft reject the import. + +# No -x exclude here, unlike addon/build.sh: these pack dirs contain no +# dotfiles, and dropping it keeps the script working with the 7-Zip `zip` +# shim on the Windows workstation as well as Info-ZIP on the CI runner. + +echo "Packaging Behavior Pack..." +cd "$SCRIPT_DIR/redstone_link_BP" +zip -rq "$BUILD_DIR/redstone_link_BP.mcpack" . + +echo "Packaging Resource Pack..." +cd "$SCRIPT_DIR/redstone_link_RP" +zip -rq "$BUILD_DIR/redstone_link_RP.mcpack" . + +echo "Creating .mcaddon bundle..." +cd "$BUILD_DIR" +zip -rq "$BUILD_DIR/redstone_link.mcaddon" redstone_link_BP.mcpack redstone_link_RP.mcpack + +echo "" +echo "Build complete!" +echo " Output: $BUILD_DIR/redstone_link.mcaddon" +echo "" +echo "To install:" +echo " - Double-click the .mcaddon file on Windows to auto-import" +echo " - Or copy packs to com.mojang/development_behavior_packs/ and development_resource_packs/" +echo "" +echo "Note: import BOTH packs. The behavior pack alone gives you working" +echo "logic with missing textures and untranslated block names." diff --git a/redstone-link-addon/redstone_link_BP/manifest.json b/redstone-link-addon/redstone_link_BP/manifest.json index cf8795d..3d067cc 100644 --- a/redstone-link-addon/redstone_link_BP/manifest.json +++ b/redstone-link-addon/redstone_link_BP/manifest.json @@ -4,20 +4,36 @@ "name": "Redstone Link", "description": "Wireless redstone: pair a transmitter and a receiver by channel name, no wire in between", "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b001", - "version": [1, 0, 0], - "min_engine_version": [1, 21, 0] + "version": [ + 1, + 0, + 0 + ], + "min_engine_version": [ + 1, + 21, + 0 + ] }, "modules": [ { "type": "data", "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b002", - "version": [1, 0, 0] + "version": [ + 1, + 0, + 0 + ] }, { "type": "script", "language": "javascript", "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b003", - "version": [1, 0, 0], + "version": [ + 1, + 0, + 0 + ], "entry": "scripts/main.js" } ], @@ -32,7 +48,11 @@ }, { "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b004", - "version": [1, 0, 0] + "version": [ + 1, + 0, + 1 + ] } ] } diff --git a/redstone-link-addon/redstone_link_RP/manifest.json b/redstone-link-addon/redstone_link_RP/manifest.json index bed6a34..93add19 100644 --- a/redstone-link-addon/redstone_link_RP/manifest.json +++ b/redstone-link-addon/redstone_link_RP/manifest.json @@ -4,7 +4,7 @@ "name": "Redstone Link Resources", "description": "Textures and lang for the silverlabs:redstone_link_tx / _rx blocks", "uuid": "e4f1a7c2-6b95-4d38-a7e0-3c81d5f2b004", - "version": [1, 0, 0], + "version": [1, 0, 1], "min_engine_version": [1, 21, 0] }, "modules": [ diff --git a/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png b/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png index e2cd18b094db86a135a9f91d6b5d6626297341cb..d070425cb398d050201136492c8f218260213028 100644 GIT binary patch delta 265 zcmV+k0rvjN1fv3wBYyzuNklX8T2}xakwgeXkcFyH{_ig6jr0)LJwXlSE@u60`(rX2c+ zVHn685L1z20UH2m&avair-Xq8lt^Il3^y6c8)XwPHNgTJB^J;DEakYx+c@ZADhg0?Lvw018zUAcsR7NeloD23uQ$Yq$RZ P0000K~|;h#FCUjWw0pXrZ)FMA0`N{6bJ4L~7!0Hg0w|*{saT-CKO^Kyv5K+&MFI zX4bfV<7SFu%Jx}oFdPo)>woonFbsow9LGFGO!;#h z2SE_P_kGy5&HJA-3p$ezpy0MK9*+@*A^oCjfHX~sC>gWB6F+yS!M@MO@%$?9>Bpy- zOeRdI0es(QS(Euy6u&$}KA*>M;jVtbM>|a>>CUZS@TrX?Nsa(2l?qXN09yzz-a_f% zA%Vpw5-#3DvVY&AF8l~j8ld0rBc5Hx$zOZ;tAdA(H9AAZ18tXX!+O4k!?_jYdrxT3 z5k;}@x-QSGyp1~zT)5hx9z_w)YPG7X&;W%(ffr$y#vILPy*`?G#`<6|(B$eC#>y$o z8mc9Me7k+10R(&+Iu+Ro4G={UobNBtKYtA~opltJ9)BU}tfN>gaw3voaJdO>Ur zWmLa)kgBT9tY?`5$&wA*Zy~<&03(1uk@Bm@2vnuAo&Qyk%jF1Qf8Bz6Zixg;nkyd6tZ?bex1;ol{e%K({UL2!duWK?1AmxZt_FUFk##;OF zl9|ipGAlgKLz1QhmbI60XOl8coj!Z)&shNe0l{VQpG(hTQ~&?~07*qoM6N<$g1Zb1 A2mk;8 diff --git a/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_tx.png b/redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_tx.png index 840156d36c6b03ffd24c469e5cece06f5b6299f6..a112e87520207fe81c680a77f742fcd7c3616384 100644 GIT binary patch delta 248 zcmVq7-QD-i40kHE)moWQUg+pVSt3BG{ZC%J-mj1 za`uadCy5%zsd5V+9kN7#J2# zVqiG;1{@pM01Jq~CJ8naq?rI@X3d8J{62 yNg1DJWJ7Q!WnuxcS{6{2gaJ^fVguO3VPXJTI&z3rBgmKl0000u530@DPLBYy%KNklVa2LR<^|1Fi(IA~R!J&!ztzSr;kwS@OA$SYPA|BlL<`Igb)Iw(FnF}BMd{Rs(%W@F!)lQ=kR@>eIzQq zX0w7ssgoqZX7UH1=S22|U~uwTmWAPPI0vw88?r1TiXvP&{K1a5%NIF21yjb&cmTu$ zyk1&mf4yFx1CV4{hHliy3wwoWJ{tUDJB|ZZx9t_^x{j;;1H5!PTo#E+uOJ8@Nm5}@ zRXp^2xU=9gXn(8KV!$20hpn+yWlz)e-x7oncxRfBgus?&;c3(_7^EJnTQw7PUB?IK z5_Pg-AahKba=ipHolc=B3Om=I?_&LOhtD*nl5HHv%&G!NlB5ctQrhiyk$v|`p(#+7 zIV<&{9C=lOq9{DV@p#Ne+5wXNZ@()50;bUsNTbm}vwzu~GcXJTnx^4ec*xFYBEkcA zjqASaba3zN1?&uunAyhYn3K`SD+XDXaTz4!*lwP|SQpd2N@%R`;lRh3!N zepCRIi8L-#HagSlzH!(1n*u%0gO=yKKxOw!ExF6*PyIU+z+a_Q0Q>YH;#L3v002ov JPDHLkV1lg)63_qu diff --git a/scripts/build-textures.py b/scripts/build-textures.py index c30a64c..35455f2 100644 --- a/scripts/build-textures.py +++ b/scripts/build-textures.py @@ -397,12 +397,99 @@ def portal_field() -> Image.Image: return img +# ── Redstone Link transmitter / receiver ─────────────────── +# Machine faces, not icons. Following the mailbox lesson: the motif has to +# reach the edges or a cube of it reads as a placeholder. Both share a gunmetal +# plate with corner rivets and a beveled frame; the transmitter carries redstone +# traces radiating OUT to all four edges, the receiver carries traces running IN +# to a collector dish. Same silhouette, mirrored behaviour, different accent. + +LINK_PLATE = (62, 66, 74) +LINK_PLATE_HL = (92, 97, 107) +LINK_PLATE_SH = (38, 41, 47) +LINK_RIVET = (140, 146, 158) + +TX_TRACE = (168, 30, 28) +TX_TRACE_HOT = REDSTONE_BRIGHT +TX_CORE = (255, 140, 110) + +RX_TRACE = (34, 96, 150) +RX_TRACE_HOT = (92, 178, 240) +RX_CORE = (200, 240, 255) + + +def _link_plate() -> Image.Image: + """Shared gunmetal base: beveled frame + corner rivets, fully opaque.""" + img = Image.new("RGBA", (16, 16), LINK_PLATE) + # Bevel: lit top/left, shadowed bottom/right + rect(img, 0, 0, 15, 0, LINK_PLATE_HL) + rect(img, 0, 0, 0, 15, LINK_PLATE_HL) + rect(img, 0, 15, 15, 15, LINK_PLATE_SH) + rect(img, 15, 0, 15, 15, LINK_PLATE_SH) + # Corner rivets + for cx, cy in ((2, 2), (13, 2), (2, 13), (13, 13)): + px(img, cx, cy, LINK_RIVET) + px(img, cx, cy + 1, LINK_PLATE_SH) + return img + + +def redstone_link_tx() -> Image.Image: + img = _link_plate() + # Traces running from the core out to all four edges: this is what makes a + # cube of it read as wiring rather than a centred sprite. + rect(img, 7, 0, 8, 15, TX_TRACE) + rect(img, 0, 7, 15, 8, TX_TRACE) + # Diagonal spurs toward the corners + for i in range(3, 7): + px(img, i, i, TX_TRACE) + px(img, 15 - i, i, TX_TRACE) + px(img, i, 15 - i, TX_TRACE) + px(img, 15 - i, 15 - i, TX_TRACE) + # Emitter core, hot centre + rect(img, 6, 6, 9, 9, TX_TRACE_HOT) + rect(img, 7, 7, 8, 8, TX_CORE) + px(img, 7, 7, (255, 235, 220)) + # Trace highlight so the cross reads as raised + rect(img, 7, 1, 7, 5, TX_TRACE_HOT) + rect(img, 1, 7, 5, 7, TX_TRACE_HOT) + return img + + +def redstone_link_rx() -> Image.Image: + img = _link_plate() + # Collector rings, broken at the axes so the in-running traces read clearly + for x in range(3, 13): + px(img, x, 3, RX_TRACE) + px(img, x, 12, RX_TRACE) + for y in range(3, 13): + px(img, 3, y, RX_TRACE) + px(img, 12, y, RX_TRACE) + for x in range(5, 11): + px(img, x, 5, RX_TRACE_HOT) + px(img, x, 10, RX_TRACE_HOT) + for y in range(5, 11): + px(img, 5, y, RX_TRACE_HOT) + px(img, 10, y, RX_TRACE_HOT) + # Feed lines from the edges into the dish + rect(img, 7, 0, 8, 2, RX_TRACE) + rect(img, 7, 13, 8, 15, RX_TRACE) + rect(img, 0, 7, 2, 8, RX_TRACE) + rect(img, 13, 7, 15, 8, RX_TRACE) + # Collector core + rect(img, 7, 7, 8, 8, RX_TRACE_HOT) + px(img, 7, 7, RX_CORE) + px(img, 8, 8, RX_CORE) + return img + + def main() -> None: save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png") save(post_office_block(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png") save(mailbox_block(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png") save(tent_canvas(), "camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png") save(portal_field(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png") + save(redstone_link_tx(), "redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_tx.png") + save(redstone_link_rx(), "redstone-link-addon/redstone_link_RP/textures/blocks/redstone_link_rx.png") if __name__ == "__main__": From 1e82825f8fad484bed5ff5bb0d37d7c2c7f5bb9b Mon Sep 17 00:00:00 2001 From: sysadmin Date: Fri, 28 Aug 2026 12:22:22 +0100 Subject: [PATCH 3/4] fix(redstone-link): add minecraft:geometry so material_instances applies The blocks rendered as an untextured default. Root cause: with no minecraft:geometry component, Bedrock renders the block through the legacy blocks.json texture path instead of material_instances. blocks.json here only sets "sound", with no "textures", so the client substituted a default. This produces no error in the content log -- both paths are valid, they just resolve differently -- which is why the pack looked correct everywhere: files installed byte-perfect, pack activated, terrain_texture.json valid, and a clean world with only this addon still showed the wrong texture. Adding "minecraft:geometry": "minecraft:geometry.full_block" fixes it, verified in-game. BP bumped to 1.0.1 so clients cannot reuse the cached copy. The lobby world pin is updated to match; a mismatch silently drops the pack from the stack. Note: 7 of the repo's other custom blocks have the same defect -- home_sign, smart_crafting_table, tent_canvas, and all four lobby portal blocks. Most blocks here DO set geometry, so those seven look like oversights rather than intent. Not fixed in this commit; they touch every live world. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC --- .../redstone_link_BP/blocks/redstone_link_rx.json | 1 + .../redstone_link_BP/blocks/redstone_link_tx.json | 1 + redstone-link-addon/redstone_link_BP/manifest.json | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json index 14f461a..57235cc 100644 --- a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json +++ b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_rx.json @@ -16,6 +16,7 @@ "explosion_resistance": 20.0 }, "minecraft:map_color": "#1A4C8C", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "redstone_link_rx", diff --git a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json index bd20b30..80b4e17 100644 --- a/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json +++ b/redstone-link-addon/redstone_link_BP/blocks/redstone_link_tx.json @@ -16,6 +16,7 @@ "explosion_resistance": 20.0 }, "minecraft:map_color": "#8C1A1A", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "redstone_link_tx", diff --git a/redstone-link-addon/redstone_link_BP/manifest.json b/redstone-link-addon/redstone_link_BP/manifest.json index 3d067cc..3ced366 100644 --- a/redstone-link-addon/redstone_link_BP/manifest.json +++ b/redstone-link-addon/redstone_link_BP/manifest.json @@ -7,7 +7,7 @@ "version": [ 1, 0, - 0 + 1 ], "min_engine_version": [ 1, @@ -22,7 +22,7 @@ "version": [ 1, 0, - 0 + 1 ] }, { @@ -32,7 +32,7 @@ "version": [ 1, 0, - 0 + 1 ], "entry": "scripts/main.js" } From 0fb2b27d7c1c2917a7658c6da6c1506179e4e72b Mon Sep 17 00:00:00 2001 From: sysadmin Date: Fri, 28 Aug 2026 12:27:59 +0100 Subject: [PATCH 4/4] fix(addons): add minecraft:geometry to the 7 untextured custom blocks Without a minecraft:geometry component Bedrock renders a custom block through the legacy blocks.json texture path instead of material_instances. These packs' blocks.json files only set "sound", with no "textures", so the client substituted a default and the blocks rendered untextured. Diagnosed and verified in-game on redstone-link; this applies the same one-line fix to the rest. Confirmed in the field for the portal blocks: portal_jamie, portal_lyla and portal_mya all fell back to the same default, so players had to guess which portal led to which world. They reference vanilla textures (emerald_block, amethyst_block, prismarine_bricks), so with geometry set they render green, purple and teal respectively -- one per child. Also fixes home_sign, smart_crafting_table, tent_canvas and portal_frame. full_block is correct for all seven: every custom block model in these packs (hammock_slab, tent_panel_l/r, portal_field) is already referenced by its own block, and no orphan model exists for any of these, so they were always meant to be plain cubes. No pack versions bumped deliberately. Block definitions live in the behavior pack and reach clients through the network block palette rather than a cached resource pack, so a bump is unnecessary -- and bumping would require updating the matching pin in all four world_behavior_packs.json files, where a mismatch silently drops the pack from the stack. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC --- .../camping_supplies_BP/blocks/tent_canvas.json | 1 + home-sign-addon/home_sign_BP/blocks/home_sign.json | 1 + lobby-addon/lobby_transfer_BP/blocks/portal_frame.json | 1 + lobby-addon/lobby_transfer_BP/blocks/portal_jamie.json | 1 + lobby-addon/lobby_transfer_BP/blocks/portal_lyla.json | 1 + lobby-addon/lobby_transfer_BP/blocks/portal_mya.json | 1 + .../smart_crafting_BP/blocks/smart_crafting_table.json | 1 + 7 files changed, 7 insertions(+) diff --git a/camping-supplies-addon/camping_supplies_BP/blocks/tent_canvas.json b/camping-supplies-addon/camping_supplies_BP/blocks/tent_canvas.json index 12998ba..086526d 100644 --- a/camping-supplies-addon/camping_supplies_BP/blocks/tent_canvas.json +++ b/camping-supplies-addon/camping_supplies_BP/blocks/tent_canvas.json @@ -12,6 +12,7 @@ "explosion_resistance": 1.0 }, "minecraft:map_color": "#547A4E", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "tent_canvas", diff --git a/home-sign-addon/home_sign_BP/blocks/home_sign.json b/home-sign-addon/home_sign_BP/blocks/home_sign.json index 3c59f85..df7eca2 100644 --- a/home-sign-addon/home_sign_BP/blocks/home_sign.json +++ b/home-sign-addon/home_sign_BP/blocks/home_sign.json @@ -16,6 +16,7 @@ "explosion_resistance": 200.0 }, "minecraft:map_color": "#C0703A", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "home_sign", diff --git a/lobby-addon/lobby_transfer_BP/blocks/portal_frame.json b/lobby-addon/lobby_transfer_BP/blocks/portal_frame.json index b37beda..9127cac 100644 --- a/lobby-addon/lobby_transfer_BP/blocks/portal_frame.json +++ b/lobby-addon/lobby_transfer_BP/blocks/portal_frame.json @@ -17,6 +17,7 @@ }, "minecraft:light_emission": 10, "minecraft:map_color": "#5C6D74", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "portal_frame", diff --git a/lobby-addon/lobby_transfer_BP/blocks/portal_jamie.json b/lobby-addon/lobby_transfer_BP/blocks/portal_jamie.json index e7075a9..b7797c4 100644 --- a/lobby-addon/lobby_transfer_BP/blocks/portal_jamie.json +++ b/lobby-addon/lobby_transfer_BP/blocks/portal_jamie.json @@ -17,6 +17,7 @@ }, "minecraft:light_emission": 10, "minecraft:map_color": "#2D9C2D", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "portal_jamie", diff --git a/lobby-addon/lobby_transfer_BP/blocks/portal_lyla.json b/lobby-addon/lobby_transfer_BP/blocks/portal_lyla.json index 9b995ad..2929a4d 100644 --- a/lobby-addon/lobby_transfer_BP/blocks/portal_lyla.json +++ b/lobby-addon/lobby_transfer_BP/blocks/portal_lyla.json @@ -17,6 +17,7 @@ }, "minecraft:light_emission": 10, "minecraft:map_color": "#9B59B6", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "portal_lyla", diff --git a/lobby-addon/lobby_transfer_BP/blocks/portal_mya.json b/lobby-addon/lobby_transfer_BP/blocks/portal_mya.json index eb80320..8ad14a0 100644 --- a/lobby-addon/lobby_transfer_BP/blocks/portal_mya.json +++ b/lobby-addon/lobby_transfer_BP/blocks/portal_mya.json @@ -17,6 +17,7 @@ }, "minecraft:light_emission": 10, "minecraft:map_color": "#4FC1E9", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "portal_mya", diff --git a/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json b/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json index 448bcea..66e075f 100644 --- a/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json +++ b/smart-crafting-addon/smart_crafting_BP/blocks/smart_crafting_table.json @@ -16,6 +16,7 @@ "explosion_resistance": 20.0 }, "minecraft:map_color": "#D4AF37", + "minecraft:geometry": "minecraft:geometry.full_block", "minecraft:material_instances": { "*": { "texture": "smart_crafting_table",