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:
@@ -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