Expand material support and checkpoint the completed building toolkit
Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
This commit is contained in:
@@ -98,3 +98,11 @@ A real graphical test ran on September 12, 2026: one Prism client, the project o
|
||||
The first request ended with `view_changed`: actual rotation differed from the requested rotation. A retry after stabilization produced a **1280×720 PNG in 2.052 seconds**, with yaw **140°**, pitch **31°**, after **20 ticks** and **3 frames** of readiness. This is a verified local measurement of one request, not a timing guarantee for other scenes or computers. Verification artifacts: `.runtime/camera-test/20260912T192813Z-2b100930.png` and its JSON metadata; they remain local and are excluded from Git.
|
||||
|
||||
The successful image was captured after the building operation completed and includes its `afterOperationId`, but **`serverRevisionVerified` remains `false`**: processing of a specific server revision is not yet acknowledged. Remaining checks include restoration of all view settings, timeout with a minimized window, disconnection during capture, third-party shaders, and a separate camera account. The working graphical cycle is verified for the single-client scenario described above.
|
||||
|
||||
## Opt-in local auto-connect
|
||||
|
||||
Set `camera-auto-connect: '127.0.0.1:25575'` in the private Paper config used by `scripts/camera-wrapper.py`; the wrapper passes `MCB_CAMERA_AUTO_CONNECT` to Minecraft. Restart the client once after installing this mod update. Only a literal loopback address and an unprivileged port are accepted. The mod retries from the title, multiplayer or disconnected screen at most once every ten seconds, never during an active connection or another menu. Joining a different server or a single-player world suspends auto-connect for that client process.
|
||||
|
||||
Create `config/minecraft-builder-camera.autojoin-disabled` inside the Minecraft instance to pause retries without restarting; remove it to resume. The health endpoint reports enabled/paused state, target and attempt count. Remove the opt-in setting and restart the client to disable permanently. Opt-in deliberately includes reconnecting after a manual disconnect from the configured server.
|
||||
|
||||
Auto-connect uses Minecraft's normal connection flow. It does not authenticate new accounts, launch Prism, execute chat commands, change game mode, move the player, or grant editing rights. A trusted local operator can separately use the Paper console for `lobby <player>` and `gamemode spectator <player>`. Graphical rendering is still required for photographs; menus and user input can invalidate a capture.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.minecraftbuilder.camera;
|
||||
|
||||
/** Opt-in loopback connection policy. No game or network dependencies. */
|
||||
public final class AutoConnectPolicy {
|
||||
private final String target;
|
||||
private long nextAttempt;
|
||||
private int attempts;
|
||||
public AutoConnectPolicy(String target, long now) {
|
||||
if (!target.matches("127\\.0\\.0\\.1:[0-9]{1,5}")) throw new IllegalArgumentException("Auto-connect requires a literal loopback address and port");
|
||||
int port = Integer.parseInt(target.substring(target.indexOf(':')+1));
|
||||
if (port < 1024 || port > 65535) throw new IllegalArgumentException("Invalid local server port");
|
||||
this.target = target; nextAttempt = now + 10_000;
|
||||
}
|
||||
public String target() { return target; }
|
||||
public int attempts() { return attempts; }
|
||||
public boolean due(long now, boolean connected, boolean eligibleScreen, boolean paused) {
|
||||
if (connected) { nextAttempt = now + 10_000; return false; }
|
||||
if (paused || !eligibleScreen || now < nextAttempt) return false;
|
||||
nextAttempt = now + 10_000; attempts++; return true;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,12 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.gui.screens.ConnectScreen;
|
||||
import net.minecraft.client.gui.screens.DisconnectedScreen;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
|
||||
import net.minecraft.client.multiplayer.ServerData;
|
||||
import net.minecraft.client.multiplayer.resolver.ServerAddress;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -42,6 +48,10 @@ public final class CameraClient implements ClientModInitializer, CameraHttpServe
|
||||
.daemon(true).name("mcb-camera-watchdog").factory());
|
||||
private volatile JsonObject cachedHealth = CameraHttpServer.error("starting", "Waiting for client tick");
|
||||
private CameraHttpServer http;
|
||||
private AutoConnectPolicy autoConnect;
|
||||
private boolean autoConnectSuspended;
|
||||
private boolean autoConnectPaused;
|
||||
private long nextPauseCheck;
|
||||
|
||||
@Override public void onInitializeClient() {
|
||||
String token = System.getenv("MCB_CAMERA_TOKEN");
|
||||
@@ -55,6 +65,8 @@ public final class CameraClient implements ClientModInitializer, CameraHttpServe
|
||||
int port = Integer.parseInt(System.getenv().getOrDefault("MCB_CAMERA_PORT", "8766"));
|
||||
if (port < 1024 || port > 65535) throw new IllegalArgumentException("Invalid camera port");
|
||||
http = new CameraHttpServer(port, token, this);
|
||||
String target = System.getenv("MCB_CAMERA_AUTO_CONNECT");
|
||||
if (target != null && !target.isBlank()) autoConnect = new AutoConnectPolicy(target, System.currentTimeMillis());
|
||||
instance = this;
|
||||
ClientTickEvents.END_CLIENT_TICK.register(this::tick);
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(this::stop);
|
||||
@@ -93,12 +105,19 @@ public final class CameraClient implements ClientModInitializer, CameraHttpServe
|
||||
}
|
||||
|
||||
private void tick(Minecraft client) {
|
||||
autoConnect(client);
|
||||
Job job = active.get();
|
||||
JsonObject health = new JsonObject();
|
||||
health.addProperty("status", "ok");
|
||||
health.addProperty("connected", client.level != null && client.player != null);
|
||||
health.addProperty("spectator", client.player != null && client.player.isSpectator());
|
||||
health.addProperty("busy", job != null);
|
||||
health.addProperty("autoConnectEnabled", autoConnect != null);
|
||||
if (autoConnect != null) {
|
||||
health.addProperty("autoConnectTarget", autoConnect.target());
|
||||
health.addProperty("autoConnectAttempts", autoConnect.attempts());
|
||||
health.addProperty("autoConnectPaused", autoConnectPaused || autoConnectSuspended);
|
||||
}
|
||||
health.addProperty("updatedAt", Instant.now().toString());
|
||||
if (client.level != null) health.addProperty("dimension", dimension(client));
|
||||
if (client.player != null) health.addProperty("playerId", client.player.getUUID().toString());
|
||||
@@ -138,6 +157,26 @@ public final class CameraClient implements ClientModInitializer, CameraHttpServe
|
||||
if (chunksLoaded(client)) job.stableTicks++; else { job.stableTicks = 0; job.readyFrames = 0; }
|
||||
}
|
||||
|
||||
private void autoConnect(Minecraft client) {
|
||||
if (autoConnect == null) return;
|
||||
long now = System.currentTimeMillis();
|
||||
if (now >= nextPauseCheck) {
|
||||
autoConnectPaused = java.nio.file.Files.exists(client.gameDirectory.toPath().resolve("config/minecraft-builder-camera.autojoin-disabled"));
|
||||
nextPauseCheck = now + 1000;
|
||||
}
|
||||
if (client.level != null) {
|
||||
ServerData server = client.getCurrentServer();
|
||||
if (server == null || !server.ip.equals(autoConnect.target())) autoConnectSuspended = true;
|
||||
}
|
||||
var screen = client.gui.screen();
|
||||
boolean eligible = screen instanceof TitleScreen || screen instanceof DisconnectedScreen || screen instanceof JoinMultiplayerScreen;
|
||||
if (!autoConnect.due(now, client.level != null || client.getConnection() != null,
|
||||
eligible, autoConnectPaused || autoConnectSuspended || client.gui.overlay() != null)) return;
|
||||
LOGGER.info("Observer connecting to configured local server (attempt {})", autoConnect.attempts());
|
||||
ConnectScreen.startConnecting(new TitleScreen(), client, ServerAddress.parseString(autoConnect.target()),
|
||||
new ServerData("Minecraft Builder local camera", autoConnect.target(), ServerData.Type.OTHER), false, null);
|
||||
}
|
||||
|
||||
/** Called after GameRenderer.render, on Minecraft's render thread. Uses the supported GPU screenshot API. */
|
||||
public static void afterRender(boolean renderWorld) {
|
||||
CameraClient worker = instance;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.minecraftbuilder.camera;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AutoConnectPolicyTest {
|
||||
@Test void onlyExplicitUnprivilegedLoopbackTargetsAreAccepted() {
|
||||
assertEquals("127.0.0.1:25575", new AutoConnectPolicy("127.0.0.1:25575",0).target());
|
||||
for(String target:new String[]{"example.com:25565","localhost:25575","127.0.0.1:80","127.0.0.1:99999","127.0.0.1:25575/","127.0.0.1:25575\n"})
|
||||
assertThrows(IllegalArgumentException.class,()->new AutoConnectPolicy(target,0));
|
||||
}
|
||||
@Test void retriesAreThrottledAndNeverInterruptAConnectionOrOtherScreen() {
|
||||
var policy=new AutoConnectPolicy("127.0.0.1:25575",0);
|
||||
assertFalse(policy.due(9999,false,true,false));
|
||||
assertTrue(policy.due(10000,false,true,false));
|
||||
assertFalse(policy.due(10001,false,true,false));
|
||||
assertFalse(policy.due(20000,false,false,false));
|
||||
assertFalse(policy.due(20000,false,true,true));
|
||||
assertTrue(policy.due(20000,false,true,false));
|
||||
assertFalse(policy.due(40000,true,true,false));
|
||||
assertFalse(policy.due(40001,false,true,false));
|
||||
assertTrue(policy.due(50000,false,true,false));
|
||||
assertEquals(3,policy.attempts());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user