package space.minecraft; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.MappingResolver; import java.lang.reflect.*; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; /** Local telemetry/control adapter. Gameplay remains in the upstream AltoClef mod. * *

Threading contract: every read or mutation of Minecraft client state * ({@link #snapshot}, {@link #connect}, AltoClef commands) runs ONLY on the * Minecraft client thread, reached through {@code Executor} dispatch (the * client implements {@code java.util.concurrent.Executor}) with a reflective * {@code execute(Runnable)} fallback. Dispatch is never run inline on a * telemetry or HTTP thread: a failed dispatch records a diagnostic and the * bridge keeps reporting a loading/error heartbeat WITHOUT claiming fresh * connected world observations. * *

Freshness contract for /state: {@code heartbeat_at} is refreshed by the * scheduler tick itself (bridge alive), while {@code observed_at} is refreshed * only by a snapshot that actually ran on the client thread. At most one * snapshot is ever queued, so rendering load cannot build a backlog. * *

Connect contract: the client starts at the normal TitleScreen (no * {@code --server/--port}, which would bypass it and leave upstream * uninitialized) and this bridge connects only on a Title/Disconnected * screen with no resource reload overlay (connecting mid-reload NPEs * WorldRenderer via ShaderProgram.addSampler), on the client thread only. */ public final class SpaceBridge implements ClientModInitializer { private static final MappingResolver MAP = FabricLoader.getInstance().getMappingResolver(); private volatile String state = "{\"connected\":false,\"loading\":true,\"heartbeat_at\":0}"; private volatile String lastError = ""; private volatile String lastCommand = ""; private final AtomicBoolean snapshotPending = new AtomicBoolean(false); private long connectAttempt; private long connectAttempts; private volatile String connectState = "startup"; private String lastGate = ""; private long creditsSince; private boolean creditsObserved; private boolean enteredEnd; private String previousDimension = ""; private static Class mc(String intermediary) throws Exception { return Class.forName(MAP.mapClassName("intermediary", "net.minecraft." + intermediary)); } private static Method accessibleMethod(Class owner, String name, Class[] types) throws Exception { try { return owner.getMethod(name, types); } catch (NoSuchMethodException missing) { Method fallback = owner.getDeclaredMethod(name, types); fallback.setAccessible(true); return fallback; } } private static Object method(Object obj, String owner, String id, String desc, Class[] types, Object... args) throws Exception { String name = MAP.mapMethodName("intermediary", "net.minecraft." + owner, id, desc); return accessibleMethod(mc(owner), name, types).invoke(obj, args); } private static Object call(Object obj, String name, Class[] types, Object... args) throws Exception { return accessibleMethod(obj.getClass(), name, types).invoke(obj, args); } private static Object call(Object obj, String name) throws Exception { return call(obj, name, new Class[0]); } private static Object client() throws Exception { return method(null, "class_310", "method_1551", "()Lnet/minecraft/class_310;", new Class[0]); } /** Enqueue on the client thread. Never runs the task inline: throws on failure. */ private static void dispatchToClient(Object client, Runnable task) throws Exception { if (client instanceof Executor) { ((Executor) client).execute(task); return; } accessibleMethod(client.getClass(), "execute", new Class[]{Runnable.class}).invoke(client, task); } private static Object screen(Object client) throws Exception { String name = MAP.mapFieldName("intermediary", "net.minecraft.class_310", "field_1755", "Lnet/minecraft/class_437;"); try { return mc("class_310").getField(name).get(client); } catch (NoSuchFieldException missing) { Field fallback = mc("class_310").getDeclaredField(name); fallback.setAccessible(true); return fallback.get(client); } } private static void respond(HttpExchange ex, int code, String body) throws Exception { byte[] data = body.getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); ex.sendResponseHeaders(code, data.length); ex.getResponseBody().write(data); ex.close(); } /** Loading/error heartbeat: fresh heartbeat_at, no connected observations claimed. */ private void heartbeat(String detail) { Map data = new LinkedHashMap<>(); data.put("connected", false); data.put("loading", true); data.put("heartbeat_at", System.currentTimeMillis() / 1000.0); data.put("dispatch_error", detail == null ? "" : detail); data.put("error", lastError); data.put("last_command", lastCommand); state = json(data); } private void queueSnapshot() { // At most one pending snapshot: skip the tick when the client thread // has not drained the previous one (e.g. under rendering load). if (!snapshotPending.compareAndSet(false, true)) { heartbeat("snapshot still queued on client thread"); return; } boolean queued = false; try { dispatchToClient(client(), () -> { try { snapshot(); } finally { snapshotPending.set(false); } }); queued = true; } catch (Exception err) { lastError = "client dispatch failed: " + err; } finally { if (!queued) { snapshotPending.set(false); heartbeat(lastError); } } } @Override public void onInitializeClient() { try { int port = Integer.parseInt(System.getenv().getOrDefault("ALTOCLEF_BRIDGE_PORT", "3002")); HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 8); server.createContext("/state", ex -> { try { respond(ex, 200, state); } catch (Exception ignored) { ex.close(); } }); server.createContext("/command", ex -> { try { if (!"POST".equals(ex.getRequestMethod())) { respond(ex, 405, "{}"); return; } String command = new String(ex.getRequestBody().readNBytes(128), StandardCharsets.UTF_8).trim(); if (!Set.of("gamer", "stop", "status").contains(command)) { respond(ex, 400, "{\"error\":\"unsupported command\"}"); return; } try { dispatchToClient(client(), () -> { try { Class alto = Class.forName("adris.altoclef.AltoClef"); Object executor = alto.getMethod("getCommandExecutor").invoke(null); if (executor == null) throw new IllegalStateException("AltoClef is not initialized"); executor.getClass().getMethod("executeWithPrefix", String.class).invoke(executor, command); lastCommand = command; lastError = ""; System.out.println("[space-altoclef] command=" + command); } catch (Exception err) { lastError = err.toString(); } }); } catch (Exception err) { lastError = "command dispatch failed: " + err; respond(ex, 503, json(Map.of("error", lastError))); return; } respond(ex, 202, "{\"accepted\":true}"); } catch (Exception err) { try { respond(ex, 503, json(Map.of("error", err.toString()))); } catch (Exception ignored) { ex.close(); } } }); server.setExecutor(Executors.newFixedThreadPool(2)); server.start(); Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "space-altoclef-telemetry"); t.setDaemon(true); return t; }).scheduleWithFixedDelay(() -> { try { queueSnapshot(); } catch (Throwable err) { lastError = "telemetry scheduler: " + err; heartbeat(lastError); } }, 2, 1, TimeUnit.SECONDS); System.out.println("[space-altoclef] local bridge ready on 127.0.0.1:" + port); } catch (Exception err) { throw new RuntimeException("Space bridge failed", err); } } private void connect(Object client, Object currentScreen) throws Exception { // Gated connect: NEVER while a resource/shader reload overlay // is up (that NPEs WorldRenderer via ShaderProgram.addSampler on a // half-built reload), never while vanilla is already connecting, and // never before upstream initialized on the TitleScreen. // Pinned Yarn 1.19.4+build.2: METHOD emh ()Lesx; aJ method_18506 getOverlay // (owner class_310 MinecraftClient, returns class_4071 Overlay). Object overlay = method(client, "class_310", "method_18506", "()Lnet/minecraft/class_4071;", new Class[0]); boolean title = currentScreen != null && (mc("class_442").isInstance(currentScreen) || mc("class_419").isInstance(currentScreen)); boolean connecting = currentScreen != null && mc("class_412").isInstance(currentScreen); String screenName = currentScreen == null ? "" : currentScreen.getClass().getName(); if (overlay != null) { gate("waiting resource reload (overlay=" + overlay.getClass().getName() + ")"); return; } if (connecting) { connectState = "connecting (vanilla ConnectScreen)"; return; } if (!title) { gate("waiting title screen (screen=" + screenName + ")"); return; } if (System.currentTimeMillis() - connectAttempt < 30000) { connectState = "retry waits 30s throttle (screen=" + screenName + ")"; return; } connectAttempt = System.currentTimeMillis(); connectAttempts++; String address = System.getenv().getOrDefault("ALTOCLEF_SERVER", "127.0.0.1:25568"); Object parsed = method(null, "class_639", "method_2950", "(Ljava/lang/String;)Lnet/minecraft/class_639;", new Class[]{String.class}, address); Object info = mc("class_642").getConstructor(String.class, String.class, boolean.class) .newInstance("Minecraft Friends", address, false); method(null, "class_412", "method_36877", "(Lnet/minecraft/class_437;Lnet/minecraft/class_310;Lnet/minecraft/class_639;Lnet/minecraft/class_642;)V", new Class[]{mc("class_437"), mc("class_310"), mc("class_639"), mc("class_642")}, screen(client), client, parsed, info); connectState = "connecting to " + address + " (attempt " + connectAttempts + ")"; System.out.println("[space-altoclef] connecting to " + address + " (attempt " + connectAttempts + ")"); } /** Throttled gate diagnostic: log only when the reason changes. */ private void gate(String reason) { connectState = reason; if (!reason.equals(lastGate)) { lastGate = reason; System.out.println("[space-altoclef] connect gated: " + reason); } } private String currentDimension() throws Exception { // Upstream WorldHelper.getCurrentDimension is package-private static; // resolve it as a declared method so snapshots survive visibility. Class helper = Class.forName("adris.altoclef.util.helpers.WorldHelper"); return String.valueOf(accessibleMethod(helper, "getCurrentDimension", new Class[0]).invoke(null)); } /** Runs ONLY on the Minecraft client thread. Refreshes observed_at. */ private void snapshot() { Map data = new LinkedHashMap<>(); try { Object client = client(); Class alto = Class.forName("adris.altoclef.AltoClef"); Object mod = alto.getMethod("getInstance").invoke(null); Object executor = alto.getMethod("getCommandExecutor").invoke(null); Object player = mod == null ? null : call(mod, "getPlayer"); data.put("connected", player != null); data.put("initialized", executor != null); data.put("last_command", lastCommand); data.put("error", lastError); Object currentScreen = screen(client); boolean credits = currentScreen != null && mc("class_445").isInstance(currentScreen); if (credits) { if (enteredEnd) creditsObserved = true; if (creditsSince == 0) creditsSince = System.currentTimeMillis(); if (System.currentTimeMillis() - creditsSince > 12000) { method(currentScreen, "class_437", "method_25419", "()V", new Class[0]); } } data.put("credits_screen", credits); data.put("credits_observed", creditsObserved); data.put("entered_end", enteredEnd); data.put("connect_state", connectState); data.put("connect_attempts", connectAttempts); if (player == null) { if (executor != null && !credits) connect(client, currentScreen); } else { String dimension = currentDimension(); if ("END".equals(dimension)) enteredEnd = true; data.put("dimension", dimension); data.put("returned_from_end", enteredEnd && "OVERWORLD".equals(dimension)); if (!dimension.equals(previousDimension)) { System.out.println("[space-altoclef] dimension=" + dimension); previousDimension = dimension; } data.put("position", Map.of("x", method(player,"class_1297","method_23317","()D",new Class[0]), "y", method(player,"class_1297","method_23318","()D",new Class[0]), "z", method(player,"class_1297","method_23321","()D",new Class[0]))); data.put("health", method(player,"class_1309","method_6032","()F",new Class[0])); Object controller = call(mod, "getController"); data.put("creative", method(controller,"class_636","method_2914","()Z",new Class[0])); Object inventory = method(player,"class_1657","method_31548","()Lnet/minecraft/class_1661;",new Class[0]); List items = new ArrayList<>(); for (int slot = 0; slot < 41; slot++) { Object stack = method(inventory,"class_1263","method_5438","(I)Lnet/minecraft/class_1799;",new Class[]{int.class},slot); int count = (Integer) method(stack,"class_1799","method_7947","()I",new Class[0]); if (count > 0) items.add(Map.of("slot",slot,"item",String.valueOf(method(stack,"class_1799","method_7909","()Lnet/minecraft/class_1792;",new Class[0])),"count",count)); } data.put("inventory", items); Object chain = call(mod,"getUserTaskChain"); if (chain == null) { // Upstream builds the task chain on TitleScreen entry; a // null chain during loading reports initialized=false with // diagnostics, never an NPE, and never a fabricated // task_active. data.put("initialized", false); data.put("task_active", false); data.put("init_note", "taskchain not yet initialized (waiting TitleScreen entry)"); } else { data.put("task_active", call(chain,"isActive")); data.put("tasks", String.valueOf(call(chain,"getTasks"))); } data.put("entered_end", enteredEnd); lastError = ""; data.put("error", ""); } } catch (Exception err) { lastError = err.toString(); data.put("error", lastError); } data.put("heartbeat_at", System.currentTimeMillis() / 1000.0); data.put("observed_at", System.currentTimeMillis() / 1000.0); state = json(data); } private static String json(Object value) { if (value == null) return "null"; if (value instanceof Boolean || value instanceof Number) return value.toString(); if (value instanceof Map map) { StringJoiner parts = new StringJoiner(",", "{", "}"); map.forEach((k,v) -> parts.add(json(String.valueOf(k)) + ":" + json(v))); return parts.toString(); } if (value instanceof Collection list) { StringJoiner parts = new StringJoiner(",", "[", "]"); list.forEach(v -> parts.add(json(v))); return parts.toString(); } return "\"" + value.toString().replace("\\","\\\\").replace("\"","\\\"") .replace("\n","\\n").replace("\r","\\r").replace("\t","\\t") + "\""; } }