From af9d37462c9ef49933659e57bd85153cf45fc267 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Mon, 27 Apr 2026 22:00:31 +0100 Subject: [PATCH] feat(hub-return): subtitle nav HUD, share waypoints, !nav fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the directional waypoint HUD off the action bar (which fights mount saddle/jump UI for screen space) into the title/subtitle slot — large rotating arrow + distance up top, label underneath, refreshed every 5 ticks so it stays pinned. Active waypoint now persists across container restarts via per-player dynamic property instead of an in-memory Map. New: - !share command + 📤 button on the compass form: pick a waypoint, pick a recipient, send them an Accept/Decline prompt; copies into their list as "Label (from sender)" with capacity check. - !nav chat fallback: list waypoints with distances, switch active with !nav , !nav off to clear. - hub_return_transfer_RP scaffold for future asset overrides. docker-compose: mount the new RP on jamie/lyla/mya. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 13 + .../hub_return_transfer_BP/scripts/main.js | 244 ++++++++++++++++-- .../hub_return_transfer_RP/manifest.json | 29 +++ 3 files changed, 258 insertions(+), 28 deletions(-) create mode 100644 hub-return-addon/hub_return_transfer_RP/manifest.json diff --git a/docker-compose.yml b/docker-compose.yml index 4ec3501..04bda63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,7 @@ services: volumes: - jamie-data:/data - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP + - ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP - ./private-chest-addon/private_chest_BP:/data/behavior_packs/private_chest_BP - ./private-chest-addon/private_chest_RP:/data/resource_packs/private_chest_RP - ./smart-crafting-addon/smart_crafting_BP:/data/behavior_packs/smart_crafting_BP @@ -76,6 +77,10 @@ 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 + - ./trees-features-addon/trees_features_BP:/data/behavior_packs/trees_features_BP + - ./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 restart: unless-stopped mem_limit: 1500m memswap_limit: 2500m @@ -94,6 +99,7 @@ services: volumes: - lyla-data:/data - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP + - ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP - ./addon/heyhe_pet_BP:/data/behavior_packs/heyhe_pet_BP @@ -135,6 +141,7 @@ services: volumes: - mya-data:/data - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP + - ./hub-return-addon/hub_return_transfer_RP:/data/resource_packs/hub_return_transfer_RP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP - ./addon/heyhe_pet_BP:/data/behavior_packs/heyhe_pet_BP @@ -157,7 +164,13 @@ 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 + - ./naturalist-lite-addon/naturalist_lite_BP:/data/behavior_packs/naturalist_lite_BP + - ./naturalist-lite-addon/naturalist_lite_RP:/data/resource_packs/naturalist_lite_RP - ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json + - ./trees-features-addon/trees_features_BP:/data/behavior_packs/trees_features_BP + - ./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 restart: unless-stopped mem_limit: 1500m memswap_limit: 2500m diff --git a/hub-return-addon/hub_return_transfer_BP/scripts/main.js b/hub-return-addon/hub_return_transfer_BP/scripts/main.js index 5ab7f51..15ffb8d 100644 --- a/hub-return-addon/hub_return_transfer_BP/scripts/main.js +++ b/hub-return-addon/hub_return_transfer_BP/scripts/main.js @@ -17,6 +17,33 @@ const ARRIVAL_RADIUS = 3; const HUD_TICK_INTERVAL = 5; const ARROWS = ["\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199", "\u2190", "\u2196"]; +// Navigation HUD lives in the title/subtitle slot so it doesn't fight the +// vanilla mount UI for the action bar. The directional content goes in the +// TITLE position (large, central) with the waypoint label in the subtitle as +// a secondary line. Empty-string title gets suppressed by some Bedrock client +// versions, so we always pass a non-empty title — that's what causes the +// arrival message to render but a setActionBar-replacement subtitle not to. +function showNav(player, mainLine, subLine) { + try { + player.onScreenDisplay.setTitle(mainLine, { + subtitle: subLine ?? "", + fadeInDuration: 0, + stayDuration: HUD_TICK_INTERVAL * 4, + fadeOutDuration: 0, + }); + } catch (_) {} +} +function clearNav(player) { + try { + player.onScreenDisplay.setTitle(" ", { + subtitle: "", + fadeInDuration: 0, + stayDuration: 1, + fadeOutDuration: 0, + }); + } catch (_) {} +} + const recentTransfers = new Map(); const spawnTimes = new Map(); @@ -24,8 +51,22 @@ const spawnTimes = new Map(); // Persisted: markers per player { [ownerId]: [ {x,y,z,dim,label,key}, ... ] } let markers = {}; -// Session-only: which marker each player is actively guiding toward -const active = new Map(); +// Per-player active waypoint index, persisted via player dynamic property so +// it survives container restarts and reconnects. +const ACTIVE_PROP = "hub_active_waypoint_v1"; +function getActiveIndex(player) { + try { + const v = player.getDynamicProperty(ACTIVE_PROP); + return typeof v === "number" ? v : null; + } catch (_) { return null; } +} +function setActiveIndex(player, idx) { + try { + if (idx == null) player.setDynamicProperty(ACTIVE_PROP, undefined); + else player.setDynamicProperty(ACTIVE_PROP, idx); + } catch (_) {} +} +function clearActiveIndex(player) { setActiveIndex(player, null); } function keyOf(loc, dimensionId) { return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`; @@ -67,10 +108,16 @@ function findMarkerByKey(key) { function removeMarker(ownerId, index) { const list = getMarkers(ownerId); list.splice(index, 1); - const activeIdx = active.get(ownerId); - if (activeIdx != null) { - if (activeIdx === index) active.delete(ownerId); - else if (activeIdx > index) active.set(ownerId, activeIdx - 1); + // Active index is stored on the owner's player dynamic property; update only + // if they're online (offline owner will revalidate their own index next tick). + for (const p of world.getAllPlayers()) { + if (p.id !== ownerId) continue; + const activeIdx = getActiveIndex(p); + if (activeIdx != null) { + if (activeIdx === index) clearActiveIndex(p); + else if (activeIdx > index) setActiveIndex(p, activeIdx - 1); + } + break; } saveWaypoints(); } @@ -149,11 +196,8 @@ world.afterEvents.playerSpawn.subscribe((event) => { }, 20); }); -world.afterEvents.playerLeave.subscribe((event) => { - // Clear session HUD state for whoever left (keyed by id) - // event.playerId exists; event.playerName for compat - if (event.playerId) active.delete(event.playerId); -}); +// Active waypoint is persisted as a player dynamic property, so playerLeave +// does not need to clear any in-memory session state. // ─── Compass Menu ─────────────────────────────────────────────── @@ -183,7 +227,7 @@ async function openCompassMenu(player) { actions.push({ kind: "select", index: i }); } - const activeIdx = active.get(player.id); + const activeIdx = getActiveIndex(player); if (activeIdx != null && list[activeIdx]) { form.button(`§c\u274C Clear active waypoint`); actions.push({ kind: "clear" }); @@ -192,6 +236,9 @@ async function openCompassMenu(player) { if (list.length > 0) { form.button("\uD83D\uDDD1 Delete a waypoint…"); actions.push({ kind: "delete_menu" }); + + form.button("\uD83D\uDCE4 Share with another player…"); + actions.push({ kind: "share" }); } form.button("\uD83E\uDDED Get a marker block"); @@ -212,14 +259,14 @@ async function openCompassMenu(player) { return; } if (a.kind === "select") { - active.set(player.id, a.index); + setActiveIndex(player, a.index); const m = list[a.index]; - player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f.`); + player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow appears at the top of your screen)`); return; } if (a.kind === "clear") { - active.delete(player.id); - player.onScreenDisplay.setActionBar(""); + clearActiveIndex(player); + clearNav(player); player.sendMessage("§b[Waypoints] §fActive waypoint cleared."); return; } @@ -227,6 +274,10 @@ async function openCompassMenu(player) { await openDeleteMenu(player); return; } + if (a.kind === "share") { + await openShareMenu(player); + return; + } if (a.kind === "give_marker") { giveMarkerBlock(player); return; @@ -286,6 +337,102 @@ async function openDeleteMenu(player) { player.sendMessage(`§b[Waypoints] §fDeleted §d${chosen.label}§f.`); } +// ─── Share Waypoint ───────────────────────────────────────────── +// Pick one of your waypoints, pick an online player in this world, send +// them a confirm prompt. On accept, the waypoint is copied into the +// recipient's list with a `(from )` suffix so it's clearly +// attributed. Cross-world shares are blocked here — a player on the lyla +// server can't be reached from jamie. Same dimension is preferred but +// cross-dim is allowed; the HUD already labels "different dimension". + +async function openShareMenu(sender) { + const list = getMarkers(sender.id); + if (list.length === 0) { + sender.sendMessage("§7[Share] You have no waypoints to share."); + return; + } + const others = world.getAllPlayers().filter((p) => p.id !== sender.id); + if (others.length === 0) { + sender.sendMessage("§7[Share] No other players are online in this world."); + return; + } + + const wpForm = new ActionFormData() + .title("Share a Waypoint") + .body("§7Pick which waypoint to share:"); + for (const m of list) { + wpForm.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`); + } + wpForm.button("§7Cancel"); + + let wpRes; + try { wpRes = await wpForm.show(sender); } catch (_) { return; } + if (wpRes.canceled || wpRes.selection === undefined) return; + if (wpRes.selection >= list.length) return; + const marker = list[wpRes.selection]; + + const recipForm = new ActionFormData() + .title(`Share "${marker.label}"`) + .body("§7Who should receive this waypoint?"); + for (const p of others) recipForm.button(`§f${p.name}`); + recipForm.button("§7Cancel"); + + let rRes; + try { rRes = await recipForm.show(sender); } catch (_) { return; } + if (rRes.canceled || rRes.selection === undefined) return; + if (rRes.selection >= others.length) return; + const recipient = others[rRes.selection]; + + // Receiver capacity check before bothering them with a prompt + const recList = getMarkers(recipient.id); + if (recList.length >= MAX_MARKERS_PER_PLAYER) { + sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`); + return; + } + + sender.sendMessage(`§b[Share] §fSent §d${marker.label}§f to §e${recipient.name}§f — waiting for them to accept…`); + + const offer = new MessageFormData() + .title("Waypoint Shared") + .body(`§e${sender.name}§r wants to share a waypoint with you:\n\n§f${marker.label}\n§7${marker.x}, ${marker.y}, ${marker.z}\n\nAdd to your compass?`) + .button1("§aAccept") + .button2("§cDecline"); + + let oRes; + try { oRes = await offer.show(recipient); } catch (_) { + sender.sendMessage(`§c[Share] §f${recipient.name} couldn't be reached.`); + return; + } + if (oRes.canceled || oRes.selection !== 0) { + sender.sendMessage(`§7[Share] §f${recipient.name} declined.`); + recipient.sendMessage(`§7[Share] §fDeclined waypoint from §e${sender.name}§f.`); + return; + } + + // Re-check capacity after the await — markers may have changed + if (recList.length >= MAX_MARKERS_PER_PLAYER) { + sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`); + recipient.sendMessage(`§c[Share] §fYour waypoint slots are full — couldn't accept §d${marker.label}§f.`); + return; + } + + const sharedLabel = `${marker.label} (from ${sender.name})`.slice(0, 40); + // Use a key that won't collide with the receiver's own lodestones + const sharedKey = `shared:${sender.id}:${marker.key}`; + recList.push({ + x: marker.x, + y: marker.y, + z: marker.z, + dim: marker.dim, + label: sharedLabel, + key: sharedKey, + }); + saveWaypoints(); + + sender.sendMessage(`§a[Share] §f${recipient.name} accepted §d${marker.label}§f.`); + recipient.sendMessage(`§a[Share] §fAdded §d${sharedLabel}§f. Type §e!nav§f to find it.`); +} + // ─── Placement → Label Prompt ─────────────────────────────────── world.afterEvents.playerPlaceBlock.subscribe((event) => { @@ -402,17 +549,17 @@ function distanceXZ(a, b) { system.runInterval(() => { for (const player of world.getAllPlayers()) { - const idx = active.get(player.id); + const idx = getActiveIndex(player); if (idx == null) continue; const list = markers[player.id]; const m = list && list[idx]; if (!m) { - active.delete(player.id); + clearActiveIndex(player); continue; } if (m.dim !== player.dimension.id) { - player.onScreenDisplay.setActionBar(`§b\u27A4 §f${m.label} §7• §cdifferent dimension`); + showNav(player, `§c⚠ Different Dimension`, `§7${m.label}`); continue; } @@ -422,10 +569,9 @@ system.runInterval(() => { const distXZ = Math.sqrt(dx * dx + dz * dz); if (distXZ <= ARRIVAL_RADIUS) { - active.delete(player.id); + clearActiveIndex(player); try { player.onScreenDisplay.setTitle(`§aArrived`, { subtitle: `§f${m.label}`, fadeInDuration: 5, stayDuration: 30, fadeOutDuration: 10 }); - player.onScreenDisplay.setActionBar(""); } catch (_) {} continue; } @@ -439,11 +585,7 @@ system.runInterval(() => { const bucket = ((Math.round(relative / 45) % 8) + 8) % 8; const arrow = ARROWS[bucket]; - try { - player.onScreenDisplay.setActionBar( - `§b\u27A4 §f${m.label} §7• §f${Math.round(distXZ)}m §8[§a${arrow}§8]` - ); - } catch (_) {} + showNav(player, `§a${arrow} §f${Math.round(distXZ)}m`, `§7${m.label}`); } }, HUD_TICK_INTERVAL); @@ -515,6 +657,52 @@ function handleChatCommand(player, msg) { return true; } + if (msg === "!share") { + system.run(() => openShareMenu(player)); + return true; + } + + if (msg === "!nav" || msg.startsWith("!nav ")) { + const list = getMarkers(player.id); + const arg = msg.slice(4).trim(); + if (arg === "off" || arg === "clear") { + clearActiveIndex(player); + clearNav(player); + player.sendMessage("§b[Waypoints] §fNavigation cleared."); + return true; + } + if (arg) { + const n = Number.parseInt(arg, 10); + if (Number.isFinite(n) && n >= 1 && n <= list.length) { + setActiveIndex(player, n - 1); + const m = list[n - 1]; + player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow at top of screen)`); + return true; + } + player.sendMessage(`§c[Waypoints] §fNo waypoint #${arg} — type §e!nav§f to list yours.`); + return true; + } + if (list.length === 0) { + player.sendMessage("§7[Waypoints] No waypoints yet — place a §dlodestone§7 to set one."); + return true; + } + const idx = getActiveIndex(player); + const cur = idx != null ? list[idx] : null; + if (cur) { + const dist = Math.round(distanceXZ(player.location, cur)); + player.sendMessage(`§b[Waypoints] §fActive: §d${cur.label}§f (${dist}m). Type §e!nav off§f to clear, or §e!nav §f to switch.`); + } else { + player.sendMessage("§b[Waypoints] §fNo active waypoint. Pick one:"); + } + for (let i = 0; i < list.length; i++) { + const m = list[i]; + const here = m.dim === player.dimension.id; + const tag = here ? `§7${Math.round(distanceXZ(player.location, m))}m` : "§8(other dim)"; + player.sendMessage(` §e${i + 1}§7. §f${m.label} ${tag}`); + } + return true; + } + if (msg === "!clearwaypoints") { system.run(async () => { const confirm = new MessageFormData() @@ -526,7 +714,7 @@ function handleChatCommand(player, msg) { try { res = await confirm.show(player); } catch (_) { return; } if (res.canceled || res.selection !== 0) return; markers[player.id] = []; - active.delete(player.id); + clearActiveIndex(player); saveWaypoints(); player.sendMessage("§b[Waypoints] §fAll your waypoints were cleared."); }); @@ -606,5 +794,5 @@ system.runTimeout(() => { system.run(() => { loadWaypoints(); - world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint; right-click your compass to navigate."); + world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint, then right-click your compass and pick one — the §anavigation arrow§f shows at the §atop§f of your screen. (Type §e!nav§f for chat fallback.)"); }); diff --git a/hub-return-addon/hub_return_transfer_RP/manifest.json b/hub-return-addon/hub_return_transfer_RP/manifest.json new file mode 100644 index 0000000..3909332 --- /dev/null +++ b/hub-return-addon/hub_return_transfer_RP/manifest.json @@ -0,0 +1,29 @@ +{ + "format_version": 2, + "header": { + "name": "Hub Return Resources", + "description": "Recovery compass icon override + future hub-return assets", + "uuid": "b1f7e2a4-3c5d-49b8-9d22-6a4f0c87e511", + "version": [ + 1, + 0, + 1 + ], + "min_engine_version": [ + 1, + 21, + 0 + ] + }, + "modules": [ + { + "type": "resources", + "uuid": "c4a13e85-2f96-4d1a-b772-9e0f3b4d6c21", + "version": [ + 1, + 0, + 1 + ] + } + ] +}