diff --git a/CLAUDE.md b/CLAUDE.md index c7a3605..e1d8144 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,16 @@ Four Bedrock servers on `10.0.0.247`, connected via native `/transfer` (no proxy Players connect to `10.0.0.247:19132` (lobby) and use portal zones to transfer to child worlds. Child worlds have a hub-return addon (compass, `!hub`/`!lobby` commands, portal zone) to transfer back. +### Transfer Mechanism + +Transfers use `@minecraft/server-admin` beta API (`transferPlayer()` function). This requires: +- **Beta APIs experiment** enabled in each world's `level.dat` (`gametest` experiment flag) +- `@minecraft/server-admin` v1.0.0-beta dependency in manifest.json +- `permissions.json` allowing `@minecraft/server-admin` + +The behavior packs are **bind-mounted** from the host project directory into Docker containers. +Deploy path on server: `/home/sysadmin/minecraft-multiworld/` + ## Connection The mc-ai-bridge runs at `10.0.0.247`: diff --git a/hub-return-addon/hub_return_transfer_BP/manifest.json b/hub-return-addon/hub_return_transfer_BP/manifest.json index ae05199..ab48ee5 100644 --- a/hub-return-addon/hub_return_transfer_BP/manifest.json +++ b/hub-return-addon/hub_return_transfer_BP/manifest.json @@ -4,7 +4,7 @@ "name": "Hub Return Transfer", "description": "Transfers players back to lobby when they step on the return portal", "uuid": "b2c3d4e5-1111-2222-3333-fedcba654321", - "version": [1, 0, 1], + "version": [1, 0, 3], "min_engine_version": [1, 21, 0] }, "modules": [ @@ -12,14 +12,18 @@ "type": "script", "language": "javascript", "uuid": "b2c3d4e5-4444-5555-6666-fedcba987654", - "version": [1, 0, 1], + "version": [1, 0, 3], "entry": "scripts/main.js" } ], "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.0.0" + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-admin", + "version": "1.0.0-beta" } ] } 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 052a1e2..46c623c 100644 --- a/hub-return-addon/hub_return_transfer_BP/scripts/main.js +++ b/hub-return-addon/hub_return_transfer_BP/scripts/main.js @@ -1,4 +1,5 @@ import { world, system, ItemStack } from "@minecraft/server"; +import { transferPlayer } from "@minecraft/server-admin"; const LOBBY_HOST = "10.0.0.247"; const LOBBY_PORT = 19132; @@ -6,9 +7,12 @@ const COMPASS_ID = "minecraft:recovery_compass"; const PORTAL_PROP = "hub_portal_location"; const PORTAL_RADIUS = 3; const TRANSFER_COOLDOWN = 5000; // 5 seconds +const SPAWN_PROTECTION = 10000; // 10 seconds — ignore portal detection after spawn // Track recently transferred players const recentTransfers = new Map(); +// Track when players spawned (to prevent transfer loop on arrival) +const spawnTimes = new Map(); function doTransfer(player) { const now = Date.now(); @@ -21,7 +25,7 @@ function doTransfer(player) { world.sendMessage(`§a${player.name} is returning to the Hub...`); try { - player.runCommand(`transfer ${player.name} ${LOBBY_HOST} ${LOBBY_PORT}`); + transferPlayer(player, { hostname: LOBBY_HOST, port: LOBBY_PORT }); } catch (e) { player.sendMessage(`§cTransfer failed: ${e.message}`); } @@ -45,6 +49,8 @@ function giveCompassIfMissing(player) { world.afterEvents.playerSpawn.subscribe((event) => { const player = event.player; + // Mark spawn time for portal protection (prevents transfer loop on arrival) + spawnTimes.set(player.name, Date.now()); // Small delay to let inventory load system.runTimeout(() => { try { @@ -82,9 +88,14 @@ system.runInterval(() => { const players = world.getAllPlayers(); for (const player of players) { + // Skip if recently transferred (cooldown) if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) { continue; } + // Skip if player just spawned (prevents loop when arriving from lobby) + if (spawnTimes.has(player.name) && now - spawnTimes.get(player.name) < SPAWN_PROTECTION) { + continue; + } const pos = player.location; const dx = Math.abs(pos.x - portal.x); @@ -131,7 +142,7 @@ function handleChatCommand(player, msg) { return false; } -// Try beforeEvents.chatSend (pre-release in some BDS versions) +// Listen for chat commands — try beforeEvents first (beta API), fall back gracefully try { world.beforeEvents.chatSend.subscribe((event) => { const msg = event.message.trim().toLowerCase(); @@ -141,11 +152,13 @@ try { system.run(() => handleChatCommand(player, msg)); } }); -} catch { - // chatSend not available — fall back to polling player chat via scriptEvent +} catch (e) { + // beforeEvents.chatSend requires beta API — chat commands won't work via chat, + // but compass and portal transfers still function + world.sendMessage("§e[Hub] §fChat commands (!hub, !lobby) unavailable — use compass or portal instead."); } -// Fallback: listen for scriptevent commands (players run: /scriptevent hub:cmd ) +// scriptEvent fallback — players can use: /scriptevent hub:cmd hub system.afterEvents.scriptEventReceive.subscribe((event) => { if (event.id !== "hub:cmd") return; const player = event.sourceEntity; diff --git a/lobby-addon/lobby_transfer_BP/manifest.json b/lobby-addon/lobby_transfer_BP/manifest.json index 47becce..984275f 100644 --- a/lobby-addon/lobby_transfer_BP/manifest.json +++ b/lobby-addon/lobby_transfer_BP/manifest.json @@ -4,7 +4,7 @@ "name": "Lobby Portal Transfer", "description": "Auto-transfers players when they step into portal areas", "uuid": "a1b2c3d4-1111-2222-3333-abcdef123456", - "version": [1, 0, 1], + "version": [1, 0, 3], "min_engine_version": [1, 21, 0] }, "modules": [ @@ -12,14 +12,18 @@ "type": "script", "language": "javascript", "uuid": "a1b2c3d4-4444-5555-6666-abcdef789012", - "version": [1, 0, 1], + "version": [1, 0, 3], "entry": "scripts/main.js" } ], "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.0.0" + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-admin", + "version": "1.0.0-beta" } ] } diff --git a/lobby-addon/lobby_transfer_BP/scripts/main.js b/lobby-addon/lobby_transfer_BP/scripts/main.js index 5f350ce..da751cd 100644 --- a/lobby-addon/lobby_transfer_BP/scripts/main.js +++ b/lobby-addon/lobby_transfer_BP/scripts/main.js @@ -1,4 +1,5 @@ import { world, system } from "@minecraft/server"; +import { transferPlayer } from "@minecraft/server-admin"; // Portal definitions: name, center position, and direct transfer target const portals = [ @@ -11,9 +12,16 @@ const PORTAL_RADIUS_X = 2.5; const PORTAL_RADIUS_Z = 2.0; const PORTAL_RADIUS_Y = 2.0; const COOLDOWN_TICKS = 100; // 5 seconds cooldown +const SPAWN_PROTECTION_TICKS = 200; // 10 seconds — ignore portal detection after spawn // Track cooldowns per player const cooldowns = new Map(); +// Track when players spawned (to prevent transfer loop on arrival) +const spawnTicks = new Map(); + +world.afterEvents.playerSpawn.subscribe((event) => { + spawnTicks.set(event.player.id, system.currentTick); +}); system.runInterval(() => { for (const player of world.getAllPlayers()) { @@ -24,6 +32,10 @@ system.runInterval(() => { const lastTransfer = cooldowns.get(playerId) || 0; if (system.currentTick - lastTransfer < COOLDOWN_TICKS) continue; + // Skip if player just spawned (prevents loop when arriving from child world) + const spawnedAt = spawnTicks.get(playerId) || 0; + if (system.currentTick - spawnedAt < SPAWN_PROTECTION_TICKS) continue; + for (const portal of portals) { const dx = Math.abs(pos.x - portal.x); const dy = Math.abs(pos.y - portal.y); @@ -33,7 +45,7 @@ system.runInterval(() => { cooldowns.set(playerId, system.currentTick); player.sendMessage(`§6Transferring to ${portal.name}...`); try { - player.runCommand(`transfer ${player.name} ${portal.host} ${portal.port}`); + transferPlayer(player, { hostname: portal.host, port: portal.port }); } catch (e) { player.sendMessage(`§cTransfer failed: ${e.message}`); } @@ -43,9 +55,10 @@ system.runInterval(() => { } }, 10); // Check every half second -// Clean up cooldowns when players leave +// Clean up tracking when players leave world.afterEvents.playerLeave.subscribe((event) => { cooldowns.delete(event.playerId); + spawnTicks.delete(event.playerId); }); system.run(() => { diff --git a/scripts/enable-beta-apis.py b/scripts/enable-beta-apis.py new file mode 100644 index 0000000..70d670d --- /dev/null +++ b/scripts/enable-beta-apis.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Enable Beta APIs experiment in a Bedrock level.dat file. + +Bedrock level.dat format: + - 4 bytes: version (little-endian int32) + - 4 bytes: payload length (little-endian int32) + - rest: uncompressed little-endian NBT payload + +This script patches the 'experiments' compound tag to add: + - gametest = 1 (byte) + - experiments_ever_used = 1 (byte) + - saved_with_toggled_experiments = 1 (byte) + +Requires: amulet-nbt (pip install amulet-nbt) +""" + +import struct +import sys +import shutil +import amulet_nbt + + +def patch_level_dat(path: str) -> None: + # Read the file + with open(path, "rb") as f: + header = f.read(8) + payload = f.read() + + version, length = struct.unpack(" [level.dat ...]") + sys.exit(1) + + for path in sys.argv[1:]: + print(f"\nPatching: {path}") + patch_level_dat(path) + + print("\nDone!") + + +if __name__ == "__main__": + main() + +