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.
@@ -6,7 +6,7 @@ A Minecraft building editor for collaboration between a human and an AI agent.
|
||||
|
||||
An unedited in-game capture of our Gothic hall: 30,125 blocks and 53 lanterns. See the [build report](docs/builds/GOTHIC_HALL.md) for the reference, checked incremental edits, and dusk and interior photos.
|
||||
|
||||
Working prototype: a Paper plugin, an editing core, an MCP/ACP Bridge, and a Fabric camera mod. Building, conflicts, undo, crash recovery, and `.schem` have been tested against local Paper through HTTP and real MCP stdio. The camera has been tested in Prism with one client: a real 1280×720 PNG was delivered through MCP. Signing in to the separate Codex profile and completing the first model turn through ACP still need verification.
|
||||
Working prototype: a Paper plugin, an editing core, an MCP/ACP Bridge, and a Fabric camera mod. Building, conflicts, undo, crash recovery, and `.schem` have been tested against local Paper through HTTP and real MCP stdio. The camera has been tested in Prism with one client: a real 1280×720 PNG was delivered through MCP. Dedicated ChatGPT sign-in, in-game text replies, and a real one-block build → inspect → undo cycle through ACP have also been verified.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -14,12 +14,24 @@ Working prototype: a Paper plugin, an editing core, an MCP/ACP Bridge, and a Fab
|
||||
- Saves a plan before applying it in slices with live block checks. Manual changes stop conflicting writes; undo also checks the current world state.
|
||||
- Keeps an on-disk journal, distinguishes request retries from new operations, and stops ambiguous operations after a crash.
|
||||
- Saves named parts and their protection, and exports/imports a limited Sponge v2 `.schem` subset through the same planning engine.
|
||||
- Provides 14 MCP tools and `/ai` game chat through a pinned `codex-acp` adapter.
|
||||
- Provides 19 MCP tools and `/ai` game chat through a pinned `codex-acp` adapter.
|
||||
- Discovers the running server's complete vanilla block and item catalog on demand; ordinary recipes accept every registered block type and its valid states.
|
||||
- Captures real images through a local worker on a spectator client. A single client with the owner temporarily in spectator has also been tested.
|
||||
- Exports real north-up world surface maps without a player through the optional terrain plugin; applies reversible, terrain-following site layouts with checked snapshots.
|
||||
|
||||
The current scope is one owner, one project, and one world, with at most 4096 blocks per plan, loaded chunks only, and a limited vanilla material palette. A complete delta history, automatic merging of manual edits, and the full design document are not implemented yet. [Detailed status and limitations](docs/IMPLEMENTATION.md).
|
||||
The current scope is one owner, one project, and one world, with at most 4096 blocks per plan and loaded chunks only. A complete delta history, automatic merging of manual edits, and the full design document are not implemented yet. [Detailed status and limitations](docs/IMPLEMENTATION.md).
|
||||
|
||||
The building palette contains 71 materials. Decorative additions include lanterns, `iron_chain` (the Minecraft 26.2 ID), iron bars, stone brick walls, persistent oak leaves, moss, gray/brown stained glass, glowstone, and gold blocks. Leaves require `persistent=true`; waterlogged states remain unsupported. The strict `.schem` codec currently retains the original 61-material subset.
|
||||
Material discovery stays compact: `project_context` returns catalog counts and a version, `material_search` returns 16 results by default (32 maximum), and `material_describe` returns the exact default state and property values for one chosen material. No full registry or list of every state combination is injected into the model context. This includes carpets, trapdoors, pots, candles, doors, redstone, fluids and waterlogged states when registered by the running Minecraft version. Item-only materials are searchable but cannot be placed as blocks. Existing block-entity data is preserved for same-material state edits and checked through opaque snapshot hashes; there is no arbitrary NBT, inventory or sign-text editor. [Material discovery and building rules](docs/MATERIALS.md).
|
||||
|
||||
Block support does not replace placement design: doors, beds and tall plants need all their parts, attachments need support, and decorative leaves should normally use `persistent=true`. Terrain brushes still reject fluids and structures in their scan. The `.schem` codec accepts registered block states but rejects entity/block-entity payloads and refuses to export block entities. World checkpoints remain necessary for complete landscaped builds and their extra data.
|
||||
|
||||
## Shacraft lobby construction
|
||||
|
||||
The clock station in zone 02 now contains a furnished vestibule, a SMASH selection floor and a two-stop lift, with unfinished upper spaces closed. See the [station photographs and verification](docs/SHACRAFT-STATION.md). For future work, use the [decoration playbook](docs/DECORATION-PLAYBOOK.md), [14 practical recipes](docs/DECORATION-RECIPES.md) and [structured recipe catalog](docs/recipes/decorations.json).
|
||||
|
||||
The arrival square is finished with the real Shacraft emblem, hexagonal paving, evergreen flower beds, benches, copper-hood lanterns, stone urns and a welcome board. A cream-capped balustrade frames the square. An independently reversible invisible enclosure currently keeps normal players inside zone 01, including at its five road approaches and above the square; the configured lobby spawn remains inside. The station foundation, local roads and broad stairs are ready; other districts remain marked reservations. [Completed zone 01 and containment verification](docs/SHACRAFT-ZONE01-COMPLETE.md) · [Balustrade](docs/SHACRAFT-BALUSTRADE.md) · [Arrival garden](docs/SHACRAFT-ARRIVAL-GARDEN.md) · [Foundations and elevations](docs/SHACRAFT-FOUNDATIONS.md) · [Earlier site survey](docs/SHACRAFT-LOBBY-LAYOUT.md).
|
||||
|
||||

|
||||
|
||||
## Build
|
||||
|
||||
@@ -36,6 +48,7 @@ Build outputs:
|
||||
- `paper-plugin/target/paper-plugin-0.1.0-SNAPSHOT.jar` — server plugin, including the editing core.
|
||||
- `camera-mod/build/libs/minecraft-builder-camera-0.1.0-SNAPSHOT.jar` — client mod.
|
||||
- `bridge/dist/` — executable MCP/ACP components.
|
||||
- `terrain-world-plugin/target/terrain-world-plugin-0.1.0-SNAPSHOT.jar` — optional separate-world generator and read-only surface map exporter.
|
||||
|
||||
## Local setup
|
||||
|
||||
@@ -54,7 +67,7 @@ Build outputs:
|
||||
/ai area here
|
||||
```
|
||||
|
||||
The second command selects an area around the player. Chunks must be loaded, and blocks next to writes must be supported. Set exact bounds with `/ai area minX minY minZ maxX maxY maxZ`.
|
||||
The second command selects an area around the player. Chunks needed for the edit and its checked surroundings must be loaded. Set exact bounds with `/ai area minX minY minZ maxX maxY maxZ`.
|
||||
|
||||
3. From another terminal at the project root, check the settings and sign in to the separate Codex profile:
|
||||
|
||||
@@ -64,7 +77,7 @@ Build outputs:
|
||||
python3 scripts/bridge.py login --status
|
||||
```
|
||||
|
||||
The user completes sign-in using a device code. The helper reads local Paper tokens without printing them. It does not copy the normal `~/.codex` profile; project state lives in `.runtime/bridge-state`. The first real turn still needs to confirm authentication and MCP permissions in the pinned adapter.
|
||||
The user completes sign-in using a device code. The helper reads local Paper tokens without printing them. It does not copy the normal `~/.codex` profile; project state lives in `.runtime/bridge-state`. After starting chat, test an actual MCP read to verify the local connection and model tool access.
|
||||
|
||||
4. Start chat:
|
||||
|
||||
@@ -94,8 +107,21 @@ JAVA_HOME="$HOME/.cache/minecraft-builder-mcp/jdk-25.0.2" camera-mod/gradlew --p
|
||||
|
||||
- [Project design and future phases](docs/DESIGN.md).
|
||||
- [Implementation status and verification](docs/IMPLEMENTATION.md).
|
||||
- [Runtime materials and compact discovery](docs/MATERIALS.md).
|
||||
- [Gothic hall from a reference: 30,125 blocks in the live world](docs/builds/GOTHIC_HALL.md).
|
||||
- [Protocol](docs/PROTOCOL.md), [editing core and journal](world-core/README.md).
|
||||
- [Bridge, sign-in, and ACP limitations](bridge/README.md).
|
||||
|
||||
Git is initialized on branch `main`. Generated worlds, secrets, dependencies, and build outputs are excluded by `.gitignore`.
|
||||
|
||||
## Terrain toolkit
|
||||
|
||||
Deterministic terrain recipes now support hills, ridges, plateaus, dry basins/channels and terraces. `terrain_preview` returns a native heightmap and cached recipe ID; `terrain_prepare` creates a checked tile plan for the existing apply/undo pipeline. Offline previews and bounded resumable batches are available through `scripts/terrain.py`. See [Terraforming tools](docs/TERRAFORMING.md) for examples, safety semantics and scale limits.
|
||||
|
||||
For a fresh map, the optional [initial terrain world plugin](terrain-world-plugin/README.md) generates a separate world directly from a recipe, with optional lakes and rivers at a configured water level. The [Shacraft lobby recipe](examples/terrain/shacraft-lobby-world.json) lays out a 768×768 mountain basin with building platforms. Initial generation has its own immutable manifest; subsequent construction uses the normal scoped editor.
|
||||
|
||||
`terrain_brush_prepare` additionally edits existing terrain relatively: raise/lower, flatten and snapshot-based smoothing, with soft edges, preserved columns, native before/after previews and checked read dependencies. See the relative-brush section of the terraforming guide.
|
||||
|
||||
The natural Shacraft v2 world replaces rectangular terrain platforms with a warped ridged mountain basin, soft hills and connected water courses. The live generation pass checked every column, including surface materials and water fill.
|
||||
|
||||

|
||||
|
||||
@@ -33,17 +33,23 @@ Optional settings:
|
||||
- `MCB_ACP_COMMAND`: path to an alternative ACP agent. Otherwise, the current Node executable and pinned `codex-acp` are used.
|
||||
- `MCB_ACP_ARGS`: JSON array of arguments, with no shell interpretation.
|
||||
- `MCB_STATE_DIR`: state directory, defaulting to `.state/chat` relative to the working directory.
|
||||
- `MCB_CODEX_HOME`: explicit path to a separate Codex home for sign-in. If a different `config.toml` already exists there, the bridge refuses to start and preserves that file; use a separate empty directory rather than the normal Codex profile.
|
||||
- `MCB_CODEX_HOME`: explicit path to a separate Codex home for sign-in. The exact older bridge-managed config is migrated with a `config.toml.before-code-mode-host` backup and without changing sign-in. If a custom `config.toml` exists there, the bridge refuses to start and preserves that file; use a separate empty directory rather than the normal Codex profile.
|
||||
|
||||
The child process receives only allowlisted environment variables, separate HOME/XDG/CODEX_HOME directories, and a minimal configuration. Settings request `read-only`, `on-request`, user review, no network inside the command sandbox, and disabled shell, apps, browser, computer use, hooks, plugins, and additional agents. Sources and settings are in `src/security.ts`. Inspection of the pinned CLI confirmed that `shell_tool` and the listed integrations were disabled; that CLI keeps `unified_exec` enabled even when explicitly disabled, which doctor reports.
|
||||
|
||||
There is a limitation in `codex-acp` 1.11.0 itself: its `read-only` mode sends a `workspace-write` sandbox with networking disabled for every turn, rather than a literal read-only sandbox. As a result, the individual session directory and temporary paths may remain writable. The code does not claim complete OS isolation and has not yet been verified on a real model turn. Bridge/ACP/MCP processes also remain trusted local programs; Paper permissions are checked separately by the server.
|
||||
The bundled Code Mode host is enabled because models with `tool_mode=code_mode_only` need it to invoke MCP, even when `features.code_mode=false`. Disabling the host leaves text chat working but makes tool calls fail with `code-mode host is disabled`. This host does not enable the separately disabled shell or integrations.
|
||||
|
||||
The Bridge denies additional permission requests and reports this in the game. Interactive permission approval through Minecraft is not implemented. It does not advertise ACP file or terminal capabilities. Paper's administrator token is not passed to the child agent; MCP receives a separate restricted agent token. Check configuration with doctor after version changes or when system-wide Codex policies are present. The dynamically supplied Minecraft MCP override does not set `default_tools_approval_mode`: the pinned adapter passes only command/args/env and replaces the corresponding configuration table. MCP permission behavior therefore remains a check for the first real turn after user sign-in; a successful build and initialize do not prove that model-driven building already works.
|
||||
There is a limitation in `codex-acp` 1.11.0 itself: its `read-only` mode sends a `workspace-write` sandbox with networking disabled for every turn, rather than a literal read-only sandbox. As a result, the individual session directory and temporary paths may remain writable. The code does not claim complete OS isolation; successful MCP calls do not prove a complete operating-system sandbox. Bridge/ACP/MCP processes also remain trusted local programs; Paper permissions are checked separately by the server.
|
||||
|
||||
During an active owner request, the Bridge grants a one-use approval only for one of its 19 known Minecraft tools. It requires a matching tool-call notification from the current adapter and session, the exact Minecraft server/tool identity, MCP approval metadata, and an `allow_once` option. Unknown tools, other servers, shell requests, stale calls, and persistent approvals remain denied. Paper independently checks the owner, project region, protected parts, and expected block states. The administrator token stays in the Bridge; MCP receives the restricted agent token. Interactive approval for additional capabilities is not implemented.
|
||||
|
||||
Explicit Minecraft MCP startup failures stop the request with a safe error message. Raw adapter diagnostics and tool arguments are not displayed in game. A missing failure notification alone does not establish readiness; verify an actual tool call after setup.
|
||||
|
||||
## MCP
|
||||
|
||||
Tools: `project_context`, `region_inspect`, `build_prepare`, `build_apply`, `operation_status`, `operation_cancel`, `operation_undo_prepare`, `part_get`, `part_define`, `camera_list`, `camera_capture`, `asset_list`, `schematic_export`, `schematic_import_prepare`.
|
||||
Tools: `project_context`, `material_search`, `material_describe`, `region_inspect`, `build_prepare`, `build_apply`, `operation_status`, `operation_cancel`, `operation_undo_prepare`, `part_get`, `part_define`, `camera_list`, `camera_capture`, `asset_list`, `schematic_export`, `schematic_import_prepare`, `terrain_preview`, `terrain_prepare`, `terrain_brush_prepare`.
|
||||
|
||||
`project_context` contains only a material catalog summary. `material_search` filters IDs with a query of at most 96 characters, a `kind` of `block` (default), `item` or `all`, and a bounded page size (default 16, maximum 32). Its optional cursor is at most 100 characters and is bound to the catalog version and filter. `material_describe` accepts one exact namespaced Minecraft ID and returns its default state, separate allowed values for each property and compact behavior hints. Item-only entries have no placeable block state. Search first, describe 1–3 selected materials, then reuse those results while the catalog version is unchanged. [Complete workflow and capability boundaries](../docs/MATERIALS.md).
|
||||
|
||||
Version 1 recipe:
|
||||
|
||||
@@ -61,11 +67,13 @@ Version 1 recipe:
|
||||
|
||||
An optional `part_id` in `build_prepare` restricts writes to the exact mask of a registered part; extensions use a separate part.
|
||||
|
||||
Tools send data to Paper for final validation and computation. The bridge does not store blocks or write to the world. MCP accepts one level of `repeat`; deeply nested repeats, arbitrary code, arches, and general transforms are not advertised yet. Supported block states and limits come from `project_context`.
|
||||
Tools send data to Paper for final validation and computation. The bridge does not store blocks or write to the world. MCP accepts one level of `repeat`; deeply nested repeats, arbitrary code, arches, and general transforms are not advertised yet. Limits come from `project_context`; exact block properties come from `material_describe`. Ordinary recipes accept all registered vanilla block states, including fluids and waterlogged states, in strings of at most 1024 characters. The state-string grammar excludes raw NBT.
|
||||
|
||||
For a plan based on an earlier `region_inspect` block read, `expected_blocks` must cover every desired position exactly once. Copy any returned `snapshot_id` unchanged alongside `pos` and `state`; block entities require that 64-character lowercase SHA-256 digest so a change to their extra data also causes a conflict. Same-material state edits preserve existing block-entity data. New block entities use their defaults; recipes do not configure sign text or inventories.
|
||||
|
||||
`build_prepare` returns `plan_id` and `plan_hash`. Pass both to `build_apply` with a stable `idempotency_key`. After a timeout, writing may already have started: check `operation_status` first and reuse the same key. The bridge does not automatically retry writes.
|
||||
|
||||
The local `.schem` library supports up to 64 files, Sponge v2, dense regions of at most 4096 blocks, and rotations of 0/90/180/270°. Place files manually in the `schematics` directory inside the Paper plugin's data directory. `asset_list` returns metadata without invented previews; `schematic_export` saves a region and returns its ID; `schematic_import_prepare` creates a normal checked plan that is then applied through `build_apply`. Paths, entities, block entities, and unsupported blocks are rejected. The strict codec currently supports the original 61-material subset; the ten newer decorative materials in the building palette are not yet supported by `.schem`.
|
||||
The local `.schem` library supports up to 64 files, Sponge v2, dense regions of at most 4096 blocks, and rotations of 0/90/180/270°. Place files manually in the `schematics` directory inside the Paper plugin's data directory. `asset_list` returns metadata without invented previews; `schematic_export` saves a region and returns its ID; `schematic_import_prepare` creates a normal checked plan that is then applied through `build_apply`. Registered block states use the runtime validator and rotation behavior. Arbitrary paths, entity/block-entity NBT payloads and unknown states are rejected. Export refuses block entities, including empty ones, rather than silently discarding their data. A palette entry for a block-entity block type without an NBT payload can be imported through the normal default/preservation rules. Preserve complete landscaped builds with world checkpoints and checked voxel recipes.
|
||||
|
||||
`camera_capture` returns `pending` and `captureId`; a request with `capture_id` reads the result. Only `completed` with a real image becomes MCP `ImageContent`. If the camera is absent or the capture is not ready, no image is fabricated. Ordinary text responses are limited to 64 KiB; reading an oversized region fails with a request to reduce its size. HTTP has time, input-size, and streaming response-size limits.
|
||||
|
||||
@@ -75,14 +83,28 @@ One active turn per project, with up to eight queued messages. Different project
|
||||
|
||||
Each request includes the player position, viewing direction, and targeted block supplied by the server, when available. Message output is limited to four messages per second per player.
|
||||
|
||||
Streamed text is collected into complete sentences or lines. Long passages are split at word boundaries into messages of at most 240 characters; a single longer word is split without breaking a Unicode surrogate pair. An unfinished short phrase waits for more text or the end of the turn. Delivery is serialized, so slow HTTP responses cannot turn individual tokens into separate chat messages or let the completion marker overtake the reply.
|
||||
|
||||
The ACP session ID and latest compact summary are persisted by atomically replacing `session.json` with permissions `0600`. After restarting, the bridge tries `session/load`; if that fails, it starts a new conversation with the summary and explicitly reports the fallback. Replayed history is not shown in chat. An unfinished previous turn is marked separately; operations already started must be checked on Paper. The summary stores the latest request and outcome and does not replace `project_context`.
|
||||
|
||||
`/ai stop` must both set the server's write-stop flag and deliver a `type:"cancel"` event to the bridge. ACP cancels generation; blocks already changed remain in the journal. An interrupted agent that does not respond to cancellation within five seconds is terminated. On each launch, the bridge creates a `client_id` and passes it to `chat_poll`. A changed ID lets Paper stop active writes and finish outstanding leased requests while notifying the user; those requests are not replayed automatically. Paper's incoming message queue is currently in memory. After `/ai stop`, the server keeps writing paused until a new owner request or `/ai resume`.
|
||||
|
||||
## Verification
|
||||
|
||||
`npm test` runs HTTP tests and a real stdio MCP handshake, and uses a separate mock ACP process to test sessions, summaries, history suppression, permission denial, and cancellation. Queue tests verify serialization within a project and independence between projects. The real pinned `codex-acp` also passed a free `initialize`: ACP v1, `loadSession: true`, and authentication methods `api-key` and `chat-gpt` before environment restrictions. A repeat check in a separate home also passed; with `NO_BROWSER=1`, the adapter advertises only `api-key`, while ChatGPT sign-in uses the separate `login` helper. This does not prove ChatGPT authentication, model quality, or Minecraft behavior: those require the complete system running against a real server/client.
|
||||
`npm test` runs HTTP tests and a real stdio MCP handshake, and uses a separate mock ACP process to test sessions, summaries, history suppression, permission denial, and cancellation. Queue tests verify serialization within a project and independence between projects. The real pinned `codex-acp` also passed a free `initialize`: ACP v1, `loadSession: true`, and authentication methods `api-key` and `chat-gpt` before environment restrictions. A repeat check in a separate home also passed; with `NO_BROWSER=1`, the adapter advertises only `api-key`, while ChatGPT sign-in uses the separate `login` helper. The initial handshake did not invoke a model. Subsequently, dedicated ChatGPT sign-in and real ACP model calls to `project_context` and `region_inspect` were verified against the running Paper server. A real model also prepared and applied one oak-plank block at (24, -60, -45), inspected it, prepared and applied checked undo, and verified air. A local test proxy restricted writes to that one cell and its recorded undo. Ordinary automated tests still use mocks and do not invoke a model.
|
||||
|
||||
The optional `node test/live-paper.mjs` runs only against a separate real test Paper server: it checks a hollow cube, applying again with the same key, `.schem` export, the asset library, undo, import at the same anchor, a second undo back to 27 air blocks, bounds, and a truthful unavailable-camera response. It leaves the exported test asset in the local library. This test changes the world and is not part of ordinary `npm test`.
|
||||
|
||||
The original APIs were checked against the [ACP SDK](https://github.com/agentclientprotocol/typescript-sdk), [codex-acp](https://github.com/agentclientprotocol/codex-acp), [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk), [Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference), and [Codex MCP documentation](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).
|
||||
|
||||
## Terrain toolkit
|
||||
|
||||
Deterministic terrain recipes now support hills, ridges, plateaus, dry basins/channels and terraces. `terrain_preview` returns a native heightmap and cached recipe ID; `terrain_prepare` creates a checked tile plan for the existing apply/undo pipeline. Offline previews and bounded resumable batches are available through `scripts/terrain.py`. See [Terraforming tools](../docs/TERRAFORMING.md) for examples, safety semantics and scale limits.
|
||||
|
||||
`terrain_brush_prepare` additionally edits existing terrain relatively: raise/lower, flatten and snapshot-based smoothing, with soft edges, preserved columns, native before/after previews and checked read dependencies. See the relative-brush section of the terraforming guide.
|
||||
|
||||
## Shared building guidance
|
||||
|
||||
`src/building-guidance.ts` is the shared source for material discovery, terrain art direction and decoration used by MCP server instructions and the ACP agent. It prioritizes focused material searches and reuse of defaults, deliberate silhouettes, calm walking areas, localized mountain detail, organic foundations, slope-aware materials, preservation and real camera verification. Tool descriptions state the exact schema boundaries: the current MCP terrain recipe offers amplitude/scale value noise and a limited palette; the advanced `shacraft-natural-v1` profile and initial water generation are operator-configured features. Ordinary building plans can place registered fluid states; terrain brushes still cannot scan through water.
|
||||
|
||||
ACP stores a fingerprint of successfully delivered instructions with its session state. An older or missing fingerprint causes the updated guidance to be sent on the next prompt in a resumed session, preserving history. Once delivered, unchanged guidance is not repeated on every turn or restart. Transport failures leave the old fingerprint so delivery can be retried.
|
||||
|
||||
@@ -8,6 +8,9 @@ import { fileURLToPath } from 'node:url';
|
||||
import { client, ndJsonStream, PROTOCOL_VERSION, type ClientConnection, type McpServer, type SessionNotification } from '@agentclientprotocol/sdk';
|
||||
|
||||
import { agentEnvironment, codexPaths, prepareCodexHome } from './security.js';
|
||||
import { BackendError } from './backend.js';
|
||||
import { AGENT_INSTRUCTIONS as INSTRUCTIONS } from './building-guidance.js';
|
||||
const INSTRUCTIONS_HASH = createHash('sha256').update(INSTRUCTIONS).digest('hex');
|
||||
export { agentEnvironment } from './security.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -20,9 +23,11 @@ export interface AcpOptions {
|
||||
timeoutMs?: number; startupTimeoutMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
interface SavedSession { sessionId: string; summary: string; interrupted: boolean }
|
||||
interface SavedSession { sessionId: string; summary: string; interrupted: boolean; instructionsHash?: string }
|
||||
|
||||
const INSTRUCTIONS = `You are the Minecraft builder for the current authorized project. Respond in the player's language using short game-chat messages. Use only minecraft-builder-mcp to inspect and edit the world. Begin each new task with project_context; read relevant world data before designing. Only implemented server capabilities may be used. Prepare compact geometry, inspect statistics, apply using stable idempotency keys, and poll operation_status. Preserve manual edits: conflict requires localized redesign or the user's decision, never blindly overwrite fresh snapshots. A cancelled or failed operation may have partial writes. Camera requests are asynchronous; poll capture_id and inspect actual image. If unavailable, explicitly say visually unverified. Never use console commands, shell, files, or other MCP servers to modify Minecraft. Do not claim any action completed without server evidence.`;
|
||||
|
||||
const MCP_STARTUP_MESSAGE = 'Minecraft MCP не запустился: инструменты строительства недоступны. Запрос остановлен; проверь настройки и процесс моста.';
|
||||
const MINECRAFT_TOOLS = new Set(['project_context', 'material_search', 'material_describe', 'region_inspect', 'build_prepare', 'build_apply', 'operation_status', 'operation_cancel', 'operation_undo_prepare', 'part_get', 'part_define', 'camera_list', 'camera_capture', 'asset_list', 'schematic_export', 'schematic_import_prepare', 'terrain_preview', 'terrain_prepare', 'terrain_brush_prepare']);
|
||||
|
||||
export class CodexSession implements AgentSession {
|
||||
private child?: ChildProcessWithoutNullStreams;
|
||||
@@ -33,12 +38,15 @@ export class CodexSession implements AgentSession {
|
||||
private cancelled = false;
|
||||
private buffer = '';
|
||||
private finalText = '';
|
||||
private lastSent = 0;
|
||||
private sentChars = 0;
|
||||
private delivery: Promise<void> = Promise.resolve();
|
||||
private firstPrompt = true;
|
||||
private instructionsHash = '';
|
||||
private summary = '';
|
||||
private reportReset = false;
|
||||
private reportInterrupted = false;
|
||||
private readonly mcpStartupFailures = new Set<string>();
|
||||
private readonly minecraftCalls = new Set<string>();
|
||||
private readonly dir: string;
|
||||
private readonly stateFile: string;
|
||||
constructor(private readonly message: Pick<ChatMessage, 'projectId' | 'playerId'>, private readonly options: AcpOptions) {
|
||||
@@ -58,19 +66,32 @@ export class CodexSession implements AgentSession {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') this.reportReset = true;
|
||||
}
|
||||
this.summary = saved?.summary.slice(0, 5000) ?? '';
|
||||
this.instructionsHash = saved?.instructionsHash ?? '';
|
||||
this.reportInterrupted = saved?.interrupted ?? false;
|
||||
const executable = this.options.command ?? process.execPath;
|
||||
const args = this.options.args ?? (this.options.command ? [] : [require.resolve('@agentclientprotocol/codex-acp')]);
|
||||
this.mcpStartupFailures.clear();
|
||||
const child = spawn(executable, args, { cwd: this.dir, stdio: ['pipe','pipe','pipe'], env: agentEnvironment(this.options.env ?? process.env, paths), shell: false, detached: process.platform !== 'win32' });
|
||||
this.child = child;
|
||||
// Adapter stderr may contain prompts or secrets. Drain it without logging raw content.
|
||||
child.stderr.resume();
|
||||
const app = client({ name: 'minecraft-builder-mcp-chat' });
|
||||
app.onRequest('session/request_permission', async () => {
|
||||
app.onRequest('session/request_permission', async ({ params }) => {
|
||||
// Only correlate a one-use approval to a known Minecraft call advertised
|
||||
// by this adapter in this active owner turn. Paper enforces the scope.
|
||||
if (this.child === child && this.active && !this.cancelled && !this.mcpStartupError() && params.sessionId === this.sessionId
|
||||
&& params._meta?.is_mcp_tool_approval === true
|
||||
&& this.minecraftCalls.has(params.toolCall.toolCallId)
|
||||
&& params.options.some(option => option.kind === 'allow_once' && option.optionId === 'allow_once')) {
|
||||
this.minecraftCalls.delete(params.toolCall.toolCallId);
|
||||
return { outcome: { outcome: 'selected', optionId: 'allow_once' } };
|
||||
}
|
||||
await this.currentReply?.('Codex запросил дополнительное разрешение. Оно отклонено: подтверждение через игровой чат пока не реализовано.', false, true);
|
||||
return { outcome: { outcome: 'cancelled' } };
|
||||
});
|
||||
app.onNotification('session/update', ({ params }) => this.onUpdate(params));
|
||||
app.onNotification('session/update', ({ params }) => {
|
||||
if (this.child === child) return this.onUpdate(params);
|
||||
});
|
||||
this.connection = app.connect(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout) as unknown as ReadableStream<Uint8Array>));
|
||||
const connection = this.connection;
|
||||
child.once('error', () => connection.close(new Error('Cannot start the ACP process. Check MCB_ACP_COMMAND.')));
|
||||
@@ -102,55 +123,114 @@ export class CodexSession implements AgentSession {
|
||||
this.sessionId = created.sessionId; configOptions = created.configOptions; this.firstPrompt = true;
|
||||
if (saved) this.reportReset = true;
|
||||
}
|
||||
const mcpError = this.mcpStartupError();
|
||||
if (mcpError) throw mcpError;
|
||||
if (this.options.model) {
|
||||
const config = configOptions?.find(option => option.category === 'model' || option.id === 'model');
|
||||
if (!config) throw new Error('Agent did not advertise a model selector; unset MCB_MODEL or use a compatible adapter.');
|
||||
await connection.agent.request('session/set_config_option', { sessionId: this.sessionId, configId: config.id, value: this.options.model });
|
||||
}
|
||||
await this.save(false);
|
||||
const lateMcpError = this.mcpStartupError();
|
||||
if (lateMcpError) throw lateMcpError;
|
||||
} catch (error) { this.close(); throw error; }
|
||||
finally { clearTimeout(setupTimeout); }
|
||||
}
|
||||
private mcpStartupError(): BackendError | undefined {
|
||||
return this.sessionId && this.mcpStartupFailures.has(this.sessionId)
|
||||
? new BackendError('mcp_unavailable', MCP_STARTUP_MESSAGE) : undefined;
|
||||
}
|
||||
private async onUpdate(params: SessionNotification): Promise<void> {
|
||||
// History replay from session/load is suppressed; another player's chat never receives it.
|
||||
if (!this.active || params.sessionId !== this.sessionId || !this.currentReply) return;
|
||||
const update = params.update;
|
||||
// codex-acp 1.11.0 synthesizes this reserved startup event separately from
|
||||
// thread history. It can arrive before session/new or session/load returns.
|
||||
// Never expose its raw content: startup errors may contain tokens or paths.
|
||||
if (update.sessionUpdate === 'tool_call' && update.toolCallId === 'mcp_startup.minecraft-builder-mcp'
|
||||
&& update.title === 'mcp__minecraft-builder-mcp__startup' && update.kind === 'other' && update.status === 'failed') {
|
||||
this.mcpStartupFailures.add(params.sessionId);
|
||||
const error = this.mcpStartupError();
|
||||
if (error && this.active) this.connection?.close(error);
|
||||
return;
|
||||
}
|
||||
// History replay from session/load is suppressed; another player's chat never receives it.
|
||||
if (!this.active || params.sessionId !== this.sessionId || !this.currentReply || this.mcpStartupError()) return;
|
||||
if (update.sessionUpdate === 'tool_call') {
|
||||
const input = update.rawInput as Record<string, unknown> | undefined;
|
||||
if (update._meta?.is_mcp_tool_call === true && update.kind === 'execute'
|
||||
&& (update.status === 'pending' || update.status === 'in_progress')
|
||||
&& input?.server === 'minecraft-builder-mcp' && typeof input.tool === 'string'
|
||||
&& MINECRAFT_TOOLS.has(input.tool) && update.title === `mcp.minecraft-builder-mcp.${input.tool}`
|
||||
&& this.minecraftCalls.size < 128) this.minecraftCalls.add(update.toolCallId);
|
||||
} else if (update.sessionUpdate === 'tool_call_update'
|
||||
&& (update.status === 'completed' || update.status === 'failed')) this.minecraftCalls.delete(update.toolCallId);
|
||||
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
||||
this.finalText = (this.finalText + update.content.text).slice(0, 8000);
|
||||
this.buffer += update.content.text;
|
||||
if (this.buffer.length >= 180 || Date.now() - this.lastSent >= 1500) await this.flush(false);
|
||||
const remaining = Math.max(0, 8000 - this.sentChars - this.buffer.length);
|
||||
this.buffer += update.content.text.slice(0, safeTextEnd(update.content.text, remaining));
|
||||
await this.flush(false);
|
||||
}
|
||||
// Deliberately do not forward thought chunks, tool arguments, or raw terminal output.
|
||||
}
|
||||
private async flush(done: boolean): Promise<void> {
|
||||
const remaining = Math.max(0, 8000 - this.sentChars);
|
||||
const clean = this.buffer.replace(/[\u0000-\u001f\u007f§]/g, ' ').trim().slice(0, remaining);
|
||||
this.buffer = '';
|
||||
if (clean) {
|
||||
for (let offset = 0; offset < clean.length; offset += 240) await this.currentReply?.(clean.slice(offset, offset + 240), false);
|
||||
const reply = this.currentReply;
|
||||
if (!reply) return;
|
||||
const { messages, remainder } = chatMessages(this.buffer, done);
|
||||
// Reserve text synchronously: ACP notifications can arrive while a reply is
|
||||
// awaiting HTTP delivery. They must not flush or account for the same text.
|
||||
this.buffer = remainder;
|
||||
for (const message of messages) {
|
||||
const clean = message.slice(0, safeTextEnd(message, Math.max(0, 8000 - this.sentChars)));
|
||||
if (!clean) continue;
|
||||
this.sentChars += clean.length;
|
||||
this.delivery = this.delivery.then(() => reply(clean, false));
|
||||
}
|
||||
this.lastSent = Date.now();
|
||||
if (done) await this.currentReply?.(this.cancelled ? 'Остановлено. Уже изменённые блоки остаются в истории.' : this.sentChars ? '' : 'Ход Codex завершён без текстового ответа; состояние мира доступно через /ai status.', true);
|
||||
if (done) {
|
||||
const status = this.cancelled ? 'Остановлено. Уже изменённые блоки остаются в истории.' : this.sentChars ? '' : 'Ход Codex завершён без текстового ответа; состояние мира доступно через /ai status.';
|
||||
this.delivery = this.delivery.then(() => reply(status, true));
|
||||
}
|
||||
await this.delivery;
|
||||
}
|
||||
async prompt(text: string, reply: Reply): Promise<void> {
|
||||
if (this.active) throw new Error('This ACP session already has an active turn.');
|
||||
this.currentReply = reply; this.cancelled = false;
|
||||
await this.start();
|
||||
try {
|
||||
await this.start();
|
||||
const mcpError = this.mcpStartupError();
|
||||
if (mcpError) throw mcpError;
|
||||
} catch (error) {
|
||||
if (error instanceof BackendError && error.code === 'mcp_unavailable') await reply(MCP_STARTUP_MESSAGE, false, true);
|
||||
this.currentReply = undefined;
|
||||
throw error;
|
||||
}
|
||||
if (this.cancelled) { await reply('Запрос остановлен до отправки Codex.', true); this.currentReply = undefined; return; }
|
||||
if (this.reportReset) { await reply('Начат новый диалог Codex с сохранённой краткой сводкой проекта.', false); this.reportReset = false; }
|
||||
if (this.reportInterrupted) { await reply('Предыдущий ход был прерван перезапуском. Проверю состояние операций перед новым строительством.', false); this.reportInterrupted = false; }
|
||||
this.active = true; this.buffer = ''; this.finalText = ''; this.sentChars = 0;
|
||||
const prefix = this.firstPrompt ? `${INSTRUCTIONS}\n${this.summary ? `Previous compact summary (historical, verify world): ${this.summary}\n` : ''}\nPlayer request:\n` : '';
|
||||
this.active = true; this.buffer = ''; this.finalText = ''; this.sentChars = 0; this.delivery = Promise.resolve(); this.minecraftCalls.clear();
|
||||
const prefix = (this.firstPrompt || this.instructionsHash !== INSTRUCTIONS_HASH) ? `${INSTRUCTIONS}\n${this.summary ? `Previous compact summary (historical, verify world): ${this.summary}\n` : ''}\nPlayer request:\n` : '';
|
||||
await this.save(true);
|
||||
const timeout = setTimeout(() => { void this.cancel().catch(() => this.close()); }, this.options.timeoutMs ?? 15 * 60_000);
|
||||
try {
|
||||
const startupError = this.mcpStartupError();
|
||||
if (startupError) throw startupError;
|
||||
const result = await this.connection!.agent.request('session/prompt', { sessionId: this.sessionId!, prompt: [{ type: 'text', text: `${prefix}${text}` }] });
|
||||
const mcpError = this.mcpStartupError();
|
||||
if (mcpError) throw mcpError;
|
||||
this.firstPrompt = false;
|
||||
this.instructionsHash = INSTRUCTIONS_HASH;
|
||||
this.cancelled ||= result.stopReason === 'cancelled';
|
||||
this.summary = `Last player request: ${text.slice(0,2000)}\nLast agent response: ${this.finalText.slice(0,3000)}`;
|
||||
await this.save(false);
|
||||
const lateMcpError = this.mcpStartupError();
|
||||
if (lateMcpError) throw lateMcpError;
|
||||
await this.flush(true);
|
||||
} catch (error) {
|
||||
const mcpError = this.mcpStartupError();
|
||||
if (mcpError) {
|
||||
await this.delivery;
|
||||
await reply(MCP_STARTUP_MESSAGE, false, true);
|
||||
throw mcpError;
|
||||
}
|
||||
throw error;
|
||||
} finally { clearTimeout(timeout); this.active = false; this.currentReply = undefined; }
|
||||
}
|
||||
async cancel(): Promise<void> {
|
||||
@@ -164,7 +244,7 @@ export class CodexSession implements AgentSession {
|
||||
}
|
||||
private async save(interrupted: boolean): Promise<void> {
|
||||
const temp = `${this.stateFile}.tmp`;
|
||||
await writeFile(temp, JSON.stringify({ sessionId: this.sessionId, summary: this.summary, interrupted }), { mode: 0o600 });
|
||||
await writeFile(temp, JSON.stringify({ sessionId: this.sessionId, summary: this.summary, interrupted, instructionsHash: this.instructionsHash }), { mode: 0o600 });
|
||||
await rename(temp, this.stateFile);
|
||||
}
|
||||
close(): void {
|
||||
@@ -178,6 +258,42 @@ export class CodexSession implements AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
/** Keep unfinished words until more tokens arrive; never emit a token on a timer. */
|
||||
function chatMessages(buffer: string, done: boolean): { messages: string[]; remainder: string } {
|
||||
let text = buffer.replace(/[\u0000-\u0009\u000b-\u001f\u007f§]/g, ' ').trimStart();
|
||||
const messages: string[] = [];
|
||||
while (text) {
|
||||
const limit = safeTextEnd(text, 240);
|
||||
// A following separator confirms the sentence boundary. A punctuation token
|
||||
// alone may still be followed by closing quotes or another punctuation mark.
|
||||
const sentence = /[.!?…]["'»”\])]*(?=\s)|\n/u.exec(text);
|
||||
let end = sentence ? sentence.index + sentence[0].length : 0;
|
||||
if (!end || end > limit) {
|
||||
if (text.length <= limit) {
|
||||
if (!done) break;
|
||||
end = text.length;
|
||||
} else {
|
||||
end = 0;
|
||||
for (let i = limit; i > 0; i--) {
|
||||
if (/\s/u.test(text[i]!)) { end = i; break; }
|
||||
}
|
||||
if (!end) end = limit; // A single overlong word still needs a bounded message.
|
||||
}
|
||||
}
|
||||
const message = text.slice(0, end).replace(/\s+/gu, ' ').trim();
|
||||
text = text.slice(end).trimStart();
|
||||
if (message) messages.push(message);
|
||||
}
|
||||
return { messages, remainder: text };
|
||||
}
|
||||
|
||||
function safeTextEnd(text: string, max: number): number {
|
||||
let end = Math.min(text.length, max);
|
||||
const last = text.charCodeAt(end - 1);
|
||||
if (end < text.length && last >= 0xd800 && last <= 0xdbff) end--;
|
||||
return end;
|
||||
}
|
||||
|
||||
function terminateAgentTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void {
|
||||
try {
|
||||
// The adapter launches Codex and MCP children. On Unix all are in our own process group.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Shared guidance for MCP clients and the in-game ACP agent. Keep capability claims exact. */
|
||||
export const MATERIAL_GUIDANCE = `Material discovery: project_context contains only a catalog summary. All vanilla block states registered by the running server are available to ordinary build recipes, including water; item-only materials can be discovered but cannot be placed as blocks. Use material_search with a focused query, then material_describe for 1–3 chosen materials to obtain exact default states and allowed property values. Reuse those results while the catalog version is unchanged; do not enumerate the registry or repeatedly describe known materials. Omitted properties use server defaults. Block states do not accept raw NBT or configure inventories/sign text; same-material edits preserve existing block-entity data. Pass through region_inspect snapshot_id hashes in expected_blocks to detect changes to that data. Terrain recipes and brushes retain their separate limited palettes and fluid restrictions.`;
|
||||
|
||||
export const TERRAIN_GUIDANCE = `Terrain art direction: compose a readable skyline and calm playable valleys before adding detail. Prefer broad low-frequency relief, localized ridged mountain detail and modest coordinate warping when the available generator supports them. Independent seeded noise layers reduce repetition; more octaves alone do not fix bad composition. Keep fine detail weak along walking routes. Preserve sightlines, water courses and space for architecture. Avoid large rectangular plateaus unless explicitly requested: adapt buildings to the landscape and flatten only their actual foundations with wide, irregular transitions. Preserve existing terrain instead of repeatedly flattening whole districts. Use rock on steep slopes and soil/grass on gentle ground; avoid grass-and-dirt contour stripes across cliffs. Grade routes and entrances separately; natural-looking terrain is not automatically walkable.
|
||||
Capability boundary: MCP terrain recipes currently expose three-octave value noise through amplitude/scale only. OpenSimplex2S, ridged noise and domain warping are available in the separately configured shacraft-natural-v1 initial-world profile, NOT as terrain_preview parameters or callable MCP tools. Do not invent noise, spline, erosion, biome or fluid APIs. MCP terrain tools do not place water; the initial-world generator can fill lakes/rivers at a configured level. Brushes reject water in their scan. Preserve waterways and their beds; request operator-side generation work if the task requires unsupported capabilities. There is no hydraulic erosion simulation or initial-generation block undo.
|
||||
Workflow: inspect a small relevant region, preserve structures and their foundations, preview composition, then apply bounded checked plans. A target heightmap is not a Minecraft screenshot. Inspect terrain from an overview and a player-height view when the camera is available; check silhouettes, chunk seams, shores, materials and route slopes. Poll pending captures by capture_id until completed or a reported error; report that error rather than claiming visual verification. On a shared camera client, ask the player to close menus and stay still. Distinguish proposed, prepared, server-applied and visually inspected work. Keep summaries compact; reuse terrain_id and operation IDs instead of dumping blocks or regenerating equivalent recipes.`;
|
||||
|
||||
export const DECORATION_GUIDANCE = `Decoration art direction: Establish purpose and silhouette before adding detail. Use a coherent palette, grouped texture variation and structural depth; avoid random high-contrast block noise. Preserve quiet surfaces, walking clearance, entrances and important sightlines. Build one small prototype, inspect it, then repeat its motifs with controlled variation. Discover selected materials and exact block states through material_search and material_describe. Verify orientation, attachment, support and stable leaves; decorative partial blocks do not guarantee containment. Keep unfinished rooms closed. Inspect actual captures from player height and an overview, in daylight and at night when available; state which views or lighting conditions remain unverified. Confirm server-applied changes before reporting completion. Documented decoration recipes are design specifications, not callable tools; translate selected details into supported bounded plans and preserve manual edits.`;
|
||||
|
||||
export const AGENT_BASE = `You are the Minecraft builder for the current authorized project. Respond in the player's language using short game-chat messages. Use only minecraft-builder-mcp to inspect and edit the world. Begin each new task with project_context; read relevant world data before designing. Only implemented server capabilities may be used. Prepare compact geometry, inspect statistics, apply using stable idempotency keys, and poll operation_status. Preserve manual edits: conflict requires localized redesign or the user's decision, never blindly overwrite fresh snapshots. A cancelled or failed operation may have partial writes. Camera requests are asynchronous; poll capture_id and inspect actual image. If unavailable, explicitly say visually unverified. Never use console commands, shell, files, or other MCP servers to modify Minecraft. Do not claim any action completed without server evidence.`;
|
||||
|
||||
export const AGENT_INSTRUCTIONS = `${AGENT_BASE}\n\n${MATERIAL_GUIDANCE}\n\n${TERRAIN_GUIDANCE}\n\n${DECORATION_GUIDANCE}`;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { mkdir, open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
||||
import { delimiter, dirname, join, resolve } from 'node:path';
|
||||
|
||||
// These are supported by the pinned Codex 0.153.4 runtime and official configuration reference.
|
||||
@@ -11,12 +12,82 @@ export const CODEX_CONFIG = {
|
||||
shell_tool: false, unified_exec: false, shell_snapshot: false,
|
||||
apps: false, hooks: false, multi_agent: false, plugins: false, remote_plugin: false,
|
||||
browser_use: false, browser_use_external: false, browser_use_full_cdp_access: false,
|
||||
computer_use: false, image_generation: false, code_mode: false, code_mode_host: false,
|
||||
// Models whose catalog tool_mode is code_mode_only need this host even with code_mode=false.
|
||||
// The host executes tool orchestration; it does not enable shell, apps, or other integrations.
|
||||
computer_use: false, image_generation: false, code_mode: false, code_mode_host: true,
|
||||
skill_mcp_dependency_install: false,
|
||||
},
|
||||
};
|
||||
export const CODEX_CONFIG_TOML = `# Managed by minecraft-builder-mcp; use a dedicated CODEX_HOME.\nsandbox_mode = "read-only"\napproval_policy = "on-request"\napprovals_reviewer = "user"\ncli_auth_credentials_store = "file"\nallow_login_shell = false\nweb_search = "disabled"\n\n[sandbox_workspace_write]\nnetwork_access = false\nwritable_roots = []\nexclude_slash_tmp = true\nexclude_tmpdir_env_var = true\n\n[shell_environment_policy]\ninherit = "none"\n\n[features]\n${Object.entries(CODEX_CONFIG.features).map(([key,value]) => `${key} = ${value}`).join('\n')}\n`;
|
||||
|
||||
// Frozen previous managed configuration: migration must never recognize arbitrary user settings.
|
||||
const PRE_CODE_MODE_HOST_CONFIG = `# Managed by minecraft-builder-mcp; use a dedicated CODEX_HOME.
|
||||
sandbox_mode = "read-only"
|
||||
approval_policy = "on-request"
|
||||
approvals_reviewer = "user"
|
||||
cli_auth_credentials_store = "file"
|
||||
allow_login_shell = false
|
||||
web_search = "disabled"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = false
|
||||
writable_roots = []
|
||||
exclude_slash_tmp = true
|
||||
exclude_tmpdir_env_var = true
|
||||
|
||||
[shell_environment_policy]
|
||||
inherit = "none"
|
||||
|
||||
[features]
|
||||
shell_tool = false
|
||||
unified_exec = false
|
||||
shell_snapshot = false
|
||||
apps = false
|
||||
hooks = false
|
||||
multi_agent = false
|
||||
plugins = false
|
||||
remote_plugin = false
|
||||
browser_use = false
|
||||
browser_use_external = false
|
||||
browser_use_full_cdp_access = false
|
||||
computer_use = false
|
||||
image_generation = false
|
||||
code_mode = false
|
||||
code_mode_host = false
|
||||
skill_mcp_dependency_install = false
|
||||
`;
|
||||
|
||||
function differentConfig(): Error {
|
||||
return new Error('MCB_CODEX_HOME contains a different config.toml. Choose a dedicated empty Codex home or the bridge-managed home; existing settings will not be overwritten.');
|
||||
}
|
||||
|
||||
async function migrateCodeModeHost(configPath: string): Promise<void> {
|
||||
const backupPath = `${configPath}.before-code-mode-host`;
|
||||
try {
|
||||
const backup = await open(backupPath, 'wx', 0o600);
|
||||
try { await backup.writeFile(PRE_CODE_MODE_HOST_CONFIG); await backup.sync(); }
|
||||
finally { await backup.close(); }
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
||||
if (await readFile(backupPath, 'utf8') !== PRE_CODE_MODE_HOST_CONFIG) {
|
||||
throw new Error('The managed Codex config backup already contains different settings; it will not be overwritten.');
|
||||
}
|
||||
}
|
||||
const temporaryPath = `${configPath}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
const temporary = await open(temporaryPath, 'wx', 0o600);
|
||||
try { await temporary.writeFile(CODEX_CONFIG_TOML); await temporary.sync(); }
|
||||
finally { await temporary.close(); }
|
||||
// Recheck after preparing the backup; do not replace settings edited during migration.
|
||||
const current = await readFile(configPath, 'utf8');
|
||||
if (current === CODEX_CONFIG_TOML) return;
|
||||
if (current !== PRE_CODE_MODE_HOST_CONFIG) throw differentConfig();
|
||||
await rename(temporaryPath, configPath);
|
||||
} finally {
|
||||
await unlink(temporaryPath).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodexPaths { home: string; codexHome: string }
|
||||
export function codexPaths(stateDir: string, codexHome?: string): CodexPaths {
|
||||
const state = resolve(stateDir);
|
||||
@@ -29,7 +100,8 @@ export async function prepareCodexHome(paths: CodexPaths): Promise<void> {
|
||||
try { existing = await readFile(configPath, 'utf8'); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
||||
if (existing !== undefined && existing !== CODEX_CONFIG_TOML) {
|
||||
throw new Error('MCB_CODEX_HOME contains a different config.toml. Choose a dedicated empty Codex home or the bridge-managed home; existing settings will not be overwritten.');
|
||||
if (existing !== PRE_CODE_MODE_HOST_CONFIG) throw differentConfig();
|
||||
await migrateCodeModeHost(configPath);
|
||||
}
|
||||
if (existing === undefined) {
|
||||
try { await writeFile(configPath, CODEX_CONFIG_TOML, { flag: 'wx', mode: 0o600 }); }
|
||||
@@ -62,14 +134,16 @@ export function securityReport(paths: CodexPaths) {
|
||||
configuredSandbox: 'read-only', adapterMode: 'read-only', adapterVersion: '1.11.0',
|
||||
adapterTurnSandbox: 'workspace-write', approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
||||
sandboxedCommandNetwork: false, shellToolRequested: false, unifiedExecRequested: false,
|
||||
codeModeHostRequested: true, codeModeHostRequiredForModelToolMode: 'code_mode_only',
|
||||
pinnedCliFeatureProbe: { codexVersion: '0.153.4', platform: 'linux', shell_tool: false, unified_exec: true },
|
||||
inheritedMcpServers: false, acpFilesystem: false, acpTerminal: false,
|
||||
runtimeVerified: false,
|
||||
limitations: [
|
||||
'Pinned Codex reports unified_exec enabled even when disabled explicitly; shell_tool is disabled. A real model tool-availability check has not run.',
|
||||
'Pinned Codex reports unified_exec enabled even when disabled explicitly; shell_tool is disabled. Doctor does not invoke a model to verify effective tool access; use an explicit end-to-end test.',
|
||||
'Models with tool_mode=code_mode_only require the bundled code-mode host even with features.code_mode=false; enabling that host does not enable the separately disabled integrations.',
|
||||
'codex-acp 1.11.0 overrides turn sandbox to workspace-write even in its read-only mode; the session directory and temporary paths may remain writable.',
|
||||
'Sandboxed-command network restrictions do not sandbox the bridge, ACP adapter or MCP server processes; these remain trusted local programs.',
|
||||
'No real model turn or end-to-end sandbox probe has run; configuration and initialization alone do not prove OS-level isolation.',
|
||||
'No end-to-end sandbox probe has run; authenticated text replies and configuration alone do not prove OS-level isolation.',
|
||||
'Administrator-managed Codex settings and repository-local configuration can also apply; use the dedicated project/state directory and inspect doctor output.',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const coordinate = z.number().int().min(-30_000_000).max(30_000_000);
|
||||
const point = z.object({ x: coordinate, z: coordinate }).strict();
|
||||
const position = z.object({ x: coordinate, y: z.number().int().min(-4096).max(4096), z: coordinate }).strict();
|
||||
const elevation = z.number().finite().min(-4096).max(4096);
|
||||
const falloff = z.number().finite().min(1).max(2048);
|
||||
const material = z.enum(['minecraft:stone','minecraft:andesite','minecraft:granite','minecraft:diorite','minecraft:deepslate',
|
||||
'minecraft:cobbled_deepslate','minecraft:dirt','minecraft:grass_block','minecraft:moss_block','minecraft:sandstone','minecraft:terracotta']);
|
||||
const feature = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('hill'), center: point, radius: z.number().min(1).max(2048), height: elevation, falloff }).strict(),
|
||||
z.object({ type: z.literal('basin'), center: point, radius: z.number().min(1).max(2048), height: elevation, falloff }).strict(),
|
||||
z.object({ type: z.literal('plateau'), min: point, max: point, height: elevation, falloff }).strict(),
|
||||
z.object({ type: z.literal('ridge'), points: z.array(point).min(2).max(32), width: z.number().min(1).max(1024), height: elevation, falloff }).strict(),
|
||||
z.object({ type: z.literal('channel'), points: z.array(point).min(2).max(32), width: z.number().min(1).max(1024), height: elevation, falloff }).strict(),
|
||||
z.object({ type: z.literal('terrace'), step: z.number().min(1).max(128), strength: z.number().min(0).max(1) }).strict(),
|
||||
]);
|
||||
export const terrainRecipe = z.object({
|
||||
version: z.literal(1), min: position, max: position, base_height: z.number().int().min(-4096).max(4096),
|
||||
seed: z.number().int().min(-2147483648).max(2147483647), mode: z.enum(['sculpt','fill','cut']),
|
||||
noise: z.object({ amplitude: z.number().min(0).max(256), scale: z.number().min(1).max(4096) }).strict(),
|
||||
palette: z.object({ rock: material, soil: material, surface: material, soil_depth: z.number().int().min(0).max(16) }).strict(),
|
||||
features: z.array(feature).max(64), preserve: z.array(z.object({ min: position, max: position }).strict()).max(64),
|
||||
}).strict();
|
||||
|
||||
|
||||
export const terrainBrush = z.object({
|
||||
min: position, max: position, center: point, radius: z.number().int().min(1).max(16),
|
||||
action: z.enum(['raise','lower','flatten','smooth']),
|
||||
amount: z.number().int().min(1).max(32).optional(), height: z.number().int().min(-4096).max(4096).optional(),
|
||||
strength: z.number().min(0).max(1).default(1), falloff: z.number().min(0).max(1).default(0.5),
|
||||
smooth_radius: z.number().int().min(1).max(3).optional(),
|
||||
preserve: z.array(z.object({min: position,max: position}).strict()).max(64).default([]),
|
||||
}).strict();
|
||||
@@ -1,11 +1,14 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { z } from 'zod';
|
||||
import { terrainRecipe, terrainBrush } from './terrain-schema.js';
|
||||
import { DECORATION_GUIDANCE, MATERIAL_GUIDANCE, TERRAIN_GUIDANCE } from './building-guidance.js';
|
||||
import { BackendError, type RpcBackend } from './backend.js';
|
||||
|
||||
const id = z.string().min(1).max(200);
|
||||
const position = z.object({ x: z.number().int().min(-30_000_000).max(30_000_000), y: z.number().int().min(-4096).max(4096), z: z.number().int().min(-30_000_000).max(30_000_000) }).strict();
|
||||
const block = z.string().regex(/^minecraft:[a-z0-9_]+(?:\[[a-z0-9_=,]+\])?$/).max(200);
|
||||
const block = z.string().regex(/^minecraft:[a-z0-9_]+(?:\[[a-z0-9_=,]+\])?$/).max(1024);
|
||||
const materialId = z.string().regex(/^minecraft:[a-z0-9_]+$/).max(128);
|
||||
// Deliberate small declarative language: the Paper compiler enforces all resource limits again.
|
||||
const shape = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('box'), min: position, max: position, block, hollow: z.boolean().optional() }).strict(),
|
||||
@@ -22,7 +25,7 @@ export function toolResult(result: unknown): CallToolResult {
|
||||
if (result && typeof result === 'object' && 'imageBase64' in result) {
|
||||
const { imageBase64, mimeType, ...rest } = result as Record<string, unknown>;
|
||||
if (rest.status !== 'completed' || typeof imageBase64 !== 'string' || imageBase64.length > 12_000_000 || !['image/png', 'image/jpeg'].includes(String(mimeType)) || !/^[A-Za-z0-9+/]*={0,2}$/.test(imageBase64)) {
|
||||
throw new BackendError('invalid_image', 'Camera returned an invalid or oversized capture.');
|
||||
throw new BackendError('invalid_image', 'Backend returned an invalid or oversized image.');
|
||||
}
|
||||
metadata = rest;
|
||||
content.push({ type: 'image', data: imageBase64, mimeType: String(mimeType) });
|
||||
@@ -35,7 +38,7 @@ export function toolResult(result: unknown): CallToolResult {
|
||||
|
||||
export function createMcpServer(backend: RpcBackend): McpServer {
|
||||
const server = new McpServer({ name: 'minecraft-builder-mcp', version: '0.1.0' }, { instructions:
|
||||
'Build only through these tools in the server-authorized project. Start with project_context and region_inspect. Prepare a compact recipe, inspect its summary, then apply with the returned plan ID/hash and a stable unique idempotency key. Conflicts preserve manual edits: never re-read and blindly overwrite them. Query operation_status until terminal; cancellation can leave partial edits. Camera unavailable means visually unverified. Never claim success from preparation alone.' });
|
||||
'Build only through these tools in the server-authorized project. Start with project_context and region_inspect. Prepare a compact recipe, inspect its summary, then apply with the returned plan ID/hash and a stable unique idempotency key. Conflicts preserve manual edits: never re-read and blindly overwrite them. Query operation_status until terminal; cancellation can leave partial edits. Camera unavailable means visually unverified. Never claim success from preparation alone. For terrain, terrain_preview caches one declarative recipe and returns a target heightmap, not a world capture. Reuse terrain_id with terrain_prepare tile_index; review each tile summary and apply through build_apply. Keep the recipe locally for restart recovery. Stop on conflicts; never regenerate a conflicting plan to force it through. Terrain tools do not place water.' + '\n\n' + MATERIAL_GUIDANCE + '\n\n' + TERRAIN_GUIDANCE + '\n\n' + DECORATION_GUIDANCE });
|
||||
function register(name: string, description: string, inputSchema: z.ZodRawShape, readOnly: boolean, idempotent = false) {
|
||||
server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: idempotent, openWorldHint: false } }, async (args) => {
|
||||
try { return toolResult(await backend.call(name, args as Record<string, unknown>)); }
|
||||
@@ -46,19 +49,24 @@ export function createMcpServer(backend: RpcBackend): McpServer {
|
||||
}
|
||||
});
|
||||
}
|
||||
register('project_context', 'Get authorized world, project, area, capabilities, supported blocks, and operation summaries. No full-world dump.', {}, true, true);
|
||||
register('region_inspect', 'Inspect a bounded inclusive region. Prefer summary; blocks detail is only for a small local area. Both min and max are required; inspect a small section of the project area.', { min: position, max: position, detail: z.enum(['summary', 'blocks']).default('summary') }, true, true);
|
||||
register('build_prepare', 'Prepare immutable geometry without changing the world. Use supported recipe operations from project_context. Returns plan_id, plan_hash and compact statistics.', { recipe, part_id: id.optional(), dependencies: z.array(position).max(512).optional() }, false);
|
||||
register('project_context', 'Get authorized world, project, area, capabilities, a compact material catalog summary, and operation summaries. No registry or full-world dump.', {}, true, true);
|
||||
register('material_search', 'Search the running server material catalog by name. Returns a bounded page of IDs and block/item flags, without block properties. Prefer a focused query; reuse catalog version and page only when needed. Item-only materials cannot be placed as blocks.', { query: z.string().max(96).optional(), kind: z.enum(['block', 'item', 'all']).default('block'), limit: z.number().int().min(1).max(32).default(16), cursor: z.string().min(1).max(100).optional() }, true, true);
|
||||
register('material_describe', 'Get one exact material ID, its default block state and allowed property values from the running server. Describe only 1–3 selected materials, then reuse their defaults; item-only materials have no placeable block state. This does not expose inventory, entity or NBT editing.', { id: materialId }, true, true);
|
||||
register('region_inspect', 'Inspect a bounded inclusive region. Prefer summary; blocks detail is only for a small local area and includes opaque snapshot_id hashes for block entities, without their NBT. Both min and max are required; inspect a small section of the project area.', { min: position, max: position, detail: z.enum(['summary', 'blocks']).default('summary') }, true, true);
|
||||
register('build_prepare', 'Prepare immutable geometry without changing the world. Use supported recipe operations from project_context. Returns plan_id, plan_hash and compact statistics. Optional expected_blocks must cover every desired position exactly once; preserve each returned snapshot_id to detect block-entity data changes. The server rejects a changed caller snapshot atomically before preparation. Same-material state edits preserve existing block-entity data; recipes do not configure inventories, sign text or raw NBT.', { recipe, part_id: id.optional(), dependencies: z.array(position).max(512).optional(), expected_blocks: z.array(z.object({ pos: position, state: block, snapshot_id: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict()).min(1).max(4096).optional() }, false);
|
||||
register('build_apply', 'Apply a prepared plan with compare-before-write protection. Reuse the SAME idempotency_key after uncertain transport outcome; query project_context to recover an unknown operation ID, then operation_status first.', { plan_id: id, plan_hash: id, idempotency_key: id }, false, true);
|
||||
register('operation_status', 'Get exact state, changed count, conflicts and completion. A terminal cancelled/conflict state can include partial writes.', { operation_id: id }, true, true);
|
||||
register('operation_cancel', 'Request cancellation before the next server batch. Already applied changes remain journaled.', { operation_id: id }, false, true);
|
||||
register('operation_undo_prepare', 'Prepare checked undo of a recorded operation. Manual edits after construction become conflicts. Apply returned undo plan with build_apply.', { operation_id: id }, false);
|
||||
register('part_get', 'Get named part metadata and protected status, without expanding every block.', { part_id: id }, true, true);
|
||||
register('part_define', 'Register an exact part mask from a completed operation. Server rejects unsupported membership or overlap.', { name: z.string().min(1).max(64), operation_id: id }, false);
|
||||
register('camera_list', 'List saved camera poses and whether a camera client is connected.', {}, true, true);
|
||||
register('camera_list', 'List saved camera poses and camera configuration. This does not prove that the observer is online or ready.', {}, true, true);
|
||||
register('camera_capture', 'Request a real capture by saved camera_id or pose. A pending response returns captureId; poll using capture_id. Only completed results contain an image. Camera unavailable is not a successful visual check.', { camera_id: id.optional(), pose: pose.optional(), after_operation_id: id.optional(), capture_id: id.optional() }, true);
|
||||
register('asset_list', 'List up to 64 local schematic assets with dimensions and metadata, optionally filtered by query. No network library or generated thumbnails. Place .schem files manually in the plugin data/schematics directory; asset IDs never accept arbitrary paths.', { query: z.string().max(64).optional() }, true, true);
|
||||
register('schematic_export', 'Export a dense inclusive region of at most 4096 supported blocks as a local Sponge v2 .schem asset. Optional origin is the clipboard anchor. Returns an asset ID and metadata; files remain in the plugin data/schematics directory.', { name: z.string().min(1).max(64).regex(/^[^\u0000-\u001f\u007f]+$/), min: position, max: position, origin: position.optional() }, false);
|
||||
register('schematic_import_prepare', 'Prepare a checked import of a local .schem asset at target, optionally rotating 0/90/180/270 degrees. Rejects entities, block entities and unsupported blocks. Returns a normal plan; inspect it and use build_apply to edit the world.', { asset_id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), target: position, rotation: z.union([z.literal(0),z.literal(90),z.literal(180),z.literal(270)]).default(0) }, false);
|
||||
register('schematic_export', 'Export a dense inclusive region of at most 4096 registered block states as a local Sponge v2 .schem asset. Rejects block entities because their extra data cannot yet be exported. Optional origin is the clipboard anchor. Returns an asset ID and metadata; files remain in the plugin data/schematics directory.', { name: z.string().min(1).max(64).regex(/^[^\u0000-\u001f\u007f]+$/), min: position, max: position, origin: position.optional() }, false);
|
||||
register('schematic_import_prepare', 'Prepare a checked import of a local .schem asset at target, optionally rotating 0/90/180/270 degrees. Accepts registered block states; rejects entity and block-entity NBT payloads. Returns a normal plan; inspect it and use build_apply to edit the world.', { asset_id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), target: position, rotation: z.union([z.literal(0),z.literal(90),z.literal(180),z.literal(270)]).default(0) }, false);
|
||||
register('terrain_preview', 'Preview and cache a deterministic terrain recipe without reading or editing the world. Returns a native heightmap image, terrain_id and tile_count. North (-Z) is up. Features run in order: hill/ridge add height; plateau blends to an absolute height; basin/channel only lower to an absolute height; terrace quantizes elevations. Radius/width is the flat core radius/half-width, falloff is the smooth outer bank. Noise is three-octave value noise with amplitude/scale only; no warp or ridged parameters. Large rectangular plateaus should be reserved for explicit architectural needs; prefer organic landforms and small blended foundations. Noise uses the seed and world coordinates. Preserve boxes omit writes. sculpt fills below and clears above the field; fill only targets below; cut only clears above. Save the recipe: cache holds 32 recipes until restart. Bounds are inclusive; large concepts can be previewed outside the current project.', { recipe: terrainRecipe, resolution: z.number().int().min(32).max(256).default(128) }, true, true);
|
||||
register('terrain_prepare', 'Prepare ONE bounded terrain tile from a previously previewed terrain_id. tile_index is zero-based; X advances first, then Z, then Y. Returns a normal immutable plan or status=empty; no world writes. Replaces only natural terrain/air; bedrock, structures and unsupported blocks reject the tile. Use preserve boxes for existing work, including natural-material builds. Apply with build_apply and poll operation_status; undo with operation_undo_prepare. Each tile uses current live contents and existing area/loaded-chunk/protected-part checks. Multi-tile changes are not atomic.', { terrain_id: z.string().regex(/^[a-f0-9]{64}$/), tile_index: z.number().int().min(0).max(134_217_727) }, false);
|
||||
register('terrain_brush_prepare', 'Read EXISTING terrain and prepare a relative brush with a native before/after/delta preview. No world edits until build_apply. action raise/lower requires amount; flatten requires absolute height; smooth averages the ORIGINAL snapshot using smooth_radius (1..3, default 1). strength 0..1 scales displacement; falloff 0..1 is the outer fraction of the radius (0=hard edge). min/max is the inclusive scan window: <=4096 cells, full circular footprint plus smoothing halo, terrain in every column, air above both old and new surfaces, enough depth for cutting. No structures/fluids in scan; smaller brushes for constrained sites. All scanned cells become checked dependencies. Preserve boxes skip an intersecting edited column. Reuses existing surface and subsoil; it does not automatically expose rock on cliffs. Use small local refinements, keep routes gentle and avoid flattening whole districts. Returns plan_state=prepared with normal plan ID/hash or plan_state=empty; review and apply, then poll status. Never reprepare to force a conflicting brush through.', { brush: terrainBrush }, false);
|
||||
return server;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { CodexSession, agentEnvironment } from '../dist/acp.js';
|
||||
import { longReply, overlongWord } from './fixtures/streamed-replies.mjs';
|
||||
async function fixture(t){
|
||||
const stateDir=await mkdtemp(join(tmpdir(),'mcb-acp-'));t.after(()=>rm(stateDir,{recursive:true,force:true}));
|
||||
const log=join(stateDir,'protocol.jsonl');
|
||||
@@ -12,6 +14,34 @@ async function fixture(t){
|
||||
const records=async()=> (await readFile(log,'utf8').catch(()=>'' )).trim().split('\n').filter(Boolean).map(JSON.parse);
|
||||
return {options,records};
|
||||
}
|
||||
test('updated terrain and material guidance reaches resumed ACP sessions once without resetting history',async t=>{
|
||||
const {options,records}=await fixture(t);
|
||||
let session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await session.prompt('first request',async()=>{});session.close();
|
||||
const key=createHash('sha256').update('p\0u').digest('hex').slice(0,32);
|
||||
const file=join(options.stateDir,key,'session.json');
|
||||
const saved=JSON.parse(await readFile(file,'utf8'));saved.instructionsHash='older-prompt';
|
||||
await writeFile(file,JSON.stringify(saved));
|
||||
session=new CodexSession({projectId:'p',playerId:'u'},options);
|
||||
await session.prompt('after update',async()=>{});
|
||||
await session.prompt('next request',async()=>{});session.close();
|
||||
session=new CodexSession({projectId:'p',playerId:'u'},options);
|
||||
await session.prompt('same guidance after restart',async()=>{});
|
||||
const calls=await records();const prompts=calls.filter(v=>v.method==='session/prompt').map(v=>v.params.prompt[0].text);
|
||||
assert.ok(prompts[1].includes('shacraft-natural-v1'));
|
||||
assert.ok(prompts[1].includes('Avoid large rectangular plateaus'));
|
||||
assert.ok(prompts[1].includes('NOT as terrain_preview parameters'));
|
||||
for(const prompt of [prompts[0],prompts[1]]) {
|
||||
assert.ok(prompt.includes('project_context contains only a catalog summary'));
|
||||
assert.ok(prompt.includes('material_search'));
|
||||
assert.ok(prompt.includes('material_describe for 1–3 chosen materials'));
|
||||
assert.ok(prompt.includes('do not enumerate the registry'));
|
||||
assert.ok(prompt.includes('item-only materials can be discovered but cannot be placed'));
|
||||
}
|
||||
assert.equal(prompts[2],'next request');assert.equal(prompts[3],'same guidance after restart');
|
||||
assert.equal(calls.filter(v=>v.method==='session/new').length,1);
|
||||
assert.equal(calls.filter(v=>v.method==='session/load').length,2);
|
||||
});
|
||||
test('ACP initializes, injects scoped MCP, preserves sessions, suppresses private replay and thoughts',async t=>{
|
||||
const {options,records}=await fixture(t);const messages=[];
|
||||
let session=new CodexSession({projectId:'project',playerId:'player'},options);t.after(()=>session.close());
|
||||
@@ -34,6 +64,38 @@ test('permission requests fail closed and explain in chat',async t=>{
|
||||
assert.ok(messages.some(text=>text.includes('отклонено')));
|
||||
assert.equal((await records()).find(item=>item.id==='approval').result.outcome.outcome,'cancelled');
|
||||
});
|
||||
test('MCP startup failure before session creation stops the prompt and redacts diagnostics',async t=>{
|
||||
const {options,records}=await fixture(t);options.args.push('mcp-fail-new');const messages=[];
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await assert.rejects(session.prompt('Must not run',async text=>messages.push(text)),error=>error.code==='mcp_unavailable');
|
||||
assert.ok(!(await records()).some(item=>item.method==='session/prompt'));
|
||||
assert.deepEqual(messages,['Minecraft MCP не запустился: инструменты строительства недоступны. Запрос остановлен; проверь настройки и процесс моста.']);
|
||||
assert.ok(!messages.join(' ').includes('agent-secret'));
|
||||
});
|
||||
test('MCP startup failure during load stops the new turn while historical failures stay private',async t=>{
|
||||
const {options,records}=await fixture(t);const messages=[];
|
||||
let session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await session.prompt('First turn',async()=>{});session.close();options.args.push('mcp-fail-load');
|
||||
session=new CodexSession({projectId:'p',playerId:'u'},options);
|
||||
await assert.rejects(session.prompt('Must not run',async text=>messages.push(text)),error=>error.code==='mcp_unavailable');
|
||||
assert.equal((await records()).filter(item=>item.method==='session/prompt').length,1);
|
||||
assert.ok(messages.some(text=>text.includes('Minecraft MCP не запустился')));
|
||||
assert.ok(!messages.join(' ').includes('PRIVATE'));
|
||||
});
|
||||
test('MCP startup failures from another session do not block the selected session',async t=>{
|
||||
const {options}=await fixture(t);options.args.push('mcp-fail-other-session');const messages=[];
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await session.prompt('Continue selected session',async text=>messages.push(text));
|
||||
assert.ok(messages.some(text=>text.includes('Built turn 1')));
|
||||
assert.ok(!messages.some(text=>text.includes('MCP не запустился')));
|
||||
});
|
||||
test('an asynchronous MCP startup failure interrupts an active turn with a safe explanation',async t=>{
|
||||
const {options}=await fixture(t);const messages=[];
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await assert.rejects(session.prompt('mcp-fail-active',async(text,done)=>messages.push({text,done})),error=>error.code==='mcp_unavailable');
|
||||
assert.ok(messages.some(item=>item.text.includes('Minecraft MCP не запустился')));
|
||||
assert.ok(!messages.some(item=>item.done || item.text.includes('agent-secret')));
|
||||
});
|
||||
test('ACP cancel completes active prompt without ending the whole daemon',async t=>{
|
||||
const {options,records}=await fixture(t);const messages=[];
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
@@ -52,3 +114,69 @@ test('cancel during startup does not send a prompt after initialization complete
|
||||
const work=session.prompt('must-not-send',async()=>{});await session.cancel();await work;
|
||||
assert.ok(!(await records()).some(item=>item.method==='session/prompt'));
|
||||
});
|
||||
|
||||
test('delayed token streams keep words intact and flush the next turn independently',async t=>{
|
||||
const {options}=await fixture(t);
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
const first=[];
|
||||
await session.prompt('stream-greeting',async(text,done)=>first.push({text,done}));
|
||||
const visible=items=>items.filter(item=>item.text).map(item=>item.text).join(' ').replace(/\s+/g,' ').trim();
|
||||
assert.equal(visible(first),'Привет! Что построим?','ACP token boundaries must not become game-chat word boundaries');
|
||||
assert.equal(first.at(-1).done,true);
|
||||
assert.equal(first.filter(item=>item.done).length,1);
|
||||
await pause(1600);
|
||||
const second=[];
|
||||
await session.prompt('stream-follow-up',async(text,done)=>second.push({text,done}));
|
||||
assert.equal(visible(second),'Понятно. Проверяю освещение','a new prompt resets buffering and completion flushes an unfinished sentence');
|
||||
assert.equal(second.at(-1).done,true);
|
||||
assert.equal(second.filter(item=>item.done).length,1);
|
||||
});
|
||||
|
||||
test('slow chat delivery preserves streamed text order and completes after the final reply',async t=>{
|
||||
const {options}=await fixture(t);
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
const messages=[];let inFlight=0;let maxInFlight=0;let calls=0;
|
||||
await session.prompt('stream-slow-delivery',async(text,done)=>{
|
||||
const call=++calls;
|
||||
maxInFlight=Math.max(maxInFlight,++inFlight);
|
||||
// A slower first HTTP reply must not let later chunks or the done marker overtake it.
|
||||
await pause(call===1 ? 100 : 3);
|
||||
messages.push({text,done});
|
||||
inFlight--;
|
||||
});
|
||||
assert.equal(inFlight,0,'prompt completion must await all chat replies');
|
||||
assert.equal(maxInFlight,1,'chat replies must be serialized');
|
||||
assert.equal(messages.at(-1).done,true);
|
||||
assert.equal(messages.filter(item=>item.done).length,1);
|
||||
const visible=messages.filter(item=>item.text).map(item=>item.text);
|
||||
assert.ok(visible.length>1,'long replies need multiple Minecraft chat messages');
|
||||
assert.ok(visible.every(text=>text.length<=240),'chat messages must respect the UTF-16 length bound');
|
||||
assert.ok(visible.every(text=>text.isWellFormed()),'chunking must not split a surrogate pair');
|
||||
assert.equal(visible.join(' ').replace(/\s+/g,' ').trim(),longReply,'visible text must retain every word exactly once and in order');
|
||||
});
|
||||
|
||||
test('a word longer than one chat message is bounded without splitting emoji',async t=>{
|
||||
const {options}=await fixture(t);
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
const messages=[];
|
||||
await session.prompt('stream-overlong-word',async(text,done)=>messages.push({text,done}));
|
||||
const visible=messages.filter(item=>item.text).map(item=>item.text);
|
||||
assert.ok(visible.length>1);
|
||||
assert.ok(visible.every(text=>text.length<=240 && text.isWellFormed()));
|
||||
assert.equal(visible.join(''),overlongWord,'an unavoidable long-word split must preserve all Unicode text');
|
||||
assert.equal(messages.at(-1).done,true);
|
||||
});
|
||||
|
||||
|
||||
test('only correlated Minecraft tools receive one-use approval in the active session',async t=>{
|
||||
for(const mode of ['valid','material-search','material-describe','terrain-preview','terrain-prepare','terrain-brush','foreign','unknown','wrong-session','uncorrelated','completed','no-meta','persistent']) {
|
||||
await t.test(mode,async t=>{
|
||||
const {options,records}=await fixture(t);const messages=[];
|
||||
const session=new CodexSession({projectId:'p',playerId:'u'},options);t.after(()=>session.close());
|
||||
await session.prompt('mcp-approval-'+mode,async text=>messages.push(text));
|
||||
const response=(await records()).find(item=>item.id==='approval').result.outcome;
|
||||
assert.deepEqual(response,['valid','material-search','material-describe','terrain-preview','terrain-prepare','terrain-brush'].includes(mode)?{outcome:'selected',optionId:'allow_once'}:{outcome:'cancelled'});
|
||||
assert.equal(messages.some(text=>text.includes('отклонено')),!['valid','material-search','material-describe','terrain-preview','terrain-prepare','terrain-brush'].includes(mode));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
import { createInterface } from 'node:readline';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { greetingChunks, followUpChunks, longReply, overlongWord, tokenChunks } from './streamed-replies.mjs';
|
||||
const send = value => process.stdout.write(JSON.stringify({jsonrpc:'2.0',...value})+'\n');
|
||||
const startupFailure = (sessionId = 'session-one') => send({method:'session/update',params:{sessionId,update:{sessionUpdate:'tool_call',toolCallId:'mcp_startup.minecraft-builder-mcp',title:'mcp__minecraft-builder-mcp__startup',kind:'other',status:'failed',content:[{type:'content',content:{type:'text',text:'PRIVATE STARTUP DETAIL agent-secret'}}]}}});
|
||||
let promptId;let cancelled=false;let turn=0;
|
||||
for await (const line of createInterface({input:process.stdin})) {
|
||||
const msg=JSON.parse(line);
|
||||
if(process.argv[2]) appendFileSync(process.argv[2],JSON.stringify(msg)+'\n');
|
||||
if(msg.method==='initialize') send({id:msg.id,result:{protocolVersion:1,agentCapabilities:{loadSession:true},authMethods:[]}});
|
||||
else if(msg.method==='session/new') send({id:msg.id,result:{sessionId:'session-one'}});
|
||||
else if(msg.method==='session/new') {
|
||||
if(process.argv[3]==='mcp-fail-new') startupFailure();
|
||||
if(process.argv[3]==='mcp-fail-other-session') startupFailure('unrelated-session');
|
||||
send({id:msg.id,result:{sessionId:'session-one'}});
|
||||
}
|
||||
else if(msg.method==='session/load') {
|
||||
send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:'PRIVATE HISTORY'}}}});
|
||||
send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'tool_call',toolCallId:'old-mcp-tool-call',title:'mcp__minecraft-builder-mcp__project_context',kind:'other',status:'failed',content:[{type:'content',content:{type:'text',text:'PRIVATE HISTORY FAILED TOOL'}}]}}});
|
||||
if(process.argv[3]==='mcp-fail-load') startupFailure();
|
||||
send({id:msg.id,result:{}});
|
||||
}
|
||||
else if(msg.method==='session/prompt') {
|
||||
turn++;promptId=msg.id;
|
||||
const content=msg.params.prompt[0].text;
|
||||
if(content.includes('mcp-fail-active')) {startupFailure();continue;}
|
||||
if(content.includes('wait-for-cancel')) continue;
|
||||
if(content.includes('mcp-approval-')) {
|
||||
const mode=content.match(/mcp-approval-([a-z-]+)/)?.[1];
|
||||
const server=mode==='foreign'?'other-server':'minecraft-builder-mcp';
|
||||
const tool=mode==='unknown'?'run_shell':mode==='material-search'?'material_search':mode==='material-describe'?'material_describe':mode==='terrain-preview'?'terrain_preview':mode==='terrain-prepare'?'terrain_prepare':mode==='terrain-brush'?'terrain_brush_prepare':'build_prepare';
|
||||
const sid=mode==='wrong-session'?'other-session':'session-one';
|
||||
if(mode!=='uncorrelated') send({method:'session/update',params:{sessionId:sid,update:{sessionUpdate:'tool_call',toolCallId:'mc-call',kind:'execute',title:`mcp.${server}.${tool}`,status:'in_progress',rawInput:{server,tool,arguments:{}},_meta:{is_mcp_tool_call:true}}}});
|
||||
if(mode==='completed') send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'tool_call_update',toolCallId:'mc-call',status:'completed'}}});
|
||||
send({id:'approval',method:'session/request_permission',params:{sessionId:'session-one',toolCall:{toolCallId:'mc-call',kind:'execute',status:'pending'},...(mode==='no-meta'?{}:{_meta:{is_mcp_tool_approval:true}}),options:[{optionId:mode==='persistent'?'allow_always':'allow_once',name:'Allow',kind:mode==='persistent'?'allow_always':'allow_once'}]}});continue;
|
||||
}
|
||||
if(content.includes('request-permission')) {send({id:'approval',method:'session/request_permission',params:{sessionId:'session-one',toolCall:{toolCallId:'dangerous',title:'Permission',kind:'execute'},options:[{optionId:'yes',name:'Allow',kind:'allow_once'}]}});continue;}
|
||||
if(content.includes('stream-greeting') || content.includes('stream-follow-up') || content.includes('stream-slow-delivery') || content.includes('stream-overlong-word')) {
|
||||
const greeting = content.includes('stream-greeting');
|
||||
const chunks = greeting ? greetingChunks : content.includes('stream-follow-up') ? followUpChunks : tokenChunks(content.includes('stream-overlong-word') ? overlongWord : longReply);
|
||||
// Real models can think for seconds before emitting their first incomplete token.
|
||||
if(greeting) await pause(1600);
|
||||
for(const text of chunks) {
|
||||
send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text}}}});
|
||||
await pause(greeting ? 12 : 1);
|
||||
}
|
||||
send({id:msg.id,result:{stopReason:'end_turn'}});
|
||||
continue;
|
||||
}
|
||||
send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'agent_thought_chunk',content:{type:'text',text:'SECRET THOUGHT'}}}});
|
||||
send({method:'session/update',params:{sessionId:'session-one',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:`Built turn ${turn}.`}}}});
|
||||
send({id:msg.id,result:{stopReason:'end_turn'}});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const greetingChunks = ['Пр', 'ивет! ', 'Что пост', 'роим?'];
|
||||
export const followUpChunks = ['По', 'нятно. ', 'Проверяю ', 'осве', 'щение'];
|
||||
export const longReply = 'Осматриваю зал. ' + Array.from({ length: 28 }, (_, i) =>
|
||||
`Арка${i + 1} цела, фонарь${i + 1} 🏮 установлен ровно`
|
||||
).join(', ') + '. Проверка завершена';
|
||||
export const overlongWord = 'А'.repeat(239) + '🏮'.repeat(150) + 'конец';
|
||||
|
||||
export function tokenChunks(text) {
|
||||
const sizes = [2, 3, 7, 4, 11];
|
||||
const chunks = [];
|
||||
for (let offset = 0, i = 0; offset < text.length; i++) {
|
||||
const end = Math.min(text.length, offset + sizes[i % sizes.length]);
|
||||
chunks.push(text.slice(offset, end));
|
||||
offset = end;
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Opt-in isolated Paper test: relative edits, native preview, halo conflicts, full restoration.
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
const client=new Client({name:'brush-live-test',version:'1'});
|
||||
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/mcp.js')],env:Object.fromEntries(Object.entries(process.env).filter(([k,v])=>v!==undefined&&k!=='MCB_TOKEN')),stderr:'pipe'});
|
||||
async function call(name,args={},allowError=false){for(let i=0;i<100;i++){
|
||||
const result=await client.callTool({name,arguments:args});const data=JSON.parse(result.content.find(c=>c.type==='text').text);
|
||||
if(result.isError&&data.code==='busy'){await pause(100);continue;}
|
||||
if(result.isError&&!allowError)throw new Error(`${name}: ${data.code}: ${data.message}`);
|
||||
return {data,result};
|
||||
}throw new Error('Still busy');}
|
||||
async function done(id){for(let i=0;i<1000;i++){const {data}=await call('operation_status',{operation_id:id});if(['applied','conflict','cancelled','failed','recovery_required'].includes(data.status))return data;await pause(50);}throw new Error(`Still running: ${id}`);}
|
||||
async function apply(plan){const args={plan_id:plan.plan_id,plan_hash:plan.plan_hash,idempotency_key:'brush-test-'+randomUUID()};const {data}=await call('build_apply',args);return await done(data.operation_id);}
|
||||
async function undo(id){const {data}=await call('operation_undo_prepare',{operation_id:id});const result=await apply(data);assert.equal(result.status,'applied');}
|
||||
const min={x:-4,y:80,z:-4},max={x:4,y:92,z:4};
|
||||
async function air(){const {data}=await call('region_inspect',{min,max,detail:'summary'});assert.deepEqual(data.palette,{'minecraft:air':1053});}
|
||||
async function surface(){const {data}=await call('region_inspect',{min:{x:0,y:80,z:0},max:{x:0,y:92,z:0},detail:'blocks'});return Math.max(...data.blocks.filter(b=>b.state!=='minecraft:air').map(b=>b.pos.y));}
|
||||
const brush={min,max,center:{x:0,z:0},radius:3,strength:1,falloff:0.5};
|
||||
let base;
|
||||
try{
|
||||
await client.connect(transport);
|
||||
assert.ok((await call('project_context')).data.capabilities.includes('terrain_brush_prepare'));await air();
|
||||
const {data:basePlan}=await call('build_prepare',{recipe:{version:1,operations:[
|
||||
{type:'box',min,max:{x:4,y:83,z:4},block:'minecraft:stone'},
|
||||
{type:'box',min:{x:-4,y:84,z:-4},max:{x:4,y:84,z:4},block:'minecraft:grass_block'},
|
||||
]}});base=await apply(basePlan);assert.equal(base.status,'applied');assert.equal(await surface(),84);
|
||||
for(const [action,extra,expected] of [['raise',{amount:3},87],['lower',{amount:2},82],['flatten',{height:86},86]]){
|
||||
const {data:plan,result}=await call('terrain_brush_prepare',{brush:{...brush,action,...extra}});
|
||||
assert.equal(plan.plan_state,'prepared');assert.equal(plan.dependency_blocks,1053);assert.equal(plan.world_edited,false);assert.equal(await surface(),84);
|
||||
const png=result.content.find(c=>c.type==='image');assert.ok(png);
|
||||
if(action==='raise')await writeFile('../docs/references/terrain-brush-live-preview.png',Buffer.from(png.data,'base64'));
|
||||
const applied=await apply(plan);assert.equal(applied.status,'applied');assert.equal(await surface(),expected);
|
||||
await undo(applied.operation_id);assert.equal(await surface(),84);
|
||||
console.log(JSON.stringify({action,status:'passed',written:applied.written,center_after:expected,undo_height:84}));
|
||||
}
|
||||
const {data:spikePlan}=await call('terrain_brush_prepare',{brush:{...brush,radius:1,action:'raise',amount:4,falloff:0}});const spike=await apply(spikePlan);assert.equal(spike.status,'applied');
|
||||
const {data:smoothPlan}=await call('terrain_brush_prepare',{brush:{...brush,action:'smooth',smooth_radius:1}});const smooth=await apply(smoothPlan);assert.equal(smooth.status,'applied');assert.ok(await surface()<88);await undo(smooth.operation_id);assert.equal(await surface(),88);
|
||||
const spikeMin={x:-1,y:84,z:-1},spikeMax={x:1,y:88,z:1};
|
||||
const spikeSnapshot=(await call('region_inspect',{min:spikeMin,max:spikeMax,detail:'blocks'})).data.blocks;
|
||||
for(const b of spikeSnapshot){const inside=b.pos.x*b.pos.x+b.pos.z*b.pos.z<=1;assert.equal(b.state,inside?(b.pos.y===88?'minecraft:grass_block[snowy=false]':'minecraft:stone'):(b.pos.y===84?'minecraft:grass_block[snowy=false]':'minecraft:air'));}
|
||||
const {data:removeSpike}=await call('build_prepare',{recipe:{version:1,operations:[{type:'box',min:{x:-1,y:85,z:-1},max:spikeMax,block:'minecraft:air'},{type:'box',min:spikeMin,max:{x:1,y:84,z:1},block:'minecraft:grass_block'}]}});
|
||||
assert.equal((await apply(removeSpike)).status,'applied');assert.equal(await surface(),84);
|
||||
console.log(JSON.stringify({action:'smooth',status:'passed',written:smooth.written}));
|
||||
const {data:stale}=await call('terrain_brush_prepare',{brush:{...brush,action:'raise',amount:2}});
|
||||
const point={x:4,y:86,z:0};const {data:foreignPlan}=await call('build_prepare',{recipe:{version:1,operations:[{type:'box',min:point,max:point,block:'minecraft:gold_block'}]}});const foreign=await apply(foreignPlan);assert.equal(foreign.status,'applied');
|
||||
const conflict=await apply(stale);assert.equal(conflict.status,'conflict');assert.equal(conflict.written,0);
|
||||
const denied=await call('terrain_brush_prepare',{brush:{...brush,action:'raise',amount:2}},true);assert.ok(denied.result.isError);await undo(foreign.operation_id);
|
||||
const {data:empty}=await call('terrain_brush_prepare',{brush:{...brush,action:'flatten',height:84}});assert.equal(empty.plan_state,'empty');assert.ok(!empty.plan_id);
|
||||
// Base ownership has been superseded by checked brush undos. Verify every fixture cell,
|
||||
// then prepare a fresh checked cleanup of this isolated fixture; never force the old undo.
|
||||
const baseMax={x:4,y:84,z:4};
|
||||
const restored=(await call('region_inspect',{min,max:baseMax,detail:'blocks'})).data.blocks;
|
||||
assert.equal(restored.length,405);for(const b of restored)assert.equal(b.state,b.pos.y===84?'minecraft:grass_block[snowy=false]':'minecraft:stone');
|
||||
const {data:cleanup}=await call('build_prepare',{recipe:{version:1,operations:[{type:'box',min,max:baseMax,block:'minecraft:air'}]}});
|
||||
assert.equal((await apply(cleanup)).status,'applied');base=undefined;await air();console.log('LIVE BRUSH MCP PASSED; full fixture restored to air');
|
||||
}finally{if(base)console.error(`Fixture needs inspection; base operation: ${base.operation_id}`);await client.close();}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Opt-in live test in an isolated all-air fixture. No credentials are printed.
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { resolve } from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||
const client=new Client({name:'terrain-live-test',version:'1'});
|
||||
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/mcp.js')],env:Object.fromEntries(Object.entries(process.env).filter(([k,v])=>v!==undefined&&k!=='MCB_TOKEN')),stderr:'pipe'});
|
||||
async function call(name,args={},allowError=false){for(let i=0;i<100;i++){
|
||||
const result=await client.callTool({name,arguments:args});const data=JSON.parse(result.content.find(c=>c.type==='text').text);
|
||||
if(result.isError&&data.code==='busy'){await pause(100);continue;}
|
||||
if(result.isError&&!allowError)throw new Error(`${name}: ${data.code}: ${data.message}`);
|
||||
return {data,result};
|
||||
}throw new Error('Still busy');}
|
||||
async function done(id){for(let i=0;i<1000;i++){const {data}=await call('operation_status',{operation_id:id});if(['applied','conflict','cancelled','failed','recovery_required'].includes(data.status))return data;await pause(50);}throw new Error(`Still running: ${id}`);}
|
||||
async function apply(plan){const args={plan_id:plan.plan_id,plan_hash:plan.plan_hash,idempotency_key:'terrain-test-'+randomUUID()};const {data}=await call('build_apply',args);const result=await done(data.operation_id);return {args,result};}
|
||||
async function undo(id){const {data}=await call('operation_undo_prepare',{operation_id:id});const {result}=await apply(data);assert.equal(result.status,'applied');return result;}
|
||||
const recipe=JSON.parse(await readFile('../examples/terrain/small-hill.json','utf8'));
|
||||
const {min,max}=recipe;
|
||||
const inspect=async()=> (await call('region_inspect',{min,max,detail:'blocks'})).data.blocks;
|
||||
const air=blocks=>assert.ok(blocks.every(b=>b.state==='minecraft:air'));
|
||||
try{
|
||||
await client.connect(transport);
|
||||
const {data:context}=await call('project_context');assert.ok(context.capabilities.includes('terrain_prepare'));
|
||||
air(await inspect());
|
||||
const {data:preview,result:previewResult}=await call('terrain_preview',{recipe});
|
||||
assert.equal(preview.kind,'terrain_heightmap_preview');assert.equal(preview.world_verified,false);assert.equal(preview.tile_count,1);
|
||||
const png=previewResult.content.find(c=>c.type==='image');assert.ok(png);assert.equal(Buffer.from(png.data,'base64').subarray(1,4).toString(),'PNG');
|
||||
air(await inspect());
|
||||
const {data:plan}=await call('terrain_prepare',{terrain_id:preview.terrain_id,tile_index:0});air(await inspect());assert.ok(plan.changed_blocks>0);
|
||||
const {args,result}=await apply(plan);assert.equal(result.status,'applied');
|
||||
assert.equal((await call('build_apply',args)).data.operation_id,result.operation_id);
|
||||
const built=await inspect();assert.ok(built.some(b=>b.state.startsWith('minecraft:grass_block')));assert.ok(built.some(b=>b.state==='minecraft:dirt'));
|
||||
assert.equal(built.filter(b=>b.state!=='minecraft:air').length,result.written);
|
||||
assert.equal((await call('terrain_prepare',{terrain_id:preview.terrain_id,tile_index:0})).data.status,'empty');
|
||||
await undo(result.operation_id);air(await inspect());
|
||||
console.log(JSON.stringify({check:'terrain-native-preview-prepare-apply-idempotency-undo',status:'passed',written:result.written}));
|
||||
const {data:stale}=await call('terrain_prepare',{terrain_id:preview.terrain_id,tile_index:0});
|
||||
const {data:goldPlan}=await call('build_prepare',{recipe:{version:1,operations:[{type:'box',min,max:min,block:'minecraft:gold_block'}]}});
|
||||
const {result:gold}=await apply(goldPlan);assert.equal(gold.status,'applied');
|
||||
const denied=await call('terrain_prepare',{terrain_id:preview.terrain_id,tile_index:0},true);assert.equal(denied.data.code,'protected_terrain');
|
||||
const {result:conflict}=await apply(stale);assert.equal(conflict.status,'conflict');assert.equal(conflict.written,0);
|
||||
const masked=structuredClone(recipe);masked.preserve=[{min,max:min}];
|
||||
const {data:maskedPreview}=await call('terrain_preview',{recipe:masked});
|
||||
const {data:maskedPlan}=await call('terrain_prepare',{terrain_id:maskedPreview.terrain_id,tile_index:0});
|
||||
const {result:maskedDone}=await apply(maskedPlan);assert.equal(maskedDone.status,'applied');
|
||||
assert.equal((await call('region_inspect',{min,max:min,detail:'blocks'})).data.blocks[0].state,'minecraft:gold_block');
|
||||
await undo(maskedDone.operation_id);await undo(gold.operation_id);air(await inspect());
|
||||
console.log(JSON.stringify({check:'protected-existing-build-preserve-mask-and-stale-plan-conflict',status:'passed'}));
|
||||
const empty=structuredClone(recipe);empty.preserve=[{min,max}];
|
||||
const {data:emptyPreview}=await call('terrain_preview',{recipe:empty});assert.equal((await call('terrain_prepare',{terrain_id:emptyPreview.terrain_id,tile_index:0})).data.status,'empty');
|
||||
const outside=structuredClone(recipe);outside.min.x=200;outside.max.x=207;
|
||||
const {data:outsidePreview}=await call('terrain_preview',{recipe:outside});assert.equal((await call('terrain_prepare',{terrain_id:outsidePreview.terrain_id,tile_index:0},true)).data.code,'out_of_bounds');
|
||||
assert.equal((await call('terrain_prepare',{terrain_id:'0'.repeat(64),tile_index:0},true)).data.code,'not_found');
|
||||
air(await inspect());console.log('LIVE TERRAIN MCP PASSED; test area restored to air');
|
||||
}finally{await client.close();}
|
||||
@@ -19,7 +19,9 @@ test('real stdio MCP lists tools, validates recipes, calls backend and returns c
|
||||
const client=new Client({name:'test',version:'1'});
|
||||
t.after(async()=>{await client.close();backend.closeAllConnections();backend.close();});
|
||||
await client.connect(transport);
|
||||
const list=await client.listTools();assert.equal(list.tools.length,14);
|
||||
assert.ok(client.getInstructions().includes('Avoid large rectangular plateaus'));
|
||||
assert.ok(client.getInstructions().includes('NOT as terrain_preview parameters'));
|
||||
const list=await client.listTools();assert.equal(list.tools.length,19);
|
||||
assert.ok(list.tools.some(tool=>tool.name==='schematic_import_prepare'));assert.ok(!list.tools.some(tool=>tool.name==='chat_poll'));
|
||||
const context=await client.callTool({name:'project_context',arguments:{player_id:'forged'}});
|
||||
assert.equal(JSON.parse(context.content[0].text).project_id,'project');
|
||||
@@ -35,6 +37,75 @@ test('real stdio MCP lists tools, validates recipes, calls backend and returns c
|
||||
const invalidRotation=await client.callTool({name:'schematic_import_prepare',arguments:{asset_id:'asset',target:{x:0,y:64,z:0},rotation:45}});assert.equal(invalidRotation.isError,true);assert.equal(seen.length,beforeAssets);
|
||||
await client.callTool({name:'schematic_import_prepare',arguments:{asset_id:'asset',target:{x:0,y:64,z:0},rotation:90}});assert.equal(seen.at(-1).params.rotation,90);
|
||||
await client.callTool({name:'asset_list',arguments:{query:'tower'}});assert.equal(seen.at(-1).params.query,'tower');
|
||||
const recipe={version:1,min:{x:0,y:0,z:0},max:{x:31,y:31,z:31},base_height:8,seed:1,mode:'sculpt',noise:{amplitude:3,scale:16},palette:{rock:'minecraft:stone',soil:'minecraft:dirt',surface:'minecraft:grass_block',soil_depth:2},features:[{type:'plateau',min:{x:2,z:2},max:{x:8,z:8},height:14,falloff:4}],preserve:[]};
|
||||
await client.callTool({name:'terrain_preview',arguments:{recipe}});assert.equal(seen.at(-1).method,'terrain_preview');assert.equal(seen.at(-1).params.resolution,128);
|
||||
await client.callTool({name:'terrain_prepare',arguments:{terrain_id:'a'.repeat(64),tile_index:1}});assert.equal(seen.at(-1).method,'terrain_prepare');
|
||||
const beforeTerrain=seen.length;
|
||||
for(const args of [{recipe:{...recipe,script:'execute'}},{recipe:{...recipe,features:[{type:'channel',points:[],width:2,falloff:3,height:0}]}},{recipe:{...recipe,palette:{...recipe.palette,rock:'minecraft:water'}}}]) assert.equal((await client.callTool({name:'terrain_preview',arguments:args})).isError,true);
|
||||
assert.equal((await client.callTool({name:'terrain_prepare',arguments:{terrain_id:'../../file',tile_index:-1}})).isError,true);
|
||||
assert.equal(seen.length,beforeTerrain);
|
||||
const brush={min:{x:-4,y:0,z:-4},max:{x:4,y:12,z:4},center:{x:0,z:0},radius:3,action:'raise',amount:2};
|
||||
await client.callTool({name:'terrain_brush_prepare',arguments:{brush}});
|
||||
assert.equal(seen.at(-1).method,'terrain_brush_prepare');assert.equal(seen.at(-1).params.brush.falloff,0.5);
|
||||
const beforeBrush=seen.length;
|
||||
for(const bad of [{...brush,radius:50},{...brush,strength:2},{...brush,action:'execute'},{...brush,script:'run'}])assert.equal((await client.callTool({name:'terrain_brush_prepare',arguments:{brush:bad}})).isError,true);
|
||||
assert.equal(seen.length,beforeBrush);
|
||||
});
|
||||
test('material discovery uses bounded read-only calls and never expands registry in schemas or instructions', async t => {
|
||||
const seen=[];
|
||||
const backend=createServer(async(req,res)=>{
|
||||
let body='';for await(const chunk of req) body+=chunk;
|
||||
const rpc=JSON.parse(body);seen.push(rpc);
|
||||
const result=rpc.method==='material_search'
|
||||
?{catalog_version:'v1',query:rpc.params.query??'',kind:rpc.params.kind,total:2,results:[{id:'minecraft:copper_door',block:true,item:true}],next_cursor:'next-page'}
|
||||
:rpc.method==='material_describe'
|
||||
?{catalog_version:'v1',id:'minecraft:copper_door',block:true,item:true,placeable:true,default_state:'minecraft:copper_door[facing=north,half=lower,hinge=left,open=false,powered=false]',properties:{facing:['north','south','east','west'],half:['lower','upper'],hinge:['left','right'],open:['false','true'],powered:['false','true']}}
|
||||
:{status:'prepared'};
|
||||
res.setHeader('content-type','application/json');res.end(JSON.stringify({ok:true,result}));
|
||||
});backend.listen(0,'127.0.0.1');await once(backend,'listening');
|
||||
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/mcp.js')],env:{MCB_BACKEND_URL:`http://127.0.0.1:${backend.address().port}`,MCB_AGENT_TOKEN:'agent',MCB_PROJECT_ID:'project',MCB_PLAYER_ID:'player'},stderr:'pipe'});
|
||||
const client=new Client({name:'materials-test',version:'1'});
|
||||
t.after(async()=>{await client.close();backend.closeAllConnections();backend.close();});
|
||||
await client.connect(transport);
|
||||
const list=await client.listTools();
|
||||
const discovery=list.tools.filter(tool=>tool.name.startsWith('material_'));
|
||||
assert.deepEqual(discovery.map(tool=>tool.name),['material_search','material_describe']);
|
||||
for(const tool of discovery){assert.equal(tool.annotations.readOnlyHint,true);assert.equal(tool.annotations.idempotentHint,true);assert.equal(tool.annotations.destructiveHint,false);}
|
||||
assert.ok(client.getInstructions().includes('project_context contains only a catalog summary'));
|
||||
assert.ok(client.getInstructions().includes('do not enumerate the registry'));
|
||||
assert.ok(!JSON.stringify(discovery).includes('minecraft:copper_door'),'registry entries must be discovered, not embedded in the MCP tool catalog');
|
||||
const found=await client.callTool({name:'material_search',arguments:{query:'copper door'}});
|
||||
assert.equal(seen.at(-1).method,'material_search');assert.equal(seen.at(-1).params.limit,16);assert.equal(seen.at(-1).params.kind,'block');
|
||||
assert.equal(JSON.parse(found.content[0].text).results[0].id,'minecraft:copper_door');
|
||||
await client.callTool({name:'material_search',arguments:{query:'copper door',kind:'all',limit:32,cursor:'next-page'}});
|
||||
assert.equal(seen.at(-1).params.cursor,'next-page');assert.equal(seen.at(-1).params.kind,'all');
|
||||
const described=JSON.parse((await client.callTool({name:'material_describe',arguments:{id:'minecraft:copper_door'}})).content[0].text);
|
||||
assert.equal(seen.at(-1).method,'material_describe');assert.deepEqual(described.properties.half,['lower','upper']);
|
||||
const beforeInvalid=seen.length;
|
||||
for(const arguments_ of [{query:'x'.repeat(97)},{kind:'entity'},{limit:0},{limit:33},{limit:1.5},{cursor:''},{cursor:'x'.repeat(101)}]){
|
||||
assert.equal((await client.callTool({name:'material_search',arguments:arguments_})).isError,true);
|
||||
}
|
||||
for(const id of ['copper_door','minecraft:stone[foo=bar]','minecraft:chest{Items:[]}','other:stone','minecraft:../stone',`minecraft:${'x'.repeat(119)}`]){
|
||||
assert.equal((await client.callTool({name:'material_describe',arguments:{id}})).isError,true);
|
||||
}
|
||||
assert.equal(seen.length,beforeInvalid,'invalid discovery arguments must be rejected before backend transport');
|
||||
const box=block=>({version:1,operations:[{type:'box',min:{x:0,y:64,z:0},max:{x:0,y:64,z:0},block}]});
|
||||
for(const block of ['minecraft:copper_door[facing=west,half=lower,hinge=left,open=false,powered=false]','minecraft:water[level=0]']){
|
||||
assert.ok(!(await client.callTool({name:'build_prepare',arguments:{recipe:box(block)}})).isError,'ordinary recipes must not retain the old material allowlist');
|
||||
}
|
||||
const snapshot={pos:{x:0,y:64,z:0},state:'minecraft:chest[facing=north,type=single,waterlogged=false]',snapshot_id:'a'.repeat(64)};
|
||||
assert.ok(!(await client.callTool({name:'build_prepare',arguments:{recipe:box('minecraft:chest[facing=west]'),expected_blocks:[snapshot]}})).isError);
|
||||
assert.equal(seen.at(-1).params.expected_blocks[0].snapshot_id,snapshot.snapshot_id,'block-entity snapshot digest must survive MCP transport unchanged');
|
||||
const beforeBadSnapshot=seen.length;
|
||||
for(const snapshot_id of ['a'.repeat(63),'A'.repeat(64),'not-a-snapshot']){
|
||||
assert.equal((await client.callTool({name:'build_prepare',arguments:{recipe:box('minecraft:stone'),expected_blocks:[{...snapshot,snapshot_id}]}})).isError,true);
|
||||
}
|
||||
assert.equal(seen.length,beforeBadSnapshot);
|
||||
const beforeUnsafe=seen.length;
|
||||
for(const block of ['minecraft:command_block{Command:"say hello"}',`minecraft:stone[p=${'x'.repeat(1024)}]`]){
|
||||
assert.equal((await client.callTool({name:'build_prepare',arguments:{recipe:box(block)}})).isError,true);
|
||||
}
|
||||
assert.equal(seen.length,beforeUnsafe,'state length and no-NBT boundaries must hold before backend transport');
|
||||
});
|
||||
test('camera image is a native MCP image rather than a text context dump',()=>{
|
||||
const result=toolResult({status:'completed',captureId:'c1',imageBase64:'YWJj',mimeType:'image/png'});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {mkdtemp,readFile,writeFile,rm} from 'node:fs/promises';
|
||||
import {mkdtemp,readFile,writeFile,readdir,rm,stat} from 'node:fs/promises';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {codexPaths,prepareCodexHome,CODEX_CONFIG_TOML,securityReport} from '../dist/security.js';
|
||||
import {codexPaths,prepareCodexHome,CODEX_CONFIG,CODEX_CONFIG_TOML,securityReport} from '../dist/security.js';
|
||||
const oldManagedConfig=CODEX_CONFIG_TOML.replace('code_mode_host = true\n','code_mode_host = false\n');
|
||||
test('dedicated home gets known config and never overwrites existing user configuration',async t=>{
|
||||
const root=await mkdtemp(join(tmpdir(),'mcb-security-'));t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
const paths=codexPaths(root);await prepareCodexHome(paths);
|
||||
@@ -13,9 +14,62 @@ test('dedicated home gets known config and never overwrites existing user config
|
||||
await assert.rejects(prepareCodexHome(paths),/will not be overwritten/);
|
||||
assert.equal(await readFile(join(paths.codexHome,'config.toml'),'utf8'),'sandbox_mode = "danger-full-access"\n');
|
||||
});
|
||||
test('migrates only the known old managed config atomically, backs it up and preserves authentication',async t=>{
|
||||
const root=await mkdtemp(join(tmpdir(),'mcb-security-'));t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
const paths=codexPaths(root);await prepareCodexHome(paths);
|
||||
const configPath=join(paths.codexHome,'config.toml');
|
||||
const authPath=join(paths.codexHome,'auth.json');
|
||||
const syntheticAuth='{"test_fixture":"preserve-me-byte-for-byte"}\n';
|
||||
await writeFile(configPath,oldManagedConfig);await writeFile(authPath,syntheticAuth,{mode:0o600});
|
||||
await prepareCodexHome(paths);
|
||||
assert.equal(await readFile(configPath,'utf8'),CODEX_CONFIG_TOML);
|
||||
assert.equal(await readFile(`${configPath}.before-code-mode-host`,'utf8'),oldManagedConfig);
|
||||
assert.equal(await readFile(authPath,'utf8'),syntheticAuth);
|
||||
if(process.platform!=='win32'){
|
||||
assert.equal((await stat(configPath)).mode&0o777,0o600);
|
||||
assert.equal((await stat(`${configPath}.before-code-mode-host`)).mode&0o777,0o600);
|
||||
}
|
||||
await prepareCodexHome(paths);
|
||||
assert.equal(await readFile(configPath,'utf8'),CODEX_CONFIG_TOML);
|
||||
assert.equal(await readFile(`${configPath}.before-code-mode-host`,'utf8'),oldManagedConfig);
|
||||
assert.equal(await readFile(authPath,'utf8'),syntheticAuth);
|
||||
assert.equal((await readdir(paths.codexHome)).filter(name=>name.endsWith('.tmp')).length,0);
|
||||
});
|
||||
test('refuses even small custom changes to old managed configuration without creating a backup',async t=>{
|
||||
const root=await mkdtemp(join(tmpdir(),'mcb-security-'));t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
const paths=codexPaths(root);await prepareCodexHome(paths);
|
||||
const configPath=join(paths.codexHome,'config.toml');
|
||||
for(const custom of [oldManagedConfig+'# my settings\n',oldManagedConfig.replace('shell_tool = false','shell_tool = true')]){
|
||||
await writeFile(configPath,custom);
|
||||
await assert.rejects(prepareCodexHome(paths),/will not be overwritten/);
|
||||
assert.equal(await readFile(configPath,'utf8'),custom);
|
||||
await assert.rejects(readFile(`${configPath}.before-code-mode-host`),{code:'ENOENT'});
|
||||
}
|
||||
});
|
||||
test('resumes with an exact previous backup but refuses to overwrite a different backup',async t=>{
|
||||
const root=await mkdtemp(join(tmpdir(),'mcb-security-'));t.after(()=>rm(root,{recursive:true,force:true}));
|
||||
const paths=codexPaths(root);await prepareCodexHome(paths);
|
||||
const configPath=join(paths.codexHome,'config.toml');const backupPath=`${configPath}.before-code-mode-host`;
|
||||
await writeFile(configPath,oldManagedConfig);await writeFile(backupPath,'different backup\n');
|
||||
await assert.rejects(prepareCodexHome(paths),/backup.*will not be overwritten/);
|
||||
assert.equal(await readFile(configPath,'utf8'),oldManagedConfig);
|
||||
assert.equal(await readFile(backupPath,'utf8'),'different backup\n');
|
||||
await writeFile(backupPath,oldManagedConfig);await prepareCodexHome(paths);
|
||||
assert.equal(await readFile(configPath,'utf8'),CODEX_CONFIG_TOML);
|
||||
assert.equal(await readFile(backupPath,'utf8'),oldManagedConfig);
|
||||
});
|
||||
test('code-mode host is enabled without enabling the separately restricted integrations',()=>{
|
||||
assert.equal(CODEX_CONFIG.features.code_mode_host,true);
|
||||
for(const [feature,enabled] of Object.entries(CODEX_CONFIG.features)){
|
||||
if(feature!=='code_mode_host')assert.equal(enabled,false,feature);
|
||||
}
|
||||
assert.equal(CODEX_CONFIG.sandbox_mode,'read-only');assert.equal(CODEX_CONFIG.web_search,'disabled');
|
||||
});
|
||||
test('doctor security report describes upstream workspace-write limitation honestly',()=>{
|
||||
const report=securityReport(codexPaths('/state'));
|
||||
assert.equal(report.configuredSandbox,'read-only');assert.equal(report.adapterTurnSandbox,'workspace-write');assert.equal(report.runtimeVerified,false);
|
||||
assert.equal(report.pinnedCliFeatureProbe.unified_exec,true);
|
||||
assert.equal(report.codeModeHostRequested,true);assert.equal(report.codeModeHostRequiredForModelToolMode,'code_mode_only');
|
||||
assert.ok(report.limitations.some(text=>text.includes('Doctor does not invoke a model')));
|
||||
assert.ok(report.limitations.some(text=>text.includes('temporary paths')));
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# Minecraft decoration playbook
|
||||
|
||||
Research reviewed **13 September 2026** for the Shacraft lobby. These notes combine primary Minecraft articles, interviews with builders and official Java release notes with our own measured-building workflow. They record reusable design knowledge; this research stage makes no changes to the world.
|
||||
|
||||
Start with this page, then open only the relevant entry in the [14-recipe cookbook](DECORATION-RECIPES.md). The [structured catalog](recipes/decorations.json) is the authoritative source for recipe dimensions, materials, assembly and acceptance checks. Recipes are original design proposals, not copied tutorial schematics, executable `build_prepare` payloads or claims of completed construction.
|
||||
|
||||
## Short working memory
|
||||
|
||||
1. Give the place a purpose and identify the main arrival, decision point and place to pause.
|
||||
2. Resolve silhouette, structural rhythm and major material fields before surface details.
|
||||
3. Assign each material a role. Keep a dominant family, supporting material and selective accent.
|
||||
4. Make depth with real layers: a recessed window, a projecting sill, a supported eave. Keep the closure behind decorative partial blocks intact.
|
||||
5. Cluster texture changes where wear, moisture, construction or planting explains them. Preserve calm surfaces between accents.
|
||||
6. Arrange furniture as useful groups. Keep entrances, lift controls and destination signs visible.
|
||||
7. Measure the actual ground, floor and route before placing a standard module. Preserve established widths and headroom.
|
||||
8. Check material availability, exact states, rotation, attachment, substrate and stability after updates.
|
||||
9. Build one sample, inspect it at player height and from the approach, then repeat with controlled variation.
|
||||
10. Compare real day/night photographs and inspect concealed sides. A map, plan or successful placement receipt is not a lighting review.
|
||||
11. Keep unfinished rooms closed. Decoration must not create a passage or a way around an existing enclosure.
|
||||
12. Save recipe IDs, anchors, rotations, seeds, operation receipts and selected photographs; load only the local context needed for the next edit.
|
||||
|
||||
These are project working rules. Numeric dimensions below are starting proposals for this lobby, not universal Minecraft laws.
|
||||
|
||||
## What the research changes in our practice
|
||||
|
||||
### Shape and depth before micro-detail
|
||||
|
||||
Aegos and Shannooty describe working out roofs and the overall silhouette before detailing. Milosz explains using a consistent palette and comparatively simple textures at large scale. For Shacraft, the clock tower should remain the first readable feature; a dormer, finial or cornice is useful only if it strengthens that composition. Review a proposed feature from the main approach before copying it around the roof. [Tranquil Towers](https://www.minecraft.net/en-us/article/tranquil-towers), [The Sky is No Limit](https://www.minecraft.net/en-us/article/sky-no-limit).
|
||||
|
||||
The glass-pane article explicitly connects windows with variation in wall depth. Our adaptation uses a recessed **full-glass closure** and a projecting stone surround, since public containment is part of this project. Thin panes can be useful elsewhere, but their appearance does not prove a sealed boundary. Quartz also provides a consistent family for architecture and small furnishings. [Taking Inventory: Glass Pane](https://www.minecraft.net/en-us/article/taking-inventory--glass-pane), [Build With It: Quartz](https://www.minecraft.net/en-us/article/build-with-it--quartz).
|
||||
|
||||
### Material variation should explain the surface
|
||||
|
||||
Rough diorite and its polished form demonstrate that texture intensity changes the character of a surface; a brighter material is not automatically a quieter one. Mossy cobblestone can suggest age. Our design inference is to place texture by cause: moisture and earth contact at a retaining wall, wear near an approach, clean edges around a maintained station entrance. Use connected patches rather than an independent random material choice for every block. [Build with It: Diorite!](https://www.minecraft.net/en-us/article/build-it--diorite-), [Build With It: Cobblestone!](https://www.minecraft.net/en-us/article/build-with-it--cobblestone-).
|
||||
|
||||
A color gradient is optional. If used, it should reinforce a chosen mass or light direction and read at the intended distance. Do not impose an arbitrary weathering percentage or a fixed color-ratio formula on every building. We should compare a small clean sample and a restrained textured sample before committing a facade.
|
||||
|
||||
### Interior groups should tell players how to use the room
|
||||
|
||||
The builders in *Interior Motives* emphasize purpose and agreement between inside and outside. Jonathan “SnugSites” discusses livable furniture arrangements and the value of two-block partitions when rooms need different finishes on each face. We should build waiting groups, information points and viewing pockets, reserving a clear route between them; an empty section of floor can be necessary circulation. [Interior Motives](https://www.minecraft.net/en-us/article/interior-motives), [Fantastic Furniture](https://www.minecraft.net/en-us/article/fantastic-furniture).
|
||||
|
||||
Spruce, plants and lanterns are useful companions to copper and masonry in *Cozy Chambers*. For our station this means warmth at the scale of benches, counters and exhibits, with a limited number of focal groups. A Minecraft article also documents a flowerpot above a decorated pot as a large planter. The original cookbook used a small stone planter; the runtime catalog now makes registered pot block types discoverable, while custom pot decoration and stored-item data remain outside the ordinary state editor. [Cozy Chambers](https://www.minecraft.net/en-us/article/cozy-chambers), [Decorated Pot](https://www.minecraft.net/en-us/article/decorated-pot).
|
||||
|
||||
### Decorate the transition between architecture and landscape
|
||||
|
||||
The official presentation of BlueNerd's landscaping work connects paths, banks, soil and vegetation. Zaypixel's path examples show how material families and nearby objects communicate different settings. Our application is a clean, formal station axis that gradually gives way to local rock, soil and planted pockets farther out. Curve a route when its destination or terrain calls for it. Do not force a kink every seven blocks: that number belongs to one organic-building tutorial, not the engine or every architectural style. [Landscaping and Terraforming](https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming), [Five simple path designs](https://www.minecraft.net/en-us/article/five-simple-path-designs).
|
||||
|
||||
Kelpie the Fox's outdoor groups provide useful roles for benches, small structures and hanging lights. The article identifies **Mizuno's 16 Craft** in its reference imagery. Borrow composition and purpose, then judge our result in the actual client textures; matching block names alone cannot reproduce a resource-pack image. [Cottagecore Decor](https://www.minecraft.net/en-us/article/tutorial--cottagecore-decor).
|
||||
|
||||
### Lighting requires its own review
|
||||
|
||||
Regular lanterns are a useful recurring fixture, but a fixture count does not establish room legibility. Look at the spaces between lights, stair landings, sign faces and exhibit fronts. We already used real station photographs to find dark interiors and refine concealed lighting; see the [completed station record](SHACRAFT-STATION.md). [Taking Inventory: Lantern](https://www.minecraft.net/en-us/article/taking-inventory--lantern).
|
||||
|
||||
**Tinted glass is not stained glass:** the Java release notes describe tinted glass as blocking light. Do not use it as a substitute for a transparent lighting lens. The same release notes document waxed copper as preventing oxidation. These facts explain material choices; exact accepted states still come from our current server. [Java Caves & Cliffs Part I](https://www.minecraft.net/en-us/article/caves---cliffs--part-i-out-today-java).
|
||||
|
||||
## Shacraft palette and composition
|
||||
|
||||
The following assignments are our design direction based on the existing arrival garden and station, rather than claims made by the sources:
|
||||
|
||||
- **Large cream masses:** `smooth_sandstone`, with `cut_sandstone` for purposeful masonry bands.
|
||||
- **Clean prominent edges:** `smooth_quartz` and `quartz_pillar`; use supported sandstone stairs/slabs when a partial shape is needed.
|
||||
- **Roofs and identity:** the `waxed_oxidized_cut_copper` family. Keep broad roof planes visible.
|
||||
- **Human-scale warmth:** spruce for benches, counters, ceilings and small structural posts.
|
||||
- **Selective emphasis:** gold at a major identity or control point, with green backing. Repeated small gold accents must not compete with the main sign.
|
||||
- **Grounded base:** stone bricks, andesite and restrained moss near actual soil or damp recesses.
|
||||
- **Planting:** a dominant evergreen mass and small pink/white flower groups, continuing zone 01's established vocabulary.
|
||||
- **SMASH:** preserve the station frame; use a contained island exhibit and restrained warm accent to distinguish this floor.
|
||||
|
||||
Place more detail at an entrance, destination or stopping point. Leave broader quiet surfaces and uninterrupted paving between those locations. This is a distribution choice, not a ban on ornament.
|
||||
|
||||
## Recipe index
|
||||
|
||||
Each cookbook entry includes dimensions, palette, assembly order, clearance, controlled variants and failure checks:
|
||||
|
||||
- Facade: `arched-window-bay`, `cornice-bracket`.
|
||||
- Roof: `closed-dormer`.
|
||||
- Interior: `waiting-pocket`, `information-counter`, `smash-exhibit`.
|
||||
- Routes: `avenue-edge`, `retaining-stair`.
|
||||
- Lighting: `copper-lantern`, `concealed-light-lens`.
|
||||
- Planting and landscape: `layered-planter`, `shore-pocket`.
|
||||
- Small props and surfaces: `luggage-bench`, `masonry-weathering`.
|
||||
|
||||
## Current capability boundary
|
||||
|
||||
The saved Stage 07 `project_context` lists **100 material IDs**. All base recipe material lists were checked against that historical snapshot. The structured recipes retain that dated capability basis so earlier designs remain reproducible; it is no longer the building allowlist. The runtime implementation in [`BuildingWorld.java`](../paper-plugin/src/main/java/io/github/minecraftbuilder/paper/BuildingWorld.java) now accepts all registered vanilla block types and their valid states. Read the live catalog summary, then discover only the materials needed for the next detail through `material_search` and `material_describe`. [Material workflow](MATERIALS.md).
|
||||
|
||||
The original recipe catalog uses flush colored paving instead of carpet, masonry planters instead of pots, full stained glass instead of panes, and wooden block props instead of item entities. These remain deliberate design variants. Registered carpets, trapdoors, flowerpots, decorated pots, banners, candles, wildflowers, leaf litter and firefly bushes can now be selected through the runtime catalog. Block-type availability does not add arbitrary banner patterns, pot decorations, sign text, inventories or text-display creation; the station's existing labels use a separate project-specific plugin. Item-only materials are searchable but cannot be placed by a block recipe.
|
||||
|
||||
Java 1.21.5 introduced useful plant density/orientation options; its firefly bush emits only light level 2, so it should be treated as an atmospheric accent rather than primary street illumination. Java 26.2 also adds sulfur and cinnabar material families. Material IDs and exact properties come from the running server's registry, so search before using a version-specific feature. Additional behavior is not a new callable MCP API. [Java 1.21.5 release notes](https://feedback.minecraft.net/hc/en-us/articles/35298208390797-Minecraft-Java-Edition-1-21-5-Spring-to-Life), [Java 26.2 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-2).
|
||||
|
||||
For this runtime, discover the chain ID rather than assuming an older name; the saved station uses `minecraft:iron_chain`. Use `persistent=true` for decorative leaves unless deliberate natural decay is intended, and check distance state on read-back. Flowers need accepted substrates, logs need real support, and stairs/slabs/lanterns need the correct directional and attachment states. Place every half or part of doors, beds and tall plants explicitly. Ordinary building plans can place fluids and waterlogged states, but terrain brushes still reject water in their scan. The existing shore recipe remains a land-side design that preserves water. Fluid flow, random ticks and other later game behavior are not fully reversed by undoing only a plan's direct writes.
|
||||
|
||||
## Applying a recipe economically
|
||||
|
||||
The Markdown cookbook is for human review; the JSON makes one entry easy to retrieve without reading the entire document. For a local repository agent:
|
||||
|
||||
```bash
|
||||
jq '.recipes[] | {id, category, size}' docs/recipes/decorations.json
|
||||
jq '.recipes[] | select(.id == "copper-lantern")' docs/recipes/decorations.json
|
||||
```
|
||||
|
||||
Save a placement record with recipe ID, recipe-catalog version and runtime material-catalog version, local origin, rotation, chosen variant, terrain/floor anchor, palette substitutions, variation seed and operation IDs. Use a focused `material_search` page (16 results by default, 32 maximum), then describe only 1–3 selected materials. Reuse their defaults and property domains while the runtime catalog version is unchanged. Inspect the target area and its support/clearance neighborhood, then compile a bounded expected-state patch. Copy each block-entity `snapshot_id` from `region_inspect` into `expected_blocks` to detect data edits without reading inventories or NBT into context. Keep the checked operation receipts and a small set of matching photographs. Large-scale repetition comes after a successful sample.
|
||||
|
||||
The shared [MCP/ACP guidance](../bridge/src/building-guidance.ts) includes short material-discovery and decoration summaries. Neither the full recipe catalog nor the material registry is injected into every model turn. The recipe JSON is not a new MCP resource or tool: the in-game ACP agent has no repository filesystem access, so its instructions must not pretend it can retrieve this file. New bridge processes receive the updated summary, and resumed ACP sessions receive changed guidance once through its stored fingerprint. Deployment and validation of the registry expansion are recorded in [MATERIALS.md](MATERIALS.md#validation).
|
||||
|
||||
## Acceptance before calling a decoration complete
|
||||
|
||||
1. **Fit:** same survey anchors, preserved structure, route width and four-block public headroom where that is the existing Shacraft contract. Four blocks is our design margin, not Minecraft's minimum player height.
|
||||
2. **Stability:** supports, substrates, leaves, attached pieces and rotated states remain valid after block updates.
|
||||
3. **Movement:** inspect approaches, corners, stairs and ceilings. A decorative fence or hedge does not replace the checked enclosure. Keep unfinished interiors inaccessible.
|
||||
4. **Visuals:** matched overview, approach and player-height views; day/night where lighting matters. Check sign visibility, density, material contrast and dark exhibit faces in the actual client.
|
||||
5. **Evidence:** distinguish proposed design, server-applied blocks, measured geometry and visually inspected results. Record an unavailable camera honestly.
|
||||
6. **Preservation:** keep the before state, checked edits and useful photographs. A later manual edit is a conflict to resolve, not an invitation to overwrite it.
|
||||
|
||||
The 14 catalog entries have been reviewed as design specifications and checked for material-name availability. They have **not** been instantiated or visually approved as a new set of in-world samples.
|
||||
@@ -0,0 +1,512 @@
|
||||
# Shacraft decoration recipes
|
||||
|
||||
Generated from [decorations.json](recipes/decorations.json), the authoritative catalog. Read the [playbook](DECORATION-PLAYBOOK.md) for principles, current capabilities and the acceptance workflow.
|
||||
|
||||
**14 original design proposals, reviewed 13 September 2026.** Material IDs match the saved 100-material Stage 07 palette. These are design references, not executable Minecraft recipes or completed in-world samples. Recheck live capabilities before applying.
|
||||
|
||||
Dimensions below are **width × height × depth** in blocks. They exclude separately specified circulation and buried support. Local +X is right, +Y is up, +Z points into the object; the front faces -Z. Measurements and spacing are project proposals, not engine limits.
|
||||
|
||||
## arched-window-bay
|
||||
|
||||
**Recessed arched window bay** · facade · **7 × 10 × 3 blocks**
|
||||
|
||||
Give a large cream facade readable shadow and a consistent structural rhythm.
|
||||
|
||||
Materials: `smooth_sandstone`, `cut_sandstone`, `smooth_sandstone_stairs`, `smooth_sandstone_slab`, `smooth_quartz`, `brown_stained_glass`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Reserve a seven-wide bay between the actual structural axes; keep adjacent columns and floor decks intact.
|
||||
2. At the glazing plane, use five-wide glass rows for the straight portion, then three and one at the crown. Fill every surrounding cell with full masonry so the plane stays sealed.
|
||||
3. Place glazing one block behind the front wall plane; use the remaining front depth for a sill and paired jamb brackets.
|
||||
4. Use sandstone stairs only as outer trim. Maintain full masonry or full glass behind partial-block trim.
|
||||
5. Repeat the approved bay on the established grid; redesign corners and the main doorway deliberately.
|
||||
|
||||
**Clearance:** Keep projections outside the existing public route and its four-block headroom; do not narrow the seven-block station entry.
|
||||
|
||||
Variants:
|
||||
|
||||
- Plain rectangular head on a secondary facade.
|
||||
- One gold accent above the principal entrance only.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Closure plane has no unfilled cells, including arch shoulders.
|
||||
- Recess reads from a diagonal player-height camera.
|
||||
- No sill hides a sign or clips furniture.
|
||||
- Stair rotation and corner shape checked after neighboring blocks update.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Different window silhouettes in every bay.
|
||||
- Assuming a pane or stair is a full containment wall.
|
||||
|
||||
Conceptual sources: [The Sky is No Limit](https://www.minecraft.net/en-us/article/sky-no-limit), [Taking Inventory: Glass Pane](https://www.minecraft.net/en-us/article/taking-inventory--glass-pane), [Build With It: Quartz](https://www.minecraft.net/en-us/article/build-with-it--quartz). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## cornice-bracket
|
||||
|
||||
**Cornice and paired bracket** · facade · **9 × 3 × 2 blocks**
|
||||
|
||||
Explain the floor/eave line without filling the wall with ornament.
|
||||
|
||||
Materials: `smooth_sandstone`, `smooth_sandstone_stairs`, `smooth_sandstone_slab`, `smooth_quartz`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Place a continuous stone band on the existing floor or eave line.
|
||||
2. Use an inverted-stair underside and a thin slab cap, with at most two blocks of total outward depth.
|
||||
3. Put the two bracket groups over structural supports and leave the central field quiet.
|
||||
4. Continue the band around corners with an explicitly resolved return; prototype the corner before repeating.
|
||||
|
||||
**Clearance:** Remain above route headroom and outside signs' viewing rays; preserve roof drainage shapes and window tops.
|
||||
|
||||
Variants:
|
||||
|
||||
- Single-block projection on small wings.
|
||||
- A stronger cap at the tower base, using the same materials.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Horizontal band remains readable from the main approach.
|
||||
- Brackets align with actual supports.
|
||||
- Cap does not overwhelm the window opening.
|
||||
|
||||
Avoid:
|
||||
|
||||
- A thick shelf over every horizontal edge.
|
||||
- Unrelated gold trim on every bracket.
|
||||
|
||||
Conceptual sources: [Tranquil Towers](https://www.minecraft.net/en-us/article/tranquil-towers), [The Sky is No Limit](https://www.minecraft.net/en-us/article/sky-no-limit), [Build With It: Quartz](https://www.minecraft.net/en-us/article/build-with-it--quartz). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## closed-dormer
|
||||
|
||||
**Closed green-copper dormer** · roof · **7 × 7 × 7 blocks**
|
||||
|
||||
Add a secondary roof feature while keeping the clock tower dominant.
|
||||
|
||||
Materials: `smooth_sandstone`, `brown_stained_glass`, `waxed_oxidized_cut_copper`, `waxed_oxidized_cut_copper_stairs`, `waxed_oxidized_cut_copper_slab`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Fit a seven-wide sample to the actual roof pitch; use cream side cheeks and three-wide recessed glass.
|
||||
2. Build the green pitched cap with a one-block overhang.
|
||||
3. Keep a solid back and floor closure behind the apparent window; retain the existing public ceiling.
|
||||
4. Test one dormer from the main approach before any repetition. Consider 14-18 blocks between centers only where the measured structural grid permits it.
|
||||
|
||||
**Clearance:** No opening into an unfinished attic, no overlap with tower faces or main roof ridge.
|
||||
|
||||
Variants:
|
||||
|
||||
- One larger central feature on a secondary wing.
|
||||
- Omit the dormer if it competes with the clock silhouette.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Tower remains the primary skyline feature.
|
||||
- Quiet copper roof planes remain visible between details.
|
||||
- Full closure survives the added roof cutout.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Treating proposed spacing as a universal rule.
|
||||
- Adding attic access as part of a cosmetic edit.
|
||||
|
||||
Conceptual sources: [Tranquil Towers](https://www.minecraft.net/en-us/article/tranquil-towers), [The Sky is No Limit](https://www.minecraft.net/en-us/article/sky-no-limit). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## waiting-pocket
|
||||
|
||||
**Paired waiting benches** · interior · **9 × 4 × 7 blocks**
|
||||
|
||||
Create a believable place to wait beside the main hall.
|
||||
|
||||
Materials: `spruce_stairs`, `spruce_slab`, `spruce_planks`, `smooth_sandstone`, `green_concrete`, `lantern`, `oak_leaves`, `dirt`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Place two three-seat spruce benches facing a shared open pocket, with cream end supports.
|
||||
2. Keep a compact table at one side rather than on the main desire line.
|
||||
3. Use a flush green paving inset to group the furniture; this substitutes for unsupported carpet.
|
||||
4. Add one planter and one supported lantern near the back, leaving the front visually open.
|
||||
|
||||
**Clearance:** Reserve an additional three-block-wide clear passing strip and four blocks of route headroom. Existing wider routes retain their width.
|
||||
|
||||
Variants:
|
||||
|
||||
- Single bench and planter in a smaller side recess.
|
||||
- Mirror the group around the room axis while varying the small prop.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Lift sign remains visible from the approach.
|
||||
- Seat fronts and table can be approached without stepping onto furniture.
|
||||
- Lamp and leaves remain stable after updates.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Rows of isolated decorative chairs.
|
||||
- Plants in front of navigation signs.
|
||||
|
||||
Conceptual sources: [Interior Motives](https://www.minecraft.net/en-us/article/interior-motives), [Fantastic Furniture](https://www.minecraft.net/en-us/article/fantastic-furniture), [Cozy Chambers](https://www.minecraft.net/en-us/article/cozy-chambers). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## information-counter
|
||||
|
||||
**Information counter** · interior · **9 × 5 × 4 blocks**
|
||||
|
||||
Provide a clear information point that belongs in a station.
|
||||
|
||||
Materials: `spruce_planks`, `spruce_slab`, `smooth_sandstone`, `smooth_sandstone_slab`, `green_concrete`, `glowstone`, `brown_stained_glass`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Frame a seven-block counter between cream end piers; use spruce body and a thin cream slab top.
|
||||
2. Align one green sign backing and one lit recess behind the counter.
|
||||
3. Keep the back wall solid. Any future staff door remains a finished wall panel until the room exists.
|
||||
4. Add actual text only through an existing authorized label mechanism; the block catalog does not create text entities.
|
||||
|
||||
**Clearance:** Reserve three clear queue blocks in front, separated from the entrance-to-lift route and its full width.
|
||||
|
||||
Variants:
|
||||
|
||||
- Short five-wide counter for a side room.
|
||||
- A small gold emblem only above the principal information point.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Queue does not cross the main route.
|
||||
- Counter is recognizable before reading the sign.
|
||||
- No false usable door into unfinished space.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Several equally bright competing headers.
|
||||
- Claiming matchmaking or staff interaction exists merely because the counter is built.
|
||||
|
||||
Conceptual sources: [Interior Motives](https://www.minecraft.net/en-us/article/interior-motives), [Build With It: Quartz](https://www.minecraft.net/en-us/article/build-with-it--quartz), [Fantastic Furniture](https://www.minecraft.net/en-us/article/fantastic-furniture). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## smash-exhibit
|
||||
|
||||
**SMASH exhibit niche** · interior · **7 × 6 × 5 blocks**
|
||||
|
||||
Communicate floating islands and knockback through one readable display.
|
||||
|
||||
Materials: `deepslate`, `smooth_sandstone`, `green_concrete`, `gold_block`, `grass_block`, `dirt`, `glowstone`, `brown_stained_glass`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Build a full dark base, full back and light stone frame around a five-wide exhibit.
|
||||
2. Choose one miniature island or one abstract impact motif as the subject.
|
||||
3. Put one small gold focal detail near the subject and keep the rest of the frame consistent with adjacent niches.
|
||||
4. Light the subject from concealed side, base or overhead recesses; preserve full-block base and closure.
|
||||
5. Use one shared frame for all six future arena places; vary the subject and real destination text only when available.
|
||||
|
||||
**Clearance:** Reserve a three-block viewing strip outside the main circulation aisle; preserve the existing selection aisle.
|
||||
|
||||
Variants:
|
||||
|
||||
- Closed trophy pedestal with a single object.
|
||||
- Small landscape model for a future named arena.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Subject is legible from player eye level without entering the display.
|
||||
- No dark face hides the model's silhouette.
|
||||
- Placeholders clearly remain unassigned.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Six unrelated palettes.
|
||||
- A visual void that is also an accidental physical hole.
|
||||
|
||||
Conceptual sources: [Interior Motives](https://www.minecraft.net/en-us/article/interior-motives), [Fantastic Furniture](https://www.minecraft.net/en-us/article/fantastic-furniture), [Cozy Chambers](https://www.minecraft.net/en-us/article/cozy-chambers). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## avenue-edge
|
||||
|
||||
**Formal avenue edge** · path · **9 × 1 × 12 blocks**
|
||||
|
||||
Connect station stonework to the garden through a continuous legible route.
|
||||
|
||||
Materials: `stone_bricks`, `smooth_stone`, `smooth_sandstone`, `mossy_stone_bricks`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Keep seven center columns as continuous clear paving and one flush border column on each side.
|
||||
2. Survey elevations along the whole segment before decoration; grade transitions as separate geometry.
|
||||
3. Place furniture in additional two-to-three-block shoulders or wider pockets, not inside the nine-block strip.
|
||||
4. Keep the station approach clean. Introduce small connected worn patches at outer edges beside planting.
|
||||
5. Widen at entrances and follow actual destinations rather than adding arbitrary bends.
|
||||
|
||||
**Clearance:** Seven-block clear walking width and four-block headroom; this example must not narrow a wider established route.
|
||||
|
||||
Variants:
|
||||
|
||||
- Use local gray stone farther from the station.
|
||||
- Curve the outer border while keeping the tread continuous.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Entire route walkable at all grade changes.
|
||||
- Flush borders do not introduce a lip.
|
||||
- No lamp or planter creates a pinch point.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Uniform random scatter of five paving materials.
|
||||
- Applying an organic-path zigzag rule to a formal station axis.
|
||||
|
||||
Conceptual sources: [Tutorial: Tips For Landscaping and Terraforming](https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming), [Five simple path designs](https://www.minecraft.net/en-us/article/five-simple-path-designs), [Build With It: Cobblestone!](https://www.minecraft.net/en-us/article/build-with-it--cobblestone-). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## copper-lantern
|
||||
|
||||
**Supported copper-arm lantern** · lighting · **3 × 7 × 1 blocks**
|
||||
|
||||
Establish one recognizable outdoor fixture family.
|
||||
|
||||
Materials: `stone_bricks`, `stripped_spruce_log`, `waxed_oxidized_cut_copper`, `iron_chain`, `lantern`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. With the sample's first above-ground cell at y=0, place a full stone foot at (0,0,0), rooted into surveyed ground below.
|
||||
2. Raise a post at x=0,z=0 through y=1..5; place a full-block copper arm across x=0..2 at y=6.
|
||||
3. Place a vertical iron chain at (2,5,0) attached beneath the full arm, then a hanging lantern at (2,4,0).
|
||||
4. Place support before attached details and inspect resulting block states.
|
||||
5. Keep the entire post outside the clear path; use a mirrored pair only at a major entry.
|
||||
|
||||
**Clearance:** Four empty cells below the hanging lantern if ground is level with the sample. Recalculate for actual terrain; do not assume the same clearance over a rising stair.
|
||||
|
||||
Variants:
|
||||
|
||||
- Shorter ground-mounted fixture in a planted pocket with no walk-under route.
|
||||
- One gold marker at a major junction, outside this base recipe.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Full arm, chain axis and hanging lantern state remain valid after updates.
|
||||
- Night view shows the next step, intersection and sign between lamps.
|
||||
- Post cannot be used to bypass an existing containment boundary.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Unsupported lanterns.
|
||||
- Treating a fixed lamp spacing as guaranteed illumination coverage.
|
||||
|
||||
Conceptual sources: [Tutorial: Cottagecore Decor](https://www.minecraft.net/en-us/article/tutorial--cottagecore-decor), [Taking Inventory: Lantern](https://www.minecraft.net/en-us/article/taking-inventory--lantern), [Caves & Cliffs: Part I out today on Java](https://www.minecraft.net/en-us/article/caves---cliffs--part-i-out-today-java). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## layered-planter
|
||||
|
||||
**Layered planting pocket** · planting · **7 × 6 × 5 blocks**
|
||||
|
||||
Produce a planted mass with a readable front, middle and background.
|
||||
|
||||
Materials: `dirt`, `grass_block`, `spruce_log`, `spruce_leaves`, `oak_leaves`, `white_tulip`, `pink_tulip`, `oxeye_daisy`, `smooth_sandstone`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Shape a low cream rim with solid soil behind it; support the entire pocket down to the existing terrain.
|
||||
2. Put one small rooted tree toward the rear and two unequal shrub groups below it.
|
||||
3. Use one dominant foliage species and two restrained flower colors near the front.
|
||||
4. Leave patches of visible grass between groups; taper density toward the edge.
|
||||
5. Save a seed and cluster anchors for repeatable variation; change group positions rather than every block independently.
|
||||
|
||||
**Clearance:** Keep the highest mass outside entrance and sign sightlines; no leaves overhang the protected walking clearance.
|
||||
|
||||
Variants:
|
||||
|
||||
- Formal clipped evergreen beside the station.
|
||||
- Looser shrubs without a tree beside the mountain path.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Flowers have valid soil.
|
||||
- Leaves use persistent=true and a stable read-back distance state.
|
||||
- Trunk has stable soil under it.
|
||||
- Foliage tint checked in the actual biome.
|
||||
|
||||
Avoid:
|
||||
|
||||
- A flower on every grass block.
|
||||
- Using leaves as the only containment boundary.
|
||||
|
||||
Conceptual sources: [Cozy Chambers](https://www.minecraft.net/en-us/article/cozy-chambers), [Tutorial: Tips For Landscaping and Terraforming](https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming), [Tutorial: Cottagecore Decor](https://www.minecraft.net/en-us/article/tutorial--cottagecore-decor). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## shore-pocket
|
||||
|
||||
**Quiet shore with a stony recess** · landscape · **12 × 4 × 8 blocks**
|
||||
|
||||
Connect existing water and terrain with a believable local bank.
|
||||
|
||||
Materials: `stone`, `andesite`, `cobblestone`, `mossy_cobblestone`, `dirt`, `grass_block`, `oak_leaves`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Sample a short existing shoreline together with its water level and submerged bed.
|
||||
2. Shape one quiet bank and one small stony recess; maintain continuous ground beneath the visible soil skin.
|
||||
3. Put exposed rock at the outcrop and moss only in selected damp recesses.
|
||||
4. Keep vegetation in sheltered land pockets and leave a clear view of the water.
|
||||
5. This catalog specifies only land-side decor. Preserve water cells and the connected bed; stop if execution requires fluid placement or an unsupported brush through water.
|
||||
|
||||
**Clearance:** Preserve public route width, foundations, water level and any existing waterfront containment.
|
||||
|
||||
Variants:
|
||||
|
||||
- Grass bank with one isolated outcrop.
|
||||
- Stone recess beside a retaining wall, without changing its footing.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Water network and visible level preserved.
|
||||
- No new flow reaches a road or foundation after updates.
|
||||
- A below-water view shows a continuous bed; top-down appearance alone is insufficient.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Repeating identical coves as tiles.
|
||||
- Assuming water exists in the MCP material palette.
|
||||
|
||||
Conceptual sources: [Tutorial: Tips For Landscaping and Terraforming](https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming), [Build With It: Cobblestone!](https://www.minecraft.net/en-us/article/build-with-it--cobblestone-). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## retaining-stair
|
||||
|
||||
**Retaining wall and short stair** · path · **9 × 5 × 10 blocks**
|
||||
|
||||
Make a grade change feel supported and intentionally connected.
|
||||
|
||||
Materials: `stone_bricks`, `stone_brick_stairs`, `stone_brick_slab`, `smooth_sandstone`, `smooth_sandstone_slab`, `mossy_stone_bricks`, `dirt`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Survey lower and upper approaches first; adapt the proposed envelope to the actual rise.
|
||||
2. Reserve five clear stair columns and two shoulder columns per side.
|
||||
3. Use three or four one-block rises followed by a three-block-deep landing; fill every tread and retaining wall down to solid ground.
|
||||
4. Return the side walls into the upper terrace instead of ending them as disconnected pillars.
|
||||
5. Keep the cream cap continuous; limit moss to local soil-contact or shaded recesses.
|
||||
|
||||
**Clearance:** Five clear stair blocks and four-block headroom along the full ascending path, including caps and any overhead fixture.
|
||||
|
||||
Variants:
|
||||
|
||||
- A shorter flight for a lower terrace.
|
||||
- An additional landing and separate second flight when the actual rise requires it.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Stair facing matches direction of ascent.
|
||||
- Approach and landing elevations agree with the survey.
|
||||
- No hollow exposed underside or unsupported cap.
|
||||
- Shoulders do not create a climb-over route around containment.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Solving a height mismatch with a decorative slab at the doorway.
|
||||
- Placing a standard stair module without measuring the destination.
|
||||
|
||||
Conceptual sources: [Tutorial: Tips For Landscaping and Terraforming](https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming), [Five simple path designs](https://www.minecraft.net/en-us/article/five-simple-path-designs), [Build With It: Cobblestone!](https://www.minecraft.net/en-us/article/build-with-it--cobblestone-). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## luggage-bench
|
||||
|
||||
**Bench and luggage group** · props · **7 × 3 × 4 blocks**
|
||||
|
||||
Tell a station story with a small, useful-looking prop group.
|
||||
|
||||
Materials: `spruce_stairs`, `spruce_planks`, `stripped_spruce_log`, `smooth_sandstone`, `green_concrete`, `gold_block`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Put a three-seat bench against the back of a widened pocket, facing the plaza.
|
||||
2. Group two wood-toned luggage shapes at one end, leaving seat fronts clear.
|
||||
3. Reserve a green backing for one real destination or station identity plaque.
|
||||
4. Use at most one small metallic accent in the group; omit it if it attracts more attention than the navigation.
|
||||
|
||||
**Clearance:** Provide a separate three-block passing strip in front and preserve any wider existing route.
|
||||
|
||||
Variants:
|
||||
|
||||
- Bench alone.
|
||||
- Bench with one planter instead of luggage.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Object reads as a resting place from the walking path.
|
||||
- Luggage does not intrude into the bench approach.
|
||||
- No label advertises an unfinished accessible destination.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Crates in every empty corner.
|
||||
- Unsupported decorative item entities or invented text APIs.
|
||||
|
||||
Conceptual sources: [Interior Motives](https://www.minecraft.net/en-us/article/interior-motives), [Fantastic Furniture](https://www.minecraft.net/en-us/article/fantastic-furniture), [Five simple path designs](https://www.minecraft.net/en-us/article/five-simple-path-designs). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## concealed-light-lens
|
||||
|
||||
**Full-block concealed light lens** · lighting · **3 × 2 × 3 blocks**
|
||||
|
||||
Illuminate a large room without adding a hanging fixture to every bay.
|
||||
|
||||
Materials: `glowstone`, `brown_stained_glass`, `smooth_sandstone`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Reserve a three-by-three floor sample outside patterned paving, tree roots, landings and interactive controls.
|
||||
2. Keep full paving in the eight perimeter columns; in the center put glowstone below a full brown stained-glass block flush with the walking surface.
|
||||
3. Keep supporting construction below the sample intact and record expected states before replacement.
|
||||
4. Prototype one lens; increase coverage only after matching night and interior photographs reveal dark areas.
|
||||
5. A ceiling variant reverses the emitter and visible lens positions within an intact full-block ceiling.
|
||||
|
||||
**Clearance:** Top of the floor lens remains flush. Ceiling version preserves all existing route headroom and sealed upper rooms.
|
||||
|
||||
Variants:
|
||||
|
||||
- Use ordinary full glass if the brown lens makes the sample too dark.
|
||||
- Concealed side lighting for an exhibit rather than more floor lights.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Full-block collision remains at the floor/ceiling surface.
|
||||
- Actual captured room and exhibit faces become legible.
|
||||
- No new bright fixture competes with the entrance or lift.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Tinted glass: it blocks light and is not interchangeable with stained glass.
|
||||
- Assuming more fixtures automatically produce a better interior.
|
||||
|
||||
Local precedent: Stage 07 used concealed brown-glass lighting and real camera review; this reusable sample has not itself been instantiated.
|
||||
|
||||
Conceptual sources: [Cozy Chambers](https://www.minecraft.net/en-us/article/cozy-chambers), [Caves & Cliffs: Part I out today on Java](https://www.minecraft.net/en-us/article/caves---cliffs--part-i-out-today-java). Recipe geometry and sequence are our own proposal.
|
||||
|
||||
## masonry-weathering
|
||||
|
||||
**Cause-based masonry weathering** · surface · **12 × 6 × 1 blocks**
|
||||
|
||||
Add age and material variation while retaining large calm wall fields.
|
||||
|
||||
Materials: `stone_bricks`, `cracked_stone_bricks`, `mossy_stone_bricks`, `andesite`, `smooth_sandstone`, `cut_sandstone`.
|
||||
|
||||
Assembly:
|
||||
|
||||
1. Choose either the gray retaining-wall family or the cream facade family; do not mix both randomly.
|
||||
2. Save a mask for soil contact, sheltered joints and selected worn edges.
|
||||
3. For gray masonry, use small connected mossy patches in the mask and a few connected cracked areas away from structural focal edges.
|
||||
4. For cream masonry, keep the field mostly smooth and use cut sandstone to express masonry bands or quiet local variation; do not add green flecks everywhere.
|
||||
5. Preserve trim, signs and dominant shapes, then inspect at near, approach and whole-building distances.
|
||||
|
||||
**Clearance:** Surface substitution only; preserve structural collision, public closure and existing manual edits.
|
||||
|
||||
Variants:
|
||||
|
||||
- Clean maintained station entrance with minimal weathering.
|
||||
- More aged retaining wall near shaded soil contact.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Weathering locations have a visible cause.
|
||||
- Large forms still read from the approach.
|
||||
- Exact seed and patch anchors recorded for reproducibility.
|
||||
|
||||
Avoid:
|
||||
|
||||
- Independent random choice at every voxel.
|
||||
- A universal percentage of damage on every material.
|
||||
- Replacing load-bearing full cubes with stairs merely for texture.
|
||||
|
||||
Conceptual sources: [The Sky is No Limit](https://www.minecraft.net/en-us/article/sky-no-limit), [Build With It: Cobblestone!](https://www.minecraft.net/en-us/article/build-with-it--cobblestone-), [Build with It: Diorite!](https://www.minecraft.net/en-us/article/build-it--diorite-). Recipe geometry and sequence are our own proposal.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Implementation status
|
||||
|
||||
The first prototype has been built and tested on a real local Paper 26.2 server, including the graphical camera client in Prism and image delivery through MCP. It does not yet cover all of v0.1 in the [design](DESIGN.md): the first authenticated Codex turn over ACP remains untested.
|
||||
The first prototype has been built and tested on a real local Paper 26.2 server, including the graphical camera client in Prism and image delivery through MCP. It does not yet cover all of v0.1 in the [design](DESIGN.md): authenticated ACP text replies, MCP reads, and a one-block build/inspect/undo cycle are verified; broader model-driven construction and cancellation tests remain.
|
||||
|
||||
## Implemented components
|
||||
|
||||
@@ -8,13 +8,15 @@ The first prototype has been built and tested on a real local Paper 26.2 server,
|
||||
|
||||
`paper-plugin` binds the core to the server thread and checks the owner, world, epoch, region, and protected parts. HTTP is available only on loopback, with separate administrator and agent keys. The plugin registers `/ai`, maintains a small chat queue, sends replies only to the initiator, and controls observer teleportation. The journal and metadata live in the plugin directory; game blocks remain vanilla.
|
||||
|
||||
`bridge` exposes 14 MCP tools over stdio, accepts Paper chat events, and launches the pinned `codex-acp`. Conversations are separated by player and project, queues are serialized, and stopping propagates to ACP. The session ID and a compact summary are persisted. The agent receives a separate MCP token; the administrator key stays with the Bridge. Large responses are bounded, and images are delivered as MCP image content.
|
||||
`bridge` exposes 19 MCP tools over stdio, accepts Paper chat events, and launches the pinned `codex-acp`. Conversations are separated by player and project, queues are serialized, and stopping propagates to ACP. The session ID and a compact summary are persisted. The agent receives a separate MCP token; the administrator key stays with the Bridge. Large responses are bounded, and images are delivered as MCP image content. The two material discovery tools return bounded search pages and property domains on demand, without placing the full registry in context.
|
||||
|
||||
`camera-mod` contains a local HTTP Worker and Minecraft 26.2 framebuffer capture. The camera waits for spectator mode, the requested position and dimension, neighboring chunk availability, and frame stabilization. HUD/FOV settings are restored after capture or failure. Mixin integration, server-side teleportation, and valid PNGs from several viewpoints have been tested in a real Prism client. The owner and observer used the same UUID.
|
||||
|
||||
## Verified checks
|
||||
|
||||
Current build: **100 automated tests passing** — 52 in the core, 17 in the Paper module, 20 in the Bridge, and 11 in the camera module. Both JARs have been built; Java results are in Maven/Gradle XML reports, and the combined log for this run is `.runtime/build-final.log`.
|
||||
The original full build passed **100 automated tests** — 52 in the core, 17 in the Paper module, 20 in the Bridge, and 11 in the camera module. A later Bridge run passed **40 checks**, including nested cases. Those historical regressions cover streamed text, Code Mode configuration migration, MCP startup failures, and narrowly scoped one-use approvals. Both JARs were built; Java results are in Maven/Gradle XML reports, and the combined log for that run is `.runtime/build-final.log`. Validation of the current registry expansion is recorded separately in [MATERIALS.md](MATERIALS.md#validation).
|
||||
|
||||
The registry expansion passed **145 Maven tests and 47 Bridge checks**. Its isolated Paper 26.2 probe described and roundtripped all **1,196 registered block defaults**, including 186 block-entity defaults, and passed **5,392 independent-property variants** through fresh placement, same-material changes and snapshot restoration. Live HTTP tests additionally verified inventory-preserving edits, conflicts on changed block-entity data, new-material schematic rotation and exact chest/sign/pot restoration after a server restart. These results cover the isolated integration server; lobby deployment is tracked in the material validation report.
|
||||
|
||||
Automated Java tests cover geometry, limits, idempotency, read dependencies, conflicts, interrupted slices, cancellation, undo, journaling, disk failures, recovery, and individual HTTP/NBT contracts. Bridge tests use real MCP stdio and a mock ACP agent for conversations, permissions, cancellation, and resumption. Camera tests cover HTTP authentication and validation without launching a graphical client.
|
||||
|
||||
@@ -23,36 +25,44 @@ The following scenarios passed on real Paper in a separately created test world:
|
||||
- A block changed after plan preparation: the operation ends in a conflict and preserves the other edit.
|
||||
- An explicit dependency changed: application stops before writing.
|
||||
- Construction, a retry with the same key, and checked undo; undo is rejected after a later external edit.
|
||||
- Operation cancellation, protection of an unsupported source block, and rejection outside the allowed area.
|
||||
- Operation cancellation, protection of a then-unsupported source block under the original palette policy, and rejection outside the allowed area.
|
||||
- Forced termination of the test's own server process during a 4096-block operation, restart, and `recovery_required` without automatic replay.
|
||||
- Administrative recovery review: the agent key is denied, a stale digest is rejected, and abandonment preserves current world contents and permits new operations after the decision is written to disk. Part protection does not prevent review, but continues to prohibit writes to the part.
|
||||
- A hollow cube through real MCP: 26 blocks; `.schem` export, the asset library, undo, import at the same anchor, and another undo restoring 27 blocks of air.
|
||||
- A missing camera returns an error without substituting an image.
|
||||
- After installing the mod in Prism, a 575-block tower was built and four real 1280×720 captures were obtained. The last completed the full Camera → Paper → Bridge → MCP ImageContent path. The first request failed when the viewpoint changed; a retry with the client stationary succeeded. [Test report](ONE_CLIENT_TEST.md).
|
||||
- A reference-based Gothic hall was built, photographed, and polished through checked recipes: 30,125 final blocks and 53 lanterns. Final verification covered 30,843 positions, including 718 removals; grass growth on eight newly planted soil blocks was explicitly recorded. Day, dusk, and interior captures were inspected. Nonpersistent leaves and waterlogged lantern states were rejected by the updated live Paper policy. [Build report](builds/GOTHIC_HALL.md).
|
||||
- A reference-based Gothic hall was built, photographed, and polished through checked recipes: 30,125 final blocks and 53 lanterns. Final verification covered 30,843 positions, including 718 removals; grass growth on eight newly planted soil blocks was explicitly recorded. Day, dusk, and interior captures were inspected. At that stage, the narrower Paper policy rejected nonpersistent leaves and waterlogged lantern states; the runtime registry expansion removes that material restriction. [Build report](builds/GOTHIC_HALL.md).
|
||||
|
||||
Real `codex-acp` completed `initialize` in a separate profile without login: ACP v1 and session loading support are confirmed. This does not yet test a model turn or tool calls after authentication. The tests did not invoke a model or consume model tokens.
|
||||
Real `codex-acp` completed `initialize` in a separate profile without login: ACP v1 and session loading support are confirmed. That initial check did not invoke a model or consume model tokens. Dedicated ChatGPT sign-in and real authenticated ACP calls to `project_context` and `region_inspect` have since passed against Paper. A subsequent model turn placed and verified one oak-plank block at (24, -60, -45), then used checked undo and verified air. Place operation: `ae0166c8-6d03-45ab-a80a-48b644819a2b`; undo: `751017ba-8e07-433b-b38d-71dcc8cf2330`. A local proxy restricted writes to the fixture and its recorded undo.
|
||||
|
||||
## Significant limitations
|
||||
|
||||
**Manual edits.** Comparing expected and current states protects against a different block immediately before writing. Known player place/break events also invalidate block ownership for undo, even if the player restores the previous material. There is no complete interception of changes from other plugins, commands, physics, or all A→B→A transitions; revisions of known external events are not yet persisted across restarts. This prototype cannot be treated as a universal system for merging arbitrary concurrent edits.
|
||||
**Manual edits.** Comparing expected and current states protects against a different block immediately before writing, including a block entity's extra data. A block-level `region_inspect` returns opaque `snapshot_id` digests for block entities; callers using `expected_blocks` must preserve those digests. Without a caller snapshot, preparation reads the current full state itself. Known player place/break events also invalidate block ownership for undo, even if the player restores the previous material. There is no complete interception of changes from other plugins, commands, physics, or all A→B→A transitions; revisions of known external events are not yet persisted across restarts. This prototype cannot be treated as a universal system for merging arbitrary concurrent edits.
|
||||
|
||||
**Crashes.** A JSON journal with fsync replaces the planned SQLite storage. The Minecraft world and our journal do not form a single transaction. An ambiguous operation after a crash blocks new writes. Explicit administrative review and abandonment using a fresh digest are available, without modifying the world and with ambiguous undo disabled. There is no automatic replay/rollback. The full journal is loaded at startup and does not yet support archival.
|
||||
|
||||
**Scope and performance.** One owner, project, and world; at most 4096 blocks and 512 explicit dependencies per plan. Writes are limited to 128 blocks with a target budget of up to 5 ms per slice; verification costs and the JVM prevent a hard tick-time guarantee. Full preparation of a bounded plan still runs on the server thread. Long-running load tests on a large server have not been conducted.
|
||||
|
||||
**Context.** Project context contains brief metadata, up to 20 operations, and up to 64 parts; exact blocks are read separately. `region_changes` currently returns `resync_required`. The agent therefore rereads selected areas; the promise of reading only deltas is not implemented yet. ACP conversations support persistence and summaries, but model usage has not been measured here.
|
||||
**Context.** Project context contains brief metadata, catalog counts/version, up to 20 operations, and up to 64 parts; exact blocks are read separately. Material IDs are discovered with `material_search` (16 results by default, 32 maximum). `material_describe` returns the default and each property's allowed values for one selected material, never every state permutation. Results can be reused while `catalog_version` is unchanged. `region_changes` currently returns `resync_required`. The agent therefore rereads selected areas; the promise of reading only deltas is not implemented yet. ACP conversations support persistence and summaries, but model usage has not been measured here.
|
||||
|
||||
**Blocks and schematics.** A limited set of 71 vanilla materials is allowed, including selected stairs and slabs, lanterns, iron chains (`iron_chain` in Minecraft 26.2), iron bars, stone brick walls, oak leaves, moss, gray and brown glass, glowstone, and gold blocks; `project_context` returns the exact list. Leaves are allowed only with `persistent=true` so that they do not decay without a tree. Waterlogged blocks, containers, doors, redstone, and other complex blocks are unsupported. Checks of surrounding blocks intentionally restrict use near unsupported environments. Sponge v2 `.schem` is implemented without a WorldEdit dependency: up to 4096 blocks, up to 64 files, rotations in multiples of 90°, no entities, block entities, or biomes, and no conversion between game versions. Its strict codec currently supports the original 61-material palette; the ten new decorative materials are available through normal building operations. Unsupported content is rejected rather than removed during export.
|
||||
**Blocks and schematics.** Ordinary building recipes use the running server's complete registered vanilla block catalog and exact state parser, including waterlogged blocks, fluids, containers, doors and redstone. Item-only catalog entries cannot be placed. Same-material block-state edits preserve existing block-entity data; new block entities use defaults. There is no raw NBT, sign-text, entity or inventory editor. Doors, beds and tall plants require explicit placement of every part; attachment, gravity and support rules still matter. Random ticks, fluid flow and redstone reactions are game behavior, not a fully journaled extension of direct block writes; checked undo does not promise to reverse all their consequences. Terrain recipes and brushes retain their narrower palette and scan policies.
|
||||
|
||||
Sponge v2 `.schem` is implemented without a WorldEdit dependency: up to 4096 blocks, up to 64 files and rotations in multiples of 90°. Its palette now uses registered block states and runtime rotation. Entity/block-entity NBT payloads, biomes and conversion between game versions remain unsupported. Export rejects every block entity rather than losing its extra data; import can place a block-entity block type from a state-only palette through the normal default/preservation rules. [Material workflow and precise boundaries](MATERIALS.md).
|
||||
|
||||
**Building language.** Box, line, cylinder, and repeat are available. Arches, arbitrary transforms, decorative palettes, JavaScript/Python execution, and automatic reconciliation of recipes with manual edits are not yet available. A registered part contains an exact mask of blocks actually written, not its entire bounding box.
|
||||
|
||||
**Camera.** A configured spectator client is required; in the tested scenario this is the same player as the project owner. During capture, that player cannot continue normal building. Game mode and original position are not restored automatically. Frame readiness is heuristic: `serverRevisionVerified: false`. Neighboring chunk availability and stable rendering do not prove receipt of all server updates. Real construction and captures have been tested; third-party shaders, disconnection during capture, and all forms of window freezing have not yet been tested.
|
||||
|
||||
**ACP.** Separate HOME/CODEX_HOME directories are used; extra integrations, shell, and forwarding of the administrator token are disabled. This is not OS-level isolation. The pinned adapter maps `read-only` mode to `workspace-write`; temporary paths may remain accessible, and the pinned CLI keeps the `unified_exec` flag enabled when `shell_tool` is disabled. Additional permission requests are currently rejected. Permissions for the dynamically supplied Minecraft MCP need testing in the first real turn. Details and diagnostics are in the [Bridge README](../bridge/README.md).
|
||||
**ACP.** Separate HOME/CODEX_HOME directories are used; extra integrations, shell, and forwarding of the administrator token are disabled. This is not OS-level isolation. The pinned adapter maps `read-only` mode to `workspace-write`; temporary paths may remain accessible, and the pinned CLI keeps the `unified_exec` flag enabled when `shell_tool` is disabled. The bundled Code Mode host is enabled for models that require it. Correlated calls to known Minecraft tools receive one-use approval during the active owner request; other permission requests remain rejected. Details and diagnostics are in the [Bridge README](../bridge/README.md).
|
||||
|
||||
## Next acceptance stage
|
||||
|
||||
1. The user logs in to the dedicated Codex profile, connects to Paper, and binds the owner. Test a small build requested through in-game `/ai`, response streaming, MCP use, and cancellation.
|
||||
1. Extend the verified ACP one-block roundtrip to larger in-game `/ai` builds, real cancellation, and reconnect recovery.
|
||||
2. Extend the verified single-client Prism scenario: test disconnection/freezing during capture and convenient switching between building and camera use.
|
||||
3. Extend the demonstrated build → inspect → correct cycle to an authenticated ACP model turn, then reassess release readiness, limits, context deltas, and geometry extensions.
|
||||
3. Extend the demonstrated procedural Gothic-hall workflow to larger authenticated ACP builds, then reassess release readiness, limits, context deltas, and geometry extensions.
|
||||
|
||||
## Terrain toolkit
|
||||
|
||||
Deterministic terrain recipes now support hills, ridges, plateaus, dry basins/channels and terraces. `terrain_preview` returns a native heightmap and cached recipe ID; `terrain_prepare` creates a checked tile plan for the existing apply/undo pipeline. Offline previews and bounded resumable batches are available through `scripts/terrain.py`. See [Terraforming tools](TERRAFORMING.md) for examples, safety semantics and scale limits.
|
||||
|
||||
`terrain_brush_prepare` additionally edits existing terrain relatively: raise/lower, flatten and snapshot-based smoothing, with soft edges, preserved columns, native before/after previews and checked read dependencies. See the relative-brush section of the terraforming guide.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Runtime materials
|
||||
|
||||
Ordinary building recipes accept every vanilla block type and valid block state registered by the running Minecraft server. The old fixed building palette has been removed. The same catalog also discovers items, but an item without a block form cannot be placed by `build_prepare`.
|
||||
|
||||
This does not require loading the registry into the agent's context. `project_context.material_catalog` returns only the catalog version, counts and search limits. The block and item counts overlap: a material can have both forms. `material_search` retrieves a small page of candidate IDs; `material_describe` retrieves details for one selected material.
|
||||
|
||||
## Discover a detail, then build it
|
||||
|
||||
1. Read `project_context` for the authorized area, limits and material-catalog version. Inspect the local construction area.
|
||||
2. Search a relevant family, for example `material_search({"query":"spruce trapdoor"})`. Search is case-insensitive and every whitespace-separated token must occur in the material ID.
|
||||
3. Describe the chosen ID with `material_describe({"id":"minecraft:spruce_trapdoor"})`. Use the returned `default_state` and allowed property values to select its facing, half, open and waterlogged state.
|
||||
4. Compile bounded geometry with that state. Keep attachments, supports, surrounding clearance and every required half or part explicit.
|
||||
5. Prepare, inspect the returned plan summary, apply with a stable idempotency key, and poll `operation_status`. Read back the relevant states and inspect an actual camera image when available.
|
||||
|
||||
Search for 1–3 details at a time. Reuse already described defaults and property values while `catalog_version` is unchanged; repeat discovery after a version change or an unknown-state error. An unchanged catalog does not make a previous world snapshot current.
|
||||
|
||||
`material_search` accepts a query of at most 96 printable characters, `kind: "block" | "item" | "all"` (default `block`), `limit: 1..32` (default 16), and an optional cursor of at most 100 characters. A response contains `catalog_version`, normalized `query`, `kind`, `total`, a bounded `results` array of `{id, block, item}`, and an optional `next_cursor`. Cursors belong to the catalog version and query/kind; start a new search if either changes. An empty query still returns only one page.
|
||||
|
||||
`material_describe` accepts one exact `minecraft:` ID of at most 128 characters. A block entry returns `default_state`, `properties` mapping each property name to its allowed values, and optional compact `behavior` hints. These are individual property domains, not every Cartesian state combination. An item-only result returns `placeable: false` and no block state. Unknown materials and unavailable runtime property introspection fail explicitly.
|
||||
|
||||
No tool descriptions contain the complete registry. No call needs to describe all materials in advance. State strings are bounded to 1024 characters; ordinary MCP text results retain the 64 KiB response limit. The guidance shared by MCP and ACP explicitly requests focused searches and reuse of results. The full recipe cookbook is also kept outside the default prompt.
|
||||
|
||||
## Block states and extra data
|
||||
|
||||
A recipe state has the form `minecraft:block[property=value,...]`. The server validates IDs, property names and values and canonicalizes the result. Omitted properties use the server defaults. Fluids, waterlogged blocks, nonpersistent leaves, carpets, doors, redstone and block-entity block types are accepted when present in the runtime registry.
|
||||
|
||||
Block-entity data is separate from the state string. A chest's orientation is a state; its inventory is extra data. A sign's block type and rotation are states; its text is extra data. New block entities use their default data. A same-material state edit preserves existing extra data. Replacing a block entity with another material uses the normal checked snapshot and undo path; the original data stays in the server-side history, not in the agent prompt.
|
||||
|
||||
With `region_inspect(detail: "blocks")`, a block entity includes an opaque `snapshot_id` alongside `pos` and `state`. When supplying `expected_blocks`, include every desired position exactly once and copy each returned digest unchanged. A digest is mandatory for an existing block entity, even if it is empty or has default data. The server rejects omitted required digests and stale states/data before preparation. The digest is a lowercase 64-character SHA-256 value; it does not reveal inventory or NBT contents. If no `expected_blocks` array is supplied, preparation captures the current full state itself.
|
||||
|
||||
```json
|
||||
{
|
||||
"pos": {"x": 0, "y": 64, "z": 0},
|
||||
"state": "minecraft:chest[facing=north,type=single,waterlogged=false]",
|
||||
"snapshot_id": "<copy the exact 64-character digest from region_inspect>"
|
||||
}
|
||||
```
|
||||
|
||||
The digest above is a placeholder, not a valid request. Copy the complete inspected entry instead of inventing or regenerating its digest. A conflict is evidence of a changed world and requires a localized decision; do not read a new snapshot merely to force the old design through.
|
||||
|
||||
There is no arbitrary NBT, item-inventory, entity, sign-text, banner-pattern or decorated-pot-data editor. Discovering an item does not create an item stack or an item entity. Existing project-specific text displays and game-selection logic remain separate from these generic building tools.
|
||||
|
||||
## Placement behavior
|
||||
|
||||
Support for a block state does not construct its neighbors. Explicitly place both door halves, both bed parts, every tall-plant half and all other pieces required by a design. Give attached blocks valid support, supply valid flower substrates and check directional states. Decorative leaves should normally use `persistent=true`; accepting natural decay states does not make them suitable for a permanent facade.
|
||||
|
||||
Fluid flow, gravity, random ticks, plant growth, oxidation and redstone reactions are Minecraft simulation behavior. The operation journal and checked undo cover direct recorded writes and their checked snapshots, not every later simulation consequence. Use a bounded prototype with the required containment and support, then inspect after game updates before repeating a motif. A prepared plan alone is not evidence that a decorative assembly remains stable.
|
||||
|
||||
Terrain generation and relative brushes have separate policies. Their existing limited terrain palettes remain; brushes still reject fluids and structures in the scan window. Placing water through ordinary checked geometry does not add a fluid-aware terrain brush or hydraulic erosion API.
|
||||
|
||||
## Schematics and preservation
|
||||
|
||||
The Sponge v2 codec accepts registered block states and uses the runtime's block-state rotation behavior. Limits remain 4096 positions per asset, 64 local files, 1 MiB compressed input, 4 MiB decompressed NBT and rotations of 0/90/180/270 degrees.
|
||||
|
||||
Entity and block-entity NBT payloads are rejected on import. A state-only palette can name a block-entity block type; it follows ordinary new-default or same-material-preservation rules when prepared. Export refuses every block entity, including empty ones, rather than silently discarding its extra data. Biomes, unknown top-level fields, required mods and game-version conversion remain unsupported. A `.schem` therefore does not replace a complete world checkpoint.
|
||||
|
||||
Keep the recipe, inspected snapshots, operation receipts and useful photographs with an appropriate world backup. The historical Stage 07 snapshot contained 100 material IDs; the decoration recipe catalog preserves that dated capability basis for reproducibility. It does not limit the current server registry or need to be expanded into thousands of entries.
|
||||
|
||||
## Validation
|
||||
|
||||
The registry expansion passed validation against a separate Paper 26.2 build 123 integration server on 2026-09-13. Its catalog version is `00474abd322518089309784c`: **1,691 materials, 1,196 block types and 1,537 item types**. Block/item counts overlap. These are observations from that runtime, not fixed protocol constants.
|
||||
|
||||
Completed checks:
|
||||
|
||||
- Real MCP stdio advertised 19 tools and delivered catalog discovery responses. The catalog summary was 130 bytes of compact JSON; the measured full `project_context`, including recent operation history, was 3,375 bytes. A focused three-result copper-trapdoor search was 374 bytes and one candle description was 324 bytes. A default 16-result HTTP search page was 1,256 bytes. These are response byte measurements, not model token counts or a model-usage benchmark.
|
||||
- The runtime property probe described all 1,196 block types successfully. The largest description was 682 bytes, with at most seven properties on one block and at most 27 values in a property domain. No Cartesian state list was returned.
|
||||
- The final registry probe placed and read back all 1,196 default block states, including 186 block-entity defaults, with zero failures. It also passed 5,392 deduplicated cases that vary individual properties: every case passed fresh placement, a same-material state edit, and restoration of the original snapshot. The temporary fixture anchor was restored.
|
||||
- Eleven live HTTP scenarios passed: pagination and bounds, property domains, item-only distinction, new block/fluid apply and undo, paired door halves, private block-entity digests, same-material chest rotation with inventory preservation, stale inventory conflicts before preparation and during application, replacement without inventory drops, a waterlogged cherry-trapdoor schematic rotated 90 degrees, and refusal to export block entities.
|
||||
- A further check after a real Paper restart used checked undo to restore exact chest, sign and decorated-pot snapshot digests. The chest retained its seven diamonds and the sign retained the known fixture text `MCP snapshot test`.
|
||||
- The full Maven suite passed 145 tests and the Bridge suite passed 47 checks, including nested cases. Bridge coverage includes bounded discovery transport, state/NBT limits, block-entity snapshot digests, scoped tool approvals and one-time delivery of changed instructions to resumed sessions. After the final narrow structure-block mode fix, the Paper/core package and its tests passed again.
|
||||
|
||||
The probe covers every registered default state and individual-property variants, not the Cartesian product of all properties or long-running redstone/fluid behavior. The live HTTP scenarios separately verify the operation journal, stale-data conflicts and restoration after restart.
|
||||
|
||||
**Lobby deployment:** the updated plugin is running in `shacraft-lobby-v2`. A fresh backup was made while the server was stopped. After restart, the project, world ID and epoch, selected region, operation count and part count matched the previous context; recent operations remained applied. Real MCP stdio advertised all 19 tools and successfully returned the new catalog, search and descriptor. The chat Bridge was restarted with its existing state directory, and its ChatGPT login remained available. These deployment checks were read-only and did not modify the lobby's construction.
|
||||
|
||||
The [verification receipt](references/material-registry-verification.json) preserves the registry counts, test coverage, response sizes and deployed plugin digest without private credentials or block-entity contents. On the lobby, the full `project_context` measured 4,840 bytes with its history; its catalog summary remained 130 bytes. Context size still depends on the requested world information, rather than just the catalog.
|
||||
|
||||
Local evidence lives in `.runtime/material-test-server/live-receipt.json`, `.runtime/material-test-server/mcp-receipt.json` and `.runtime/material-test-server/plugins/MaterialRegistryProbe/report.json`. These test-runtime paths are not distributed assets or links to files in Git.
|
||||
|
||||
## Repeating the live fixture
|
||||
|
||||
The separate [registry probe](../scripts/material-registry-probe/README.md) retains the source and builder for the exhaustive default-state and individual-property checks. Its helper plugin is opt-in and belongs only in the disposable test server; it is not part of the production plugin.
|
||||
|
||||
[`scripts/test-materials-live.py`](../scripts/test-materials-live.py) is an opt-in integration fixture, separate from ordinary automated tests. It changes and restores bounded test blocks, uses known chest/sign data, and exercises a real server restart. It does not create or launch a server.
|
||||
|
||||
Provision a separate Paper test server in `.runtime/material-test-server` with project `material-integration`, a region whose maximum X is 511, local automation enabled for the `console` principal, HTTP on `127.0.0.1:18765`, and RCON on `127.0.0.1:25586`. The operator supplies that server's private agent and RCON credentials in its local `test-access.json`; keep the file outside Git. The fixture checks the project and region before mutation and is intentionally not configurable to point at the lobby.
|
||||
|
||||
With that isolated server running, invoke from the repository root:
|
||||
|
||||
```bash
|
||||
python3 scripts/test-materials-live.py before-restart
|
||||
```
|
||||
|
||||
After the phase finishes and writes `live-receipt.json`, cleanly stop and restart **that same isolated test server**, preserving its world, plugin data and journal. Then run:
|
||||
|
||||
```bash
|
||||
python3 scripts/test-materials-live.py after-restart
|
||||
```
|
||||
|
||||
The second phase consumes the saved operation receipt and checks restoration after restart; running it twice is not a fresh test. The fixture leaves known test fixtures and an exported schematic in the isolated world/library. It must not be run against a shared lobby or treated as a read-only health check.
|
||||
@@ -12,12 +12,26 @@ Success: `{ "ok": true, "result": { ... } }`. Error: `{ "ok": false, "error": {
|
||||
|
||||
The administrator key is required for `chat_poll`, `chat_reply`, `recovery_review`, and `recovery_abandon`. The agent key permits world tools within the bound owner's project, but not administrative recovery. The MCP process injects `player_id/project_id` from its configuration; the model is not asked to supply them. The plugin rechecks the owner and their current permissions. An isolated test world can explicitly enable `allow-local-automation` with the `console` principal; administrative recovery operations also pass through this scope check.
|
||||
|
||||
## Material discovery
|
||||
|
||||
`project_context.material_catalog` contains `{version, materials, blocks, items, search_default_limit, search_max_limit}`. Block and item counts overlap for materials that have both forms. This replaces the expanded `supported_materials` list; no material registry or property list is sent in project context.
|
||||
|
||||
`material_search` and `material_describe` are read-only, idempotent MCP tools available within the same authorized project scope. `material_search` accepts optional `query` (at most 96 printable characters), `kind: "block" | "item" | "all"` (default `block`), `limit: 1..32` (default 16), and an opaque `cursor` of at most 100 characters. Search matches all whitespace-separated query tokens against material IDs, case-insensitively. It returns `{catalog_version, query, kind, total, results: [{id, block, item}], next_cursor?}`. A cursor belongs to its catalog version and normalized query/kind; changed filters or stale catalogs require a new search. Empty queries remain paginated.
|
||||
|
||||
`material_describe` accepts `{id: "minecraft:material_name"}` with a maximum ID length of 128 characters. It returns `{catalog_version, id, block, item, placeable, default_state?, properties?, behavior?}`. Block entries include the full default state and `properties: {name: [allowed_value, ...]}`; the response never enumerates the Cartesian product of all property combinations. Optional behavior hints identify features such as gravity, fluids, attachment, multiple parts or block-entity data. Item-only entries return `placeable: false` without block states. Direct RPC also accepts an ID without the `minecraft:` prefix; MCP requires the explicit namespace. Unknown IDs or unavailable property introspection fail explicitly.
|
||||
|
||||
Callers should search a focused family and describe only 1–3 selected materials, reusing those results while the catalog version remains unchanged. See [MATERIALS.md](MATERIALS.md) for the building workflow and behavior limits.
|
||||
|
||||
## Reading and writing
|
||||
|
||||
`region_inspect` requires `min/max` as `{x,y,z}` and `detail: "summary" | "blocks"`. Coordinates are inclusive integers. The prototype limit is 4096 positions. The response contains a palette with counts and, for `blocks`, exact states. Data comes from loaded chunks without implicit world generation.
|
||||
`region_inspect` requires `min/max` as `{x,y,z}` and `detail: "summary" | "blocks"`. Coordinates are inclusive integers. The prototype limit is 4096 positions. The response contains a palette with counts and, for `blocks`, exact `{pos, state, snapshot_id?}` entries. Block entities include a lowercase 64-character SHA-256 `snapshot_id` covering their captured data; NBT and inventory contents are not returned to the model. Data comes from loaded chunks without implicit world generation.
|
||||
|
||||
`build_prepare` accepts `recipe: {version: 1, operations: [...]}`, an optional `dependencies` array, and an optional `part_id` for an exact part mask. Geometry: `box(min,max,block,hollow?)`, `line(from,to,block)`, `cylinder(center,radius,height,block,hollow?)`, `repeat(count,offset,operations)`. This is a JSON description, not executable JavaScript.
|
||||
|
||||
Ordinary recipes accept registered vanilla block IDs and their valid properties as `minecraft:block[property=value,...]`, at most 1024 characters. Omitted properties use the runtime defaults; the server validates and canonicalizes the result. Item-only materials, unknown IDs/properties/values and raw NBT are rejected. Same-material state edits preserve existing block-entity data; a newly created block entity uses defaults. There is no inventory, sign-text or entity-data editor.
|
||||
|
||||
Callers planning from an earlier survey can supply `expected_blocks: [{pos: {x,y,z}, state: "minecraft:grass_block", snapshot_id?: "..."}, ...]`. When present, the array must cover every compiled desired position exactly once (maximum 4096). Copy each inspected `snapshot_id` unchanged: it is required for existing block entities, including those with default or empty data. The server checks these original states and digests in the same main-thread task that prepares the plan, returning `stale_snapshot` if any have changed; a required digest that was omitted is an invalid request. Without `expected_blocks`, preparation captures the current full state itself. `project_context.checked_expected_blocks` advertises support. This closes the caller-inspection-to-preparation gap; the usual comparison before applying still protects the prepared plan.
|
||||
|
||||
Preparation returns `plan_id`, `plan_hash`, `changed_blocks`, `region`, and `expires_at`. The full plan and original blocks stay in the journal. `build_apply` accepts this ID, hash, and an `idempotency_key` that remains constant for that logical call. Repeating the same call returns the same operation. A different plan requires a different key.
|
||||
|
||||
`operation_status(operation_id)` returns status, counters, and a bounded sample of conflicts. `operation_cancel` stops subsequent slices. `operation_undo_prepare` creates a reverse plan; it goes through the normal `build_apply` and checks. Losing the response to an apply request does not justify creating a different key and repeating the write: first check `project_context` or repeat the original call with the same key.
|
||||
@@ -26,6 +40,8 @@ Preparation returns `plan_id`, `plan_hash`, `changed_blocks`, `region`, and `exp
|
||||
|
||||
Before a write, the actual states of target blocks and declared dependencies are checked; the check runs again after the intent is persisted. A mismatch is never overwritten automatically. Undo also accounts for known subsequent writes by our operations, even when a block's value again matches the earlier result.
|
||||
|
||||
Multi-block structures require explicit states for every part: placing a door's lower half does not construct its upper half. Support and attachment constraints remain the caller's design responsibility. Fluids, gravity, random ticks, plant growth and redstone may cause later game changes. The journal and checked undo cover the direct operation's recorded writes, not every resulting simulation effect.
|
||||
|
||||
The plugin observes uncancelled `BlockPlaceEvent` and `BlockBreakEvent` events in the configured world. Such an event revokes the earlier operation's right to undo that block, including an edit followed by a change back to the original state (ABA). These notifications are held only in the current process's memory. After a restart, and for external paths without an observed event, current-content checks remain in place; a complete history of other plugins' actions is not promised.
|
||||
|
||||
A full server-side journal of external changes is not implemented yet. `region_changes` returns `resync_required`; the agent uses fresh, bounded reads. This is an explicit prototype limitation.
|
||||
@@ -48,11 +64,11 @@ The implemented RPC names are `asset_list`, `schematic_export`, and `schematic_i
|
||||
|
||||
`asset_list` accepts an optional `query` for case-insensitive name search and returns `{ "assets": [...] }`. Each entry contains `assetId`, `name`, `width`, `height`, `length`, `blockCount`, `dataVersion`, `offset`, `sha256`, and `bytes`. The catalog holds up to 64 files; every file is validated when read, so a corrupt schematic may cause the entire list request to fail.
|
||||
|
||||
`schematic_export` accepts `name`, inclusive `min/max`, and an optional `origin`. The name contains 1–64 printable characters. `origin` defines the schematic's anchor point and defaults to `min`. The server reads a dense rectangular region, including air, within the current project area and the `max_plan_blocks` limit (at most 4096). Unsupported blocks and entities other than players cause rejection; players are not written to the schematic. The result is one object with the same metadata fields as `asset_list`. The file remains in the `schematics` subdirectory of the plugin's data directory; the RPC returns neither its contents nor an arbitrary path.
|
||||
`schematic_export` accepts `name`, inclusive `min/max`, and an optional `origin`. The name contains 1–64 printable characters. `origin` defines the schematic's anchor point and defaults to `min`. The server reads a dense rectangular region, including air, within the current project area and the `max_plan_blocks` limit (at most 4096). Registered block states are accepted. Block entities, including empty ones, and entities other than players cause rejection; their data is never silently discarded, and players are not written to the schematic. The result is one object with the same metadata fields as `asset_list`. The file remains in the `schematics` subdirectory of the plugin's data directory; the RPC returns neither its contents nor an arbitrary path.
|
||||
|
||||
`schematic_import_prepare` accepts `asset_id`, `target: {x,y,z}`, and an optional `rotation: 0 | 90 | 180 | 270` (default 0). Rotation is clockwise when viewed from above, around the `target` anchor; the saved `offset` is taken into account. Supported stair directions and log/pillar axes are also transformed. The result is the usual `plan_id/plan_hash/changed_blocks/region/expires_at`; writing requires a separate `build_apply`. Air in the schematic is part of the plan and can remove existing supported blocks. The area, original contents, surroundings, block policy, and conflicts are checked through the normal preparation and application path.
|
||||
`schematic_import_prepare` accepts `asset_id`, `target: {x,y,z}`, and an optional `rotation: 0 | 90 | 180 | 270` (default 0). Rotation is clockwise when viewed from above, around the `target` anchor; the saved `offset` is taken into account. Block-state orientation uses the runtime rotation behavior, including directional and axis properties. The result is the usual `plan_id/plan_hash/changed_blocks/region/expires_at`; writing requires a separate `build_apply`. Air in the schematic is part of the plan and can remove existing blocks. The area, original contents, surroundings, block policy, and conflicts are checked through the normal preparation and application path. State-only palette entries for block-entity block types use the same new-default or same-material-preservation rules as ordinary recipes.
|
||||
|
||||
A limited subset of Sponge Schematic v2 is supported: gzip and NBT with a palette of vanilla building block states. Limits are 4096 positions, a 1 MiB compressed file, and 4 MiB of decompressed NBT. Entities, block entities, biomes, unknown top-level fields, required mods, and other format versions are rejected. A `DataVersion` newer than the current server is not accepted; there is no DataFixer conversion. Full compatibility with every WorldEdit schematic is not claimed.
|
||||
A limited subset of Sponge Schematic v2 is supported: gzip and NBT with a palette of registered vanilla block states. Limits are 4096 positions, a 1 MiB compressed file, and 4 MiB of decompressed NBT. Entity and block-entity NBT payloads, biomes, unknown top-level fields, required mods, and other format versions are rejected. A `DataVersion` newer than the current server is not accepted; there is no DataFixer conversion. Full compatibility with every WorldEdit schematic is not claimed.
|
||||
|
||||
An external `.schem` can be placed in the local schematic directory in advance with a name matching `[A-Za-z0-9][A-Za-z0-9_-]{0,63}.schem`; its filename stem then serves as `asset_id`. Arbitrary paths, network URLs, and symbolic links are not accepted. The prototype does not support file uploads through RPC.
|
||||
|
||||
@@ -71,3 +87,9 @@ Paper serializes requests, moves the configured spectator observer, and sends a
|
||||
Captures include a heuristic assessment of chunk/frame readiness. `serverRevisionVerified: false` remains in place until strict client acknowledgement is implemented. A missing camera, a timeout, or failure to obtain a fresh frame never counts as visual success.
|
||||
|
||||
Worker details: [camera-mod/README.md](../camera-mod/README.md). Threading and journal: [world-core/README.md](../world-core/README.md). ACP and isolation: [bridge/README.md](../bridge/README.md).
|
||||
|
||||
## Terrain
|
||||
|
||||
`terrain_preview(recipe,resolution?)` renders and caches a bounded deterministic height-field recipe without reading or writing world blocks. It returns a native PNG through the Bridge, a content-addressed `terrain_id`, tile layout and sampled statistics. The cache holds 32 recipes until restart. `terrain_prepare(terrain_id,tile_index)` prepares one bounded tile against live natural terrain and returns either the usual immutable plan summary plus `tile_bounds`, or `status: empty` with zero changes. Existing `build_apply`, cancellation, conflict checks and checked undo apply unchanged. The terrain language, limits, preserve masks and local batch workflow are specified in [TERRAFORMING.md](TERRAFORMING.md). Preview output is explicitly marked `world_verified: false`.
|
||||
|
||||
`terrain_brush_prepare(brush)` prepares a relative edit of an existing live surface. Its bounded scan is stored as read dependencies (up to 4096), and the response combines a native before/after/delta image with a normal plan summary. `plan_state: empty` has no plan ID. Actions are raise, lower, flatten and smooth. Input, scan-window semantics and limitations are documented in [TERRAFORMING.md](TERRAFORMING.md#relative-brushes-on-existing-terrain).
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Shacraft arrival square — garden finish
|
||||
|
||||
Stage 03 finishes **zone 01** on the existing foundations. The clock station and other districts keep their previous construction state.
|
||||
|
||||
The main reference is sheet 03, *Clock station and arrival square*, supported by sheets 01, 02, 10 and 11. Its defining features are a pale open apron, the green Shacraft emblem, concentric hexagonal paving bands, evergreen planting islands, pink and white flowers, benches and warm garden lighting. The concept images guide the composition; they are not measured Minecraft drawings.
|
||||
|
||||

|
||||
|
||||
## Built composition
|
||||
|
||||
- The former square setting-out grid is replaced with a cream paved field and three flush hexagonal contour bands.
|
||||
- The actual green artwork from the preserved `logo-180.png` is sampled into a 25 × 35 block footprint. The open hexagonal shield and S replace the earlier placeholder medallion.
|
||||
- Six low stone-edged planting islands contain six custom conifers, six smaller topiary trees, low evergreen clusters and 258 flowers. The southern beds extend to the balustrade, eliminating isolated strips of paving behind them.
|
||||
- Six spruce benches face the square. Each has a three-block level approach through the planter edge. The benches are decorative; no sitting interaction is added.
|
||||
- Sixteen supported lantern fixtures use slender posts and green copper hoods. Eight former light piers are replaced, including a balanced pair beside the arrival stair.
|
||||
- Obsolete zone 01 survey stakes, its block number and its floating survey label are retired. Other district markings remain.
|
||||
|
||||
Paving remains at block Y95, with the main walking plane at Y96. The central sightline toward the clock station and all existing road connections are preserved. No terrain platform, station construction or new fountain is introduced by this stage.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Checked construction
|
||||
|
||||
Before the work, the full server surface matched Stage 02 at all 589,824 columns. A fresh 359,964-voxel plaza survey included soil, foundations, fixtures and canopy air; 294,516 overlapping voxel states also matched the previous checkpoint. Unexpected manual edits stop the compiler, and the normal checked editor protects against changes between preparation and application.
|
||||
|
||||
The final candidate consists of 4,734 changed blocks. Its review checks persistent leaf distances, valid soil under flowers, grounded trunks, lantern support, bench approaches and 1.8 blocks of walking headroom. Grass beneath opaque tree trunks is replaced with dirt explicitly so random ticks do not change the recorded foundation state. The road audit starts with the previously verified usable surface, excludes actual planting and furniture, and checks the original eight route widths and their connectivity.
|
||||
|
||||
Final verification uses fresh observed blocks and a new full server map. These sequential surveys are not atomic world snapshots; construction remains idle during them. Perspective screenshots are real Fabric client captures, with heuristic render readiness. Geometry checks do not represent a human playtest or a full simulation of moving-player collisions.
|
||||
|
||||
The [final live report](references/shacraft-arrival-garden-verification.json) passed after **4,811 checked writes in 12 batches**, including a 77-block finishing pass that gives the conifers continuous green tips. All **4,734 final changed states** matched the latest observation. It checked 537 persistent leaf states, 258 supported flowers, 73 connected trunk blocks in 12 trees, 16 lantern supports and **4,955 clear walking samples**. All eight inherited road widths and 18 bench-front positions passed, with no isolated usable paving. The full **589,824-column map** matched the expected world, and all 41,467 visible water columns remained unchanged.
|
||||
|
||||

|
||||
|
||||
## Reusable tools
|
||||
|
||||
- `scripts/plaza-assets.py`: deterministic conifers, oriented benches and supported copper-hood lantern fixtures. Functions return local voxel states and never write to a world.
|
||||
- `scripts/build-shacraft-plaza.py`: composes the approved garden geometry, real brand mask and static assets into a checked recipe. It preserves foundation elevations and existing road treads.
|
||||
- `scripts/verify-plaza.py`: independent candidate/live validation of planting, fixtures, walking clearance and the inherited road network.
|
||||
- `scripts/layout.py`: bounded checked placement, receipts and conflict-aware reverse undo.
|
||||
|
||||
The Paper policy now supports 99 materials. The added flowers are single-block decorative species; new copper forms are waxed. Existing restrictions on fluids, waterlogged states and nonpersistent leaves remain in force. The strict schematic codec retains its earlier material subset, so complete world checkpoints and voxel recipes are the preservation format for these gardens.
|
||||
|
||||
The final recipe, original observations and applied ledger are retained locally under `.runtime/plaza-stage03/`. The reference study and its earlier candidate are preserved separately from the finished state. Stage 02 remains available in the local Git archive and its full world backup.
|
||||
|
||||
The restorable final world checkpoint is `.runtime/projects/shacraft-arrival-garden-20260913/`, captured after an explicit save flush with automatic saving temporarily disabled and then re-enabled. It includes the full world container, matching plugin state, both stage ledgers and the earlier foundation receipts. Undo the apex polish ledger before the main garden ledger. The removed zone 01 text display is an independently recorded operator action rather than a block-journal change.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Shacraft arrival square — perimeter balustrade
|
||||
|
||||
Stage 04 completes the edge of the finished zone 01 garden with a continuous pale handrail, regularly spaced sandstone piers and connected stone balusters. It follows the existing hexagonal terrace while keeping the road approaches open. The garden, logo apron, lighting and clock-station construction retain their previous state.
|
||||
|
||||

|
||||
|
||||
## Boundary and heights
|
||||
|
||||
The balustrade contains **302 perimeter columns** split into **five uninterrupted fence runs**, separated by the existing approach openings. The runs contain 46, 80, 74, 71 and 31 columns. Forty-six cut-sandstone piers mark endpoints, principal corners and intermediate bays.
|
||||
|
||||
Paving remains at block Y95, with its walking plane at Y96. Balusters and piers occupy Y96. A bottom smooth-sandstone slab at Y97 forms a continuous handrail with its upper surface at **Y97.5**, 1.5 blocks above the plaza walking plane. Wall connections are explicit, including tall side connections beneath the handrail and reciprocal connections to six adjoining road-rail columns.
|
||||
|
||||
The boundary forms a cardinal block chain. Elbows close the diagonal steps so adjacent railing pieces meet along block faces. Where planting occupies the inner choice, the connector moves to the outer side. Twenty-two small outer columns have stone-brick footings rising from observed full support to cut-sandstone coping at Y95. They support individual elbows without adding another terrace.
|
||||
|
||||
Four existing southern planter-rim slab columns are incorporated into the new boundary. Planting contents, existing lamps, road treads and the clear approach widths are preserved.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Checked construction
|
||||
|
||||
`scripts/build-shacraft-balustrade.py` reads captured blocks and compiles a plan without changing the world. It refuses unexpected decoration at fence or handrail positions, protects inherited road cells, and finds observed footing support. `scripts/layout.py` checks expected states during preparation, journals bounded applications and verifies their results.
|
||||
|
||||
The editor made **692 checked writes in four batches**: 682 initial writes and ten finishing writes for the road-rail joints and buried soil. Twenty-one grass blocks beneath the new footings had already changed naturally to dirt; the remaining buried grass block was explicitly stabilized. The recorded final comparison covers **710 distinct expected states**, including those observed soil transitions. Their positions are preserved in `settled-soil.json` alongside the stage records.
|
||||
|
||||
The [final observed-world report](references/shacraft-balustrade-verification.json) passed. It checks the complete **556,308-voxel captured volume** against the baseline plus final plan, including **555,598 voxels outside the final changes**, with no mismatches. All 302 handrail columns, 46 piers, 22 grounded footings and six reciprocal rail joins passed. It also confirms all 1,172 preserved garden fixture states, **4,815 walking/headroom samples**, the original eight route widths and 32 bench-access columns.
|
||||
|
||||
The separate complete surface comparison matched all **589,824 map columns**, including 589,516 columns outside the edited area. All 41,467 previously visible water columns remained unchanged. The navigation graph reached all 57,352 surface samples across 14,338 usable columns.
|
||||
|
||||
These sequential voxel and surface captures are not atomic snapshots; construction was idle during final measurements. The volume check covers the supplied observed bounds. Route checks verify geometry, support and headroom, without simulating a moving player's complete collision body. Perspective images are actual Fabric client captures with heuristic render readiness.
|
||||
|
||||

|
||||
|
||||
## Tools and preservation
|
||||
|
||||
- `scripts/build-shacraft-balustrade.py` compiles the continuous perimeter, grounded elbows and explicit connections.
|
||||
- `scripts/verify-balustrade.py` independently checks the final rail, protected garden, approaches, navigation and complete observed volume.
|
||||
- `scripts/verify-foundation-survey.py` checks the full before/after server surface against the final column states.
|
||||
- `scripts/layout.py` provides checked placement, journal receipts and conflict-aware reverse undo.
|
||||
|
||||
Stage inputs, captured observations, final recipes and application receipts remain under `.runtime/balustrade-stage04/`. The archive separates initial computed previews and candidate checks from final Minecraft images and fresh observed-world reports.
|
||||
|
||||
The restorable checkpoint is `.runtime/projects/shacraft-plaza-balustrade-20260913/`. It contains the complete world container, matching plugin state and stage receipts. The world was explicitly flushed with saving temporarily disabled for the copy, and automatic saving was re-enabled afterward. All 137 recorded world-file hashes passed verification.
|
||||
|
||||
Previous references and stages remain in `/home/emil/Desktop/Shacraft-Lobby-Archive`. Raw observations, complete world saves, private plugin configuration, authentication data and journals stay outside its Git history. Undo the finishing ledger before the initial balustrade ledger, then process older stages only if requested. Natural soil transitions are recorded explicitly and must be considered during restoration; manual-edit conflicts stop checked undo.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Shacraft — foundations for zones 01 and 02
|
||||
|
||||
This document preserves the Stage 02 foundation checkpoint. Zone 01 has since received its [Stage 03 arrival garden finish](SHACRAFT-ARRIVAL-GARDEN.md).
|
||||
|
||||
This construction stage develops the arrival square and clock station in the approved natural v2 world. It also builds their local approaches. The other district reservations and future bridge spans remain at the survey stage.
|
||||
|
||||

|
||||
|
||||
## Levels and circulation
|
||||
|
||||
The arrival square and oval station forecourt use paving blocks at **Y95**, with the walking surface at Y96. The station floor uses blocks at **Y98**, with the walking surface at Y99. The station follows its actual hall, tower and pavilion footprint. Its western retaining wall has shallow blind arches and stone pilasters; solid backing remains behind the recesses.
|
||||
|
||||
A nine-block clear avenue links the square to the forecourt. A **21-block clear stair** rises to the station in half-block tread increments, with full landings. The local southern approach is nine blocks wide, the district connections seven, and the lake approach five. Curved roads use full transverse rows of correctly facing stairs, with separate edge coping. Lamps and balustrades sit outside the reserved clear lanes.
|
||||
|
||||
Finished road endpoints, expressed as X/Z and the supporting block's Y:
|
||||
|
||||
- Southern approach: `(1, 110)`, Y79, joining native ground at the same level.
|
||||
- Portal approach: `(-72, -46)`, Y89, joining native ground at the same level.
|
||||
- Lake approach: `(-90, 32)`, an east-facing bottom stair at Y84, stepping down to native full blocks at Y83.
|
||||
- Northwest station approach: `(-80, -77)`, Y85, joining native ground at the same level.
|
||||
- Eastern bridge approach: `(82, 8)`, Y87.
|
||||
- Northeast bridge approach: `(82, -78)`, Y86.
|
||||
|
||||
The two bridge approaches end at temporary balustrades beyond the completed walking deck. Those barriers must be removed when the corresponding bridge spans are constructed. Beyond the other local endpoints, the remaining route markings reserve later work; they are not finished roads.
|
||||
|
||||

|
||||
|
||||
## Materials and construction
|
||||
|
||||
The plaza has pale sandstone paving, restrained stone courses and a flush green Shacraft medallion. The station has a stone structural floor with setting-out bands for the future hall, clock tower and end pavilions. Exposed bases combine deepslate, stone brick and andesite coping. Twenty-seven lantern piers light the edges.
|
||||
|
||||
Every deck has structural courses and solid support down to the observed original terrain. The generator does not create a new rectangular terrain platform outside the building footprint. Small local cuts clear the paving and headroom; the original mountains and watercourses remain outside this construction scope.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Checked workflow
|
||||
|
||||
The design was compiled against a **1,981,407-voxel live before-survey**, including underground support and headroom. Unexpected differences from the original terrain plus known survey markers stop compilation. The placement driver also checks the caller's expected states atomically during preparation, journals bounded operations and verifies each applied batch.
|
||||
|
||||
The stage inputs and receipts are retained locally under `.runtime/foundations-stage02/`. A fresh post-build voxel survey checks standing heights and 1.8 blocks of vertical clearance on the actual full blocks and both stair treads. An independent half-block navigation graph checks route connectivity and full-width cross sections. These are geometric and block-state checks, not a simulated moving player or a manual playtest.
|
||||
|
||||
The full server-derived before/after surface maps are compared across the 768 × 768 world. The verifier applies air edits before finding the new top block, so lowered surfaces are checked correctly. Surface maps and sequential voxel surveys are not atomic; construction is kept idle while final measurements are taken. Perspective images use the real Fabric client, whose render readiness remains heuristic.
|
||||
|
||||
The [completed verification report](references/shacraft-foundations-verification.json) records **79,081 checked writes across 67 journalled batches**, including a 408-block refinement that moved the western pilasters clear of the arches and darkened their intact backing. All **78,673 final changed voxel states** matched the post-build survey; **15,708 standing-height and headroom samples** passed. All **589,824 surface columns** matched the expected result, including **572,923 columns outside the edited area**. All 41,467 previously visible water columns remained unchanged, and no observed water blocks were replaced.
|
||||
|
||||
## Tools and preservation
|
||||
|
||||
- `scripts/foundation-study/design.py` and `geometry.py` define the elevations, contours, stairs and road corridors.
|
||||
- `scripts/build-shacraft-foundations.py` compiles observed terrain, approved geometry and materials into a checked block plan; it does not write to the world.
|
||||
- `scripts/layout.py` applies bounded batches and supports checked reverse undo through the stage ledger.
|
||||
- `scripts/foundation-survey.py` records actual voxel states and checks floor support and headroom.
|
||||
- `scripts/foundation-study/verify_geometry.py` independently checks planned navigation.
|
||||
- `scripts/verify-foundation-survey.py` compares actual surface maps with the complete edited column state, including excavation.
|
||||
|
||||
Original references and the marked-site checkpoint are preserved in the separate local Git repository `/home/emil/Desktop/Shacraft-Lobby-Archive`. Each completed stage has its own directory, source provenance and SHA-256 manifest. Restorable world backups remain outside Git, together with their matching private plugin state and operation journals.
|
||||
|
||||
The completed world checkpoint is `.runtime/projects/shacraft-foundations-01-02-20260913/`, captured with saving disabled after an explicit flush; normal saving was then re-enabled. It contains the full world container, including `world/dimensions/minecraft/shacraft_lobby_v2`, both relevant plugins and the construction ledgers. World-file checksums are recorded in `checkpoint.json`.
|
||||
|
||||
An undo must run newer stage ledgers before older ones and stop on manual-edit conflicts. Floating labels are separately managed entities; their operator commands are recorded with the stage artifacts rather than in the block journal.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Shacraft lobby site marking
|
||||
|
||||
This document preserves the **Stage 01 survey checkpoint**. The current world has progressed to [Stage 02: foundations and local roads for zones 01 and 02](SHACRAFT-FOUNDATIONS.md).
|
||||
|
||||
The existing `shacraft_lobby_v2` alpine terrain is now marked for the future lobby. This is a site survey: building contours, entrances, courtyards, paths, bridge deck reservations, height stakes and wayfinding. Buildings, playable minigames and finished paths have not been constructed.
|
||||
|
||||

|
||||
|
||||
## Visit
|
||||
|
||||
Connect to the private local server at `127.0.0.1:25575` using the **26.2 MCP Building** Prism profile, then use `/lobby`. Spectator mode is useful for inspecting the complete arrangement. The configured camera client can reconnect autonomously after a local server restart.
|
||||
|
||||
The planning envelope is 768 × 768 blocks. Natural mountains frame the developed valley; the terrain was not flattened to fit district rectangles. Surface contours replace a single observed top block. Raised lines reserve future bridge, pier and airship elevations while leaving the river below intact.
|
||||
|
||||
## Legend and coordinates
|
||||
|
||||
Coordinates below identify the districts in X/Z. The same two-digit numbers are drawn with blocks; lit colored posts carry nearby floating names.
|
||||
|
||||
- **01 · lime · arrival square · (0, 8).** Open hexagonal plaza and Shacraft medallion, with a direct view toward the clock station.
|
||||
- **02 · yellow · clock station · (-18, -119).** Hall, projecting tower, end pavilions and a smaller oval forecourt.
|
||||
- **03 · purple · portal concourse · (-201, -156).** Chamfered hall, six separate portal bays and a garden approach.
|
||||
- **04 · cyan · airship harbor · (179, -137).** Terminal, three west-facing piers at Y79 and a flagship reservation at Y92.
|
||||
- **05 · red · sky gardens · (207, 88).** Palm glasshouse, winter garden and observatory. White paths follow their surrounding courts.
|
||||
- **06 · orange · market quarter · (-195, 225).** Six small building plots, an open market square and individual entrance connections. These plots use the calmer southern shoulder.
|
||||
- **07 · white · arrival avenue · (8, 284).** Long southern approach and a final viaduct at Y65.
|
||||
- **08 · blue · lake waterworks · (-135, 43).** Pumping house, viewing terrace, Y51 boardwalk and two marked future stair approaches.
|
||||
- **09 · pink · scenic overlooks.** Five small viewpoints with optional trails, preserving the mountain slopes.
|
||||
|
||||
White paired lines reserve path edges. Main avenues are designed for nine-block clear corridors, the district loop generally seven, and smaller access paths three to five. Colored building boundaries are reservations, not final material choices. White cross-ties and tall end stakes show bridge deck widths and elevations. Labels identify the larger level changes where stairs or staged access will be needed. The current block contours alone do not certify walking access.
|
||||
|
||||
## What was placed and checked
|
||||
|
||||
- 41 geometric reservations and 48 route segments, including six bridges, three airship piers, six portal bays and six market buildings.
|
||||
- 16,967 distinct marker positions, applied with 17,040 checked writes across 36 journalled batches. The small second pass adds missing access links and extends bridge height stakes.
|
||||
- Eight district labels and eleven access/elevation labels, verified again after the server restarted.
|
||||
- Every batch was checked before planning, atomically against its caller snapshot during preparation, and after application. Stable operation keys and persisted receipts support resumption and checked undo.
|
||||
- Full before/after world surface comparison and targeted reads beneath obscuring markers are recorded in the [verification report](references/shacraft-layout-verification.json). The map is read from actual Paper blocks, not reconstructed from the noise generator.
|
||||
|
||||

|
||||
|
||||
## Autonomous map exporter
|
||||
|
||||
The optional terrain plugin implements this **server-console** command:
|
||||
|
||||
```text
|
||||
lobby map survey-name
|
||||
lobby status
|
||||
```
|
||||
|
||||
It writes `plugins/ShacraftTerrain/maps/survey-name.png` and `.json`. The PNG has one pixel per world column, north up, with material colors and height shading. The JSON includes actual surface heights, material palette and coordinates. Existing names are never overwritten. The exporter loads only existing chunks, reads one chunk per tick, and renders/writes the detached result asynchronously. It requires no Minecraft client or account.
|
||||
|
||||
The exporter reads chunks sequentially, so a map is not an atomic world snapshot. Construction was idle during the verification captures. A top surface also hides the water beneath a bridge; separate bounded RPC reads verified that water. Text display entities appear in game but are not included in this block-surface map.
|
||||
|
||||
This is an orthographic world map. Perspective screenshots still use the Fabric spectator client and its heuristic render readiness.
|
||||
|
||||
## Reproduce and revise
|
||||
|
||||
The reusable [spatial plan](../examples/layout/shacraft-lobby-layout.json) and [access refinement](../examples/layout/shacraft-access.json) describe reservations, routes, heights and intent. Their geometry was studied against the natural terrain before live surface data was used for placement.
|
||||
|
||||
- `scripts/mark-shacraft-layout.py` compiles the primary survey from a current surface map and authorized world scope.
|
||||
- `scripts/compile-layout-access.py` compiles the access refinements against the original survey and known primary marker states.
|
||||
- `scripts/layout.py report` describes bounded batches without touching a server. `prepare` checks the input without allocating expiring plans. `apply --execute` writes and verifies; `undo --execute` uses reverse journalled undo.
|
||||
- `scripts/render-layout-map.py` renders the labelled atlas from live surface data. Supplying `--blocks` explicitly produces a labelled **design preview** instead.
|
||||
- `scripts/verify-layout-survey.py` compares every visible column and checks hidden water through bounded live reads.
|
||||
|
||||
The local applied ledgers are `.runtime/layout-study/placement-final.json` and `.runtime/layout-study/access-ledger.json`, with adjacent immutable input snapshots. Undo the access ledger before the primary ledger. Manual changes can block undo and must be preserved and reviewed. The 19 text displays are a separate operator-managed layer with tag `shacraft_layout_v1`; they are not part of the block journal. An explicit undo of the whole survey also requires removing that tagged display layer in the same world.
|
||||
|
||||
A complete stopped-world backup, including plugin state and layout ledgers, is stored locally in `.runtime/projects/shacraft-marked-lobby-20260913/`. The earlier unmarked natural world remains backed up separately. Private configuration and authentication material are excluded from the public examples and report.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Shacraft clock station — vestibule and SMASH gallery
|
||||
|
||||
The clock station is built, furnished and verified in the local lobby world. Its two public floors contain the arrival vestibule and SMASH selection gallery, with enclosed personal lift cabins, warm architectural lighting and a closed island display. The complete world and matching plugin state are backed up.
|
||||
|
||||

|
||||
|
||||
The station occupies the existing irregular zone 02 foundation. Its 5,764 surveyed columns fit inside X **-74..40**, Z **-145..-83**. Pale stone arcades, green copper roofs, spruce ceilings, brass accents and warm lanterns continue the original clock-station reference. The roof silhouette rises from eaves at Y126 to a main ridge at Y140 and a clock-tower peak at Y158. Roof overhangs extend the complete building envelope to X **-76..42**, Z **-147..-81**.
|
||||
|
||||
The coordinate drawings and generated perspectives remain in `docs/references/zone02-interior-v1/`. Those images establish the visual direction; the saved layout and actual foundation geometry govern placement. They are not photographs of the completed building.
|
||||
|
||||

|
||||
|
||||
## Two complete public floors
|
||||
|
||||
The vestibule floor occupies block Y98, with players standing at **Y99**. The SMASH floor occupies block Y112, with players standing at **Y113**. The intermediate deck at Y111..112 is continuous. Decorative coffer ceilings close each room; the upper structural ceiling at Y124..125 separates both public floors from the inaccessible roof and clock tower.
|
||||
|
||||
The south entrance follows the existing X=-6 approach axis. Its clear opening spans X=-9..-3, retaining seven blocks of width. Matching square columns align vertically on both floors. Shallow cream corbels and spruce ceiling panels remain above the circulation space, while inset green and brass-colored floor bands guide the central approach. Side galleries contain spruce benches, small planted stone boxes and restrained floor mosaics. There are no additional unfinished public rooms, stairs to the attic or open shafts.
|
||||
|
||||
The interior generator owns only the two-block setback inside the actual foundation, at block Y98..123. It leaves the exterior wall and window layers to the exterior compiler. Its 5,064 interior columns contain fifteen benches, ten small topiary planters and 48 hanging lanterns, with three additional miniature trees in the diorama. The complete recipe combines the exterior and interior before selecting only changed states against the captured baseline.
|
||||
|
||||
Real Minecraft photographs prompted a lighting refinement. Sparse brown-glass floor tiles reveal concealed glowstone below the paving; matching small lenses recess into the plain spruce ceiling panels. The regular nine-block layout adjusts locally around beams, mosaics, furniture, controls and landing points. All lens and emitter replacements retain full-block collision and solid floor/ceiling separation. The enclosed diorama receives its own concealed lighting.
|
||||
|
||||

|
||||
|
||||
## Enclosed lift cabins
|
||||
|
||||
The lift housing is **X=-10..-2, Z=-124..-116** on both floors. Each stationary cabin has a five-by-five clear interior at X=-8..-4, Z=-122..-118 and a five-block-wide south opening. Both cabin floors remain solid. The space above each cabin is closed, so the lift provides no passage to a shaft or unfinished upper rooms.
|
||||
|
||||
Gold selector blocks sit on the north interior wall at **(-6, 101, -123)** and **(-6, 115, -123)**. Right-clicking a selector from inside the matching cabin changes floors for that player alone, arriving at **(-5.5, 113, -119.5)** or **(-5.5, 99, -119.5)** facing south. The listener checks the destination support and two air blocks before teleporting. The independent geometry audit checks four blocks of headroom.
|
||||
|
||||
`/station` enters the ground-floor vestibule. `/station 1` and `/station 2` select the two completed cabin stops. The commands were exercised on the local server and both resulting player positions were read back; a third stop was rejected. The physical mouse-click path is covered by listener and cabin-selection tests but was not exercised through an automated client click.
|
||||
|
||||
Zone 01's previous invisible boundary remains in place. Use the station entry command while that earlier garden enclosure is retained; opening the physical route is a separate checked edit.
|
||||
|
||||
Twenty-four persistent text displays provide the exterior name, lift instructions, floor names, numbered arena placeholders and rules panels. A repeated installation replaced the previous set without increasing the count. These separately managed entities are recorded outside the block recipe. Arena placeholders do not perform world transfers or matchmaking.
|
||||
|
||||
## SMASH gallery
|
||||
|
||||
Six seven-by-five selection alcoves line the north gallery at Z=-140..-136, with centres at X **-42, -32, -22, -12, -2 and 8**. A seven-block-deep clear aisle at Z=-135..-129 connects them. Each alcove has a small decorative island relief, a framed display and a reachable selection surface. Slots **01–06 remain unassigned**; they do not advertise invented arena worlds, live queues or active minigames.
|
||||
|
||||
The western display occupies **X=-42..-24, Z=-124..-110**, a 19-by-15 rectangle. Miniature planted islands stand above a solid illuminated base, enclosed by continuous pale stone and glass. Twelve full brown-glass floor lenses admit concealed light inside the display while maintaining its intact base. It is a closed display on an intact floor, with no playable void below it. The nearby west gallery explains damage, knockback and double jumping. The eastern wing provides waiting benches and an orange terracotta floor accent. No SMASH arenas are included in this construction stage.
|
||||
|
||||

|
||||
|
||||
## Verification and preservation
|
||||
|
||||
The completed stage required **84,446 checked writes across 98 batches** and has **83,924 final distinct changed states**. The final observed world matched every planned state. All **572,326 observed voxels outside the edits** also matched the baseline, completing the check of the full 656,250-voxel survey.
|
||||
|
||||
Independent observed-block checks pass for **4,796 vestibule walking columns** and **4,176 SMASH walking columns**, each with four blocks of clear headroom, and all **107 named furnishing and lift approaches**. Those counts include the exterior doorway columns; the interior generator alone reports 4,775 vestibule columns. Both cabins, both lift doorways, the main approach and the complete selection aisle were checked voxel by voxel. The fixture audit passed for 72 planned lantern supports, 78 persistent leaf states and thirteen rooted station/display trees.
|
||||
|
||||
The enclosure audit uses a temporary virtual cap at the intentional south entrance, then floods from both public floors and lift cabins. The cap is an analysis boundary, not a block placed across the doorway. Full glass and known full cubes stop the flood; partial and unknown shapes are treated as passable. The observed building has no connection from either furnished floor to the attic or clock tower. This static geometry audit does not prove lift behavior, spectator restrictions or administrative access control.
|
||||
|
||||
The complete visible-surface comparison passed for all **589,824 map columns**: 6,496 changed station columns and 583,328 unchanged outside columns. All **41,467 previously visible water columns** were preserved. The map was captured before the last lighting and nameplate polish. A separate final voxel comparison confirmed that all 602 subsequent polish differences lie below the exported roof surfaces, and that the actual final top blocks and surveyed blocks above them match the map for every station column.
|
||||
|
||||
The final plugin build passed 97 Maven tests; seven station-verifier tests and ten foundation tests also passed. World and surface captures are sequential rather than atomic. The four final photographs are actual Minecraft client captures with heuristic render readiness; their metadata does not claim a verified server revision. Observed block checks and runtime interaction receipts provide the separate measurement evidence.
|
||||
|
||||
`scripts/station-exterior.py` and `scripts/station-interior.py` generate the geometry. `scripts/build-shacraft-station.py` combines their owned regions, checks the observed baseline against the prior foundation checkpoint and compiles expected-state edits. `scripts/light-shacraft-station.py` generates the concealed lighting grid against the observed built station. `scripts/verify-station.py` independently checks the resulting circulation, enclosure and captured volume. Stage records are kept under `.runtime/station-stage07/`.
|
||||
|
||||
One interrupted batch verification exposed Minecraft's explicit `deepslate[axis=y]` state. The original receipt and manifest were retained. A fresh partial survey confirmed the applied prefix, and a separate remaining recipe continued from that observed state. This was a state-serialization correction at three miniature island tips; the retained recovery report records the exact positions and hashes.
|
||||
|
||||
The authoritative final recipe and metadata are `.runtime/station-stage07/final-v2/station.json` and `station.metadata.json`. The final observation is `after-complete.json.gz`, with `actual-complete-qa.json`, `complete-surface-verification.json`, `complete-surface-invariance.json` and `runtime-verification.json` recording the checks. The owner was returned to the vestibule at **(-5.5, 99, -92.5)** with spectator mode preserved.
|
||||
|
||||
The restorable backup is `.runtime/projects/shacraft-station-complete-20260913/`: **650 world and plugin files, 237,341,328 bytes**, with every recorded SHA-256 hash verified. Saving was flushed and temporarily disabled during copying, then automatic saving was re-enabled at 15:42:17 local time. `backup-receipt.json` records its location and verification.
|
||||
|
||||
The local Git archive preserves the final documents, generator and verifier snapshots, compact reports, reference plans, Minecraft photographs and surface maps. Complete world saves, raw voxel observations, private configuration, authentication, plugin binaries and placement journals remain in the separate local runtime backup. Earlier reference and construction checkpoints remain unchanged.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Shacraft zone 01 — finishing details and invisible boundary
|
||||
|
||||
The arrival square is finished and its five approaches are closed with an invisible collision boundary. Players can explore the completed garden while the station and other districts remain outside the current playable area. The existing emblem, six planting islands, benches, lights and broad central sightline remain the focus.
|
||||
|
||||

|
||||
|
||||
## Visible finish
|
||||
|
||||
Two low pale-stone urns with white flowers stand at X/Z **(-10, 35)** and **(10, 35)**. A low green nameboard at **(-8, 43)** carries `SHACRAFT` and `Площадь прибытия`. Its text is a separately managed display entity attached visually to the board, with no directional arrows toward closed roads.
|
||||
|
||||
Five flush threshold bands use stone with small green-copper insets at the north, south, east, west and northwest approaches. Paving remains at block Y95, with the walking plane at Y96. Three full sandstone joint piers complete the road-to-plaza boundary, bringing the perimeter pier count to 49.
|
||||
|
||||
The visible recipe contains 106 changed block states, followed by a sixteen-write urn refinement. Together with the enclosure, the editor made **11,518 checked writes in twenty batches**. All **11,502 final distinct changed states** matched the final observed world.
|
||||
|
||||

|
||||
|
||||
## Invisible enclosure
|
||||
|
||||
The boundary uses **11,396 barrier blocks**, recorded in a separate placement ledger so the enclosure can later be removed while keeping the garden finish. It includes side walls and a roof at block **Y116**, above the existing trees. The full-block floor at Y95 contributes to the enclosure.
|
||||
|
||||
The compiler bends the boundary inward around existing partial-block stairs and rails instead of replacing those details. The intended interior excludes 87 boundary-adjacent voxels for these local folds. The independently derived collision membrane contains **17,276 full-cube positions**: 11,396 barriers and 5,880 existing or finished structural blocks.
|
||||
|
||||
The [final observed containment report](references/shacraft-zone01-containment-verification.json) passed. The intended interior volume has one connected component containing 117,673 voxels. No interior voxel was reached from an exterior flood, and all **5,748 swept player-box boundary probes** were blocked. The probes use a 0.6 × 1.8 block player body; the complete static containment criterion is the full independently derived membrane. Unknown and partial blocks are conservatively treated as empty during the exterior flood.
|
||||
|
||||
This guarantee concerns normal continuous collision-based movement. Spectator mode, block removal, operator commands, plugin teleports and arbitrary discontinuous teleport or pearl behavior are outside its scope. The boundary is not an administrative access-control system.
|
||||
|
||||

|
||||
|
||||
## Arrival, respawn and maps
|
||||
|
||||
The configured `/lobby` destination is **(0.5, 96, 43.5)**, inside the southern welcome area. A scoped death-respawn handler returns players who died in the lobby world to that configured point, preventing Minecraft's highest-surface search from placing them on the invisible roof. It leaves other worlds and non-death transitions alone. The lobby spawn radius is zero. The scoped event behavior was unit tested; no player was killed for verification.
|
||||
|
||||
The owner was returned to creative mode at the verified arrival point after inspection. The nameboard entity, spawn settings and owner return are recorded separately from the block journals in `operator-actions.json`.
|
||||
|
||||
The server map exporter explicitly ignores `minecraft:barrier`, alongside air, when finding each visible surface. Exported metadata declares this policy, so maps show the garden beneath its invisible roof. Collision verification reads actual blocks separately and includes barriers.
|
||||
|
||||

|
||||
|
||||
## Final verification and preservation
|
||||
|
||||
The complete **622,098-voxel captured volume** matched the baseline plus final plan, including **610,596 voxels outside the edits**. The finishing audit passed for 537 persistent leaves, 260 supported flowers, sixteen lanterns, twelve rooted trees, **4,794 walking/headroom samples** and 32 bench-access columns.
|
||||
|
||||
The complete visible-surface comparison matched all **589,824 map columns**. It covered 6,164 affected columns, with 87 visible surface changes; all 583,660 columns outside the affected area matched their baseline. All 41,467 previously visible water columns remained unchanged. The distinction between actual collision blocks and the declared visible-surface policy is preserved in the separate reports.
|
||||
|
||||
These sequential captures are not atomic snapshots. The full-volume statement applies to the supplied observed bounds, and static geometric checks are not a human playtest. Perspective images are actual Minecraft client captures with heuristic render readiness.
|
||||
|
||||
`scripts/finish-shacraft-zone01.py` compiles the visible finish, enclosure and boundary metadata. `scripts/verify-zone-containment.py` independently derives the collision membrane, checks containment and protects existing decoration. Stage records remain under `.runtime/zone01-stage05/`.
|
||||
|
||||
The barrier ledger is separate from the finishing and urn-polish ledgers. Remove barriers through their checked ledger when reopening the other districts; the visible garden finish can remain. Undo the urn polish before undoing the earlier visible finish. The nameboard text and scoped spawn settings are separately recorded operator/configuration actions.
|
||||
|
||||
The restorable backup is `.runtime/projects/shacraft-zone01-complete-20260913/`. The complete world container and matching plugin/receipt state were copied with saving temporarily disabled after an explicit flush, then automatic saving was re-enabled. All 137 recorded world-file hashes passed verification.
|
||||
|
||||
The local Git archive preserves compact design and final reports, source snapshots, maps, photographs and the world-backup location. Raw voxel observations, complete world saves, private configuration, authentication, plugin binaries and journals remain outside Git. All previous references and construction checkpoints remain unchanged.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Terraforming tools
|
||||
|
||||
The terrain toolkit turns one small JSON recipe into a repeatable height field, a raster preview, and bounded edit plans. It prepares the terrain foundation for the Shacraft lobby: mountain ridges, cliff-backed building platforms, a lake bed, garden terraces and ravines. Buildings and fluids are separate work.
|
||||
|
||||
## Design and boundaries
|
||||
|
||||
A recipe describes an **absolute target surface**, not a brush that reads and smooths the existing world. Features operate in the listed order on `base_height` plus seeded three-octave value noise. World coordinates determine the noise and surface layers; neither tile boundaries nor a cropped envelope change their phase. The write envelope is inclusive and clips writes, not the underlying field.
|
||||
|
||||
Implemented features:
|
||||
|
||||
- `hill`: adds `height` inside a circular core, with a smooth outer falloff. A negative height makes a depression.
|
||||
- `ridge`: adds `height` along a polyline. `width` is the core half-width; repeated points are allowed.
|
||||
- `plateau`: blends to absolute `height` inside a rectangular footprint and outward through `falloff`. Place these after noise/hills to obtain level building pads.
|
||||
- `basin`: only lowers the surface to absolute `height` inside a circular core and banks. It never raises a low area.
|
||||
- `channel`: only lowers to absolute `height` along a polyline, with a flat bed and smooth banks.
|
||||
- `terrace`: blends toward elevations rounded down to multiples of `step`; `strength` is 0..1. This affects the whole field at that step in the recipe, so subsequent plateaus can restore exact platform heights.
|
||||
|
||||
`radius` and `width` describe a flat core radius/half-width in blocks. `falloff` is the additional outside transition distance; all distances are positive. A small core with a large falloff produces a rounded hill. Polyline paths may have 2–32 points. Heights can be negative. All feature heights are absolute except the additive hill/ridge heights.
|
||||
|
||||
Modes:
|
||||
|
||||
- `sculpt`: terrain material below the target surface, air above it, within the envelope.
|
||||
- `fill`: targets only cells at/below the target surface. It can replace natural material there, but does not clear above it.
|
||||
- `cut`: targets only air above the target surface. It does not add terrain or repaint the remaining surface.
|
||||
|
||||
The palette contains `rock`, `soil`, `surface`, and `soil_depth` (0–16). The top block uses `surface`, the next `soil_depth` blocks use `soil`, and deeper cells use `rock`. Layering follows the global surface even when it is outside a particular vertical tile. No new topsoil is invented at a tile boundary.
|
||||
|
||||
Supported natural materials: stone, andesite, granite, diorite, deepslate, cobbled deepslate, dirt, grass block, moss block, sandstone and terracotta. Cave/void air can also be replaced. Bedrock, water, lava, trees, buildings made of other materials, and block entities are rejected. **Natural-material buildings cannot be distinguished from terrain by material alone.** Exclude existing work with inclusive 3D `preserve` boxes and use the existing part protection system. A preserve mask omits writes; it does not infer or protect the structural supports beneath a build. Include its foundations and a suitable margin.
|
||||
|
||||
This first implementation is a 2.5D height field. It does not create caves, overhangs, imported heightmaps, vegetation, biomes, automatic erosion or fluid flow. Lake beds and ravines are dry. Neighbor/environment checks still apply. Terraforming does not weaken the normal write or undo policy.
|
||||
|
||||
## MCP workflow
|
||||
|
||||
1. Call `project_context` and inspect the intended site. Select a dedicated area before writing.
|
||||
2. Call `terrain_preview` with `recipe` and optional `resolution` (32–256, default 128). It does not read or alter world blocks. The native PNG shows the target field; it is not a Minecraft screenshot or a before/after comparison. North (-Z) is up. Pink marks a preserved cell at the sampled surface. Narrow/subsurface masks may not appear in the sampled image.
|
||||
3. Keep the returned `terrain_id`, `tile_count`, and `tile_budget`. Save the recipe locally. The ID is a SHA-256 of the recipe with sorted object keys; feature order remains significant. The plugin caches up to 32 recipes until restart. Regenerate a preview from the same JSON after eviction/restart.
|
||||
4. Call `terrain_prepare(terrain_id, tile_index)`. Indices start at zero. Tiles advance X first, then Z, then Y, from the envelope minimum. At the normal 4096-block budget, tiles are at most 16×16×16. A smaller configured budget changes their edge and numbering; never resume a batch under a different budget.
|
||||
5. Review `tile_bounds` and `changed_blocks`. `status: empty` means there are no changed targets, either because of the mask/mode or because the sampled targets already match. No plan is journaled for these tiles. Otherwise use the returned `plan_id` and `plan_hash` with `build_apply` and a stable unique `idempotency_key`.
|
||||
6. Poll `operation_status` to completion. Stop on conflict, cancelled, failed or recovery-required status. Do not prepare a replacement to force a conflicting tile through.
|
||||
7. Undo with `operation_undo_prepare` and normal checked apply. For a batch, reverse the operation order. Cancellation/failure can leave partial edits; tiles are not one atomic transaction.
|
||||
|
||||
Recipes are bounded to 64 features, 64 preserve boxes, 2048 blocks per horizontal side, 1,048,576 columns, 384 blocks vertically, and 134,217,728 voxels in the envelope. Preview statistics are sampled, not exhaustive extrema. `clipped_samples` reports surfaces outside the vertical write envelope; such surfaces need a wider envelope or deliberate clipping.
|
||||
|
||||
The terrain compiler and preview renderer run outside the server tick. Live reads, natural-material checks, area checks, protected parts and plan snapshots run through Paper's main thread. Writes use the existing sliced, durable intent/receipt journal. Manual modifications after preparation are checked again on application; existing undo ownership rules remain in force. Only already-loaded chunks may be edited; this toolkit does not silently generate or force-load a whole map.
|
||||
|
||||
## Local tools and examples
|
||||
|
||||
Build once with `./mvnw package` and `npm --prefix bridge test`. Offline preview needs the Java runtime installed by the project's bootstrap script; `MCB_JAVA_HOME` can override its path.
|
||||
|
||||
Render the proposed Shacraft terrain without a running server:
|
||||
|
||||
```bash
|
||||
python3 scripts/terrain.py preview examples/terrain/shacraft-massif.json \
|
||||
--output docs/references/shacraft-terrain-heightmap.png
|
||||
```
|
||||
|
||||

|
||||
|
||||
The example proposes a 768×768×192 envelope and therefore 27,648 possible tiles at 4096 blocks each. This is a layout experiment, not an instruction to apply all tiles to the current world. It must be assigned a new, suitable project area before use. The current Gothic hall site is not a valid destination for it.
|
||||
|
||||
For an authorized small site, a local batch avoids sending every tile's polling traffic through model context. This Linux runner uses an exclusive manifest lock, saves requests before sending them, stops on conflicts and processes at most 64 explicitly selected tiles per invocation:
|
||||
|
||||
```bash
|
||||
python3 scripts/terrain.py apply examples/terrain/small-hill.json \
|
||||
--start 0 --count 1 --manifest .runtime/my-terrain-batch.json --execute
|
||||
|
||||
# Resume the exact same command and manifest after a transport interruption.
|
||||
# Do not delete or edit a manifest to bypass a conflict.
|
||||
|
||||
python3 scripts/terrain.py undo --manifest .runtime/my-terrain-batch.json --execute
|
||||
```
|
||||
|
||||
The small example requires a loaded, authorized all-air test area at x/z 0..7, y 80..87. It is not automatically applied. The default backend configuration belongs to the local development server; `--config` selects another local plugin config. `--console` is only for an isolated server explicitly configured with `allow-local-automation: true`. Credentials never enter the manifest or printed results.
|
||||
|
||||
A manifest binds the recipe, project, world, epoch, tile budget, range, immutable plans, stable keys, operation IDs and undo progress. Lost apply responses are resolved by resuming the original apply with its saved key. Undo refuses to start an unconfirmed apply merely to discover whether it ran. Expired plans and recovery-required operations require inspection; the runner does not regenerate or force them automatically. An interrupted undo resumes from its recorded reverse plans. Once undo has started, the manifest cannot be reused for new construction.
|
||||
|
||||
## Verification and current scale limit
|
||||
|
||||
Automated tests cover ordered features, negative elevations/coordinates, deterministic noise, horizontal and vertical tiling, layer continuity, preserve masks, input/resource rejection, PNG metadata, real journal apply/undo/conflicts, native MCP transport and validation, and persistence before a potentially lost apply response.
|
||||
|
||||
The opt-in `bridge/test/live-terrain.mjs` exercises the real MCP → Paper path in an isolated fixture: preview without writes, a 201-block hill, idempotent replay, checked undo, protected existing gold block, preserved cell, stale-plan conflict, empty mask, area rejection and unknown recipe ID. The local batch runner is also tested by apply → resume → undo with full air restoration.
|
||||
|
||||
A compact recipe and native PNG reduce context cost; they do not eliminate server work. Large sites still need many live reads and potentially large journals. No full 768×768 application, throughput target or crash/disk benchmark has been completed. This toolkit is ready for small terrain sections and progressive landscape work; a whole-map rollout should be benchmarked and staged separately.
|
||||
|
||||
## Relative brushes on existing terrain
|
||||
|
||||
`terrain_brush_prepare` reads an actual bounded world snapshot, calculates a circular brush, and returns an immutable plan together with a native **BEFORE / AFTER / HEIGHT CHANGE** image. It does not edit the world. The existing `build_apply`, `operation_status`, cancellation and checked undo tools apply the result. `plan_state: empty` means that the brush has no effect and there is no plan to apply; the image is still returned. `source: live_snapshot` identifies this preview, in contrast to an absolute recipe preview that has no world reads. The image is a height diagram, not a Minecraft camera capture.
|
||||
|
||||
Input is `{ "brush": { ... } }`. The example [relative-brush.json](../examples/terrain/relative-brush.json) is for the isolated live-test terrain at Y=84; choose bounds from the actual project before using it elsewhere.
|
||||
|
||||
- `min/max`: inclusive scan and edit window, at most **4096 cells**. Every scanned column must contain natural terrain with air above it. Include the full circular footprint, enough depth for cutting, and air above both the old and proposed top. If this window is too small, the tool rejects the request instead of silently clipping the effect or guessing a surface beyond its boundary.
|
||||
- `center`: integer `{x,z}` in world coordinates. `radius`: 1–16 blocks; the volume and halo constraints can require a smaller radius.
|
||||
- `action: raise | lower`: `amount` is a positive integer, 1–32 blocks, relative to each original column's own elevation.
|
||||
- `action: flatten`: `height` is the absolute target Y coordinate. It can raise low ground and cut high ground in the same stroke.
|
||||
- `action: smooth`: the target is the uniform average of the original heights in a square `(2*smooth_radius+1)` neighborhood. `smooth_radius` is 1–3, default 1. Include that halo beyond the circular footprint on every side. All targets use the same original snapshot, so smoothing is independent of processing order. For another smoothing pass, inspect the completed result and create a new stroke deliberately.
|
||||
- `strength`: 0–1, default 1, scales the vertical displacement. Block displacements round symmetrically away from zero at half a block; very weak strokes may have no effect.
|
||||
- `falloff`: 0–1, default 0.5, is the fraction of the radius used for the smooth outer fade. Zero makes a hard edge. With a positive falloff the effect reaches zero at the outer boundary; the inner core has full strength.
|
||||
- `preserve`: up to 64 inclusive boxes inside the scan window. If a box intersects any cell in a column's proposed vertical edit, that whole column is left unchanged to avoid tearing a hole around the mask.
|
||||
|
||||
Raised columns move the original top block to the new surface and extend the material immediately below the old top. If that material is unavailable/air, grass or moss falls back to dirt; other natural tops extend themselves. Lowered columns remove material above the target and put the original surface material on the new top. Cutting refuses to cap a void inside the snapshot. There is no general soil-depth simulation or geological layer translation.
|
||||
|
||||
All scanned cells, including unchanged columns and the smoothing halo, are stored as **read dependencies** in the plan. The engine rechecks them before slices and after journal IO, accounting for this operation's own writes. A changed dependency stops the brush; earlier completed slices can remain and must be inspected. This is a checked operation, not an atomic whole-region transaction. The Paper dependency budget is 4096 for brushes; the separate explicit dependency input to `build_prepare` remains capped at 512. Full snapshots stay on the server and in the journal; the model receives the image and compact statistics.
|
||||
|
||||
The whole scan window must contain only the supported natural materials/air, including in preserved and halo columns. Structures, fluids, bedrock, vegetation and unsupported blocks in that window reject the brush. Masks do not bypass this scan policy. Natural stone constructions need explicit exclusion or protected parts, just as with absolute recipes. Only loaded chunks inside the selected project are read. These brushes find the uppermost surface **inside the provided window**; they do not prove that there is no separate roof or terrain above that window.
|
||||
|
||||
Overlapping strokes retain the editor's strict undo-ownership rules: undoing a newer stroke does not automatically restore an older operation's right to undo the same blocks. This is not a multi-stroke undo stack. Never force an old undo or silently recreate a plan after a conflict.
|
||||
|
||||
In-game requests can be ordinary `/ai` text, for example:
|
||||
|
||||
```text
|
||||
/ai Raise the existing terrain in front of me by 3 blocks, radius 3, with a soft edge. Use terrain_brush_prepare, verify the scan area is free of structures, then apply and check the result.
|
||||
/ai Smooth this slope with a small relative terrain brush. Preserve nearby buildings.
|
||||
```
|
||||
|
||||
The opt-in `bridge/test/live-brush.mjs` verifies the real MCP → Paper route for raise, lower, flatten and smooth; preview before writes, all-snapshot dependencies, checked undo, a conflict caused by a halo edit, no-effect plans and complete isolated-fixture cleanup. It saves `docs/references/terrain-brush-live-preview.png` from the actual prepared plan. Core tests also cover symmetric falloff, scan/halo limits, clipping rejection, preserve columns and a dependency change during journal persistence.
|
||||
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"kind": "design_reference_catalog",
|
||||
"reviewed_on": "2026-09-13",
|
||||
"project": "Shacraft lobby",
|
||||
"status": "Original design proposals; not executable build_prepare recipes and not newly built or playtested.",
|
||||
"units": "Dimensions are width (X), height (Y), depth (Z) in blocks. Envelopes exclude separately specified circulation and buried support. Front is -Z; +Z points into the object. Adapt to surveyed world coordinates and rotate directional block states with the geometry.",
|
||||
"capability_basis": "Material names checked against the saved Stage 07 project_context palette on 2026-09-13. Re-read live project_context before building. Material availability does not validate states, placement support or geometry.",
|
||||
"source_notes": "source_ids identify conceptual inspiration. All dimensions, placement sequences and Shacraft adaptations below are original proposals, not source schematics.",
|
||||
"sources": [
|
||||
{"id":"S01","title":"Interior Motives","url":"https://www.minecraft.net/en-us/article/interior-motives","use":"Purposeful interiors and agreement between interior and exterior architecture."},
|
||||
{"id":"S02","title":"Tranquil Towers","url":"https://www.minecraft.net/en-us/article/tranquil-towers","use":"Develop roof shape and silhouette before details."},
|
||||
{"id":"S03","title":"The Sky is No Limit","url":"https://www.minecraft.net/en-us/article/sky-no-limit","use":"Consistent palette and simple textures for a large composition."},
|
||||
{"id":"S04","title":"Taking Inventory: Glass Pane","url":"https://www.minecraft.net/en-us/article/taking-inventory--glass-pane","use":"Window depth; our sealed variant uses recessed full glass."},
|
||||
{"id":"S05","title":"Build With It: Quartz","url":"https://www.minecraft.net/en-us/article/build-with-it--quartz","use":"A pale material family can connect architecture and furnishings."},
|
||||
{"id":"S06","title":"Fantastic Furniture","url":"https://www.minecraft.net/en-us/article/fantastic-furniture","use":"Habitable furniture groups and independently finished partition faces."},
|
||||
{"id":"S07","title":"Cozy Chambers","url":"https://www.minecraft.net/en-us/article/cozy-chambers","use":"Wood, plants and lighting soften stone and copper interiors."},
|
||||
{"id":"S08","title":"Block of the Month: Decorated Pot","url":"https://www.minecraft.net/en-us/article/decorated-pot","use":"Flowerpot above decorated pot is a future material option, not supported by the current palette."},
|
||||
{"id":"S09","title":"Tutorial: Tips For Landscaping and Terraforming","url":"https://www.minecraft.net/en-us/article/tutorial--tips-for-landscaping-and-terraforming","use":"BlueNerd: connect paths, banks, vegetation and shaped terrain."},
|
||||
{"id":"S10","title":"Five simple path designs","url":"https://www.minecraft.net/en-us/article/five-simple-path-designs","use":"Zaypixel: route materials and nearby objects communicate place."},
|
||||
{"id":"S11","title":"Tutorial: Cottagecore Decor","url":"https://www.minecraft.net/en-us/article/tutorial--cottagecore-decor","use":"Kelpie the Fox: outdoor places to rest; reference uses Mizuno's 16 Craft."},
|
||||
{"id":"S12","title":"Taking Inventory: Lantern","url":"https://www.minecraft.net/en-us/article/taking-inventory--lantern","use":"Lanterns as recurring fixtures; historical spawning advice is not used."},
|
||||
{"id":"S13","title":"Build With It: Cobblestone!","url":"https://www.minecraft.net/en-us/article/build-with-it--cobblestone-","use":"Moss and vegetation can suggest age in masonry."},
|
||||
{"id":"S14","title":"Caves & Cliffs: Part I out today on Java","url":"https://www.minecraft.net/en-us/article/caves---cliffs--part-i-out-today-java","use":"Waxed copper, tinted glass light blocking, and introduced decorative blocks."},
|
||||
{"id":"S15","title":"Minecraft Java Edition 1.21.5: Spring to Life","url":"https://feedback.minecraft.net/hc/en-us/articles/35298208390797-Minecraft-Java-Edition-1-21-5-Spring-to-Life","use":"New plant orientations/densities and low-emission firefly bushes; not current MCP capabilities."},
|
||||
{"id":"S16","title":"Build with It: Diorite!","url":"https://www.minecraft.net/en-us/article/build-it--diorite-","use":"Texture intensity matters; polished variants can produce calmer surfaces."},
|
||||
{"id":"S17","title":"Minecraft Java Edition 26.2","url":"https://www.minecraft.net/en-us/article/minecraft-java-edition-26-2","use":"Current game has additional material families; game availability is distinct from MCP support."}
|
||||
],
|
||||
"recipes": [
|
||||
{
|
||||
"id":"arched-window-bay","category":"facade","name":"Recessed arched window bay",
|
||||
"size":{"width":7,"height":10,"depth":3},
|
||||
"purpose":"Give a large cream facade readable shadow and a consistent structural rhythm.",
|
||||
"materials":["minecraft:smooth_sandstone","minecraft:cut_sandstone","minecraft:smooth_sandstone_stairs","minecraft:smooth_sandstone_slab","minecraft:smooth_quartz","minecraft:brown_stained_glass"],
|
||||
"steps":["Reserve a seven-wide bay between the actual structural axes; keep adjacent columns and floor decks intact.","At the glazing plane, use five-wide glass rows for the straight portion, then three and one at the crown. Fill every surrounding cell with full masonry so the plane stays sealed.","Place glazing one block behind the front wall plane; use the remaining front depth for a sill and paired jamb brackets.","Use sandstone stairs only as outer trim. Maintain full masonry or full glass behind partial-block trim.","Repeat the approved bay on the established grid; redesign corners and the main doorway deliberately."],
|
||||
"clearance":"Keep projections outside the existing public route and its four-block headroom; do not narrow the seven-block station entry.",
|
||||
"variants":["Plain rectangular head on a secondary facade.","One gold accent above the principal entrance only."],
|
||||
"acceptance":["Closure plane has no unfilled cells, including arch shoulders.","Recess reads from a diagonal player-height camera.","No sill hides a sign or clips furniture.","Stair rotation and corner shape checked after neighboring blocks update."],
|
||||
"avoid":["Different window silhouettes in every bay.","Assuming a pane or stair is a full containment wall."],
|
||||
"source_ids":["S03","S04","S05"]
|
||||
},
|
||||
{
|
||||
"id":"cornice-bracket","category":"facade","name":"Cornice and paired bracket",
|
||||
"size":{"width":9,"height":3,"depth":2},
|
||||
"purpose":"Explain the floor/eave line without filling the wall with ornament.",
|
||||
"materials":["minecraft:smooth_sandstone","minecraft:smooth_sandstone_stairs","minecraft:smooth_sandstone_slab","minecraft:smooth_quartz"],
|
||||
"steps":["Place a continuous stone band on the existing floor or eave line.","Use an inverted-stair underside and a thin slab cap, with at most two blocks of total outward depth.","Put the two bracket groups over structural supports and leave the central field quiet.","Continue the band around corners with an explicitly resolved return; prototype the corner before repeating."],
|
||||
"clearance":"Remain above route headroom and outside signs' viewing rays; preserve roof drainage shapes and window tops.",
|
||||
"variants":["Single-block projection on small wings.","A stronger cap at the tower base, using the same materials."],
|
||||
"acceptance":["Horizontal band remains readable from the main approach.","Brackets align with actual supports.","Cap does not overwhelm the window opening."],
|
||||
"avoid":["A thick shelf over every horizontal edge.","Unrelated gold trim on every bracket."],
|
||||
"source_ids":["S02","S03","S05"]
|
||||
},
|
||||
{
|
||||
"id":"closed-dormer","category":"roof","name":"Closed green-copper dormer",
|
||||
"size":{"width":7,"height":7,"depth":7},
|
||||
"purpose":"Add a secondary roof feature while keeping the clock tower dominant.",
|
||||
"materials":["minecraft:smooth_sandstone","minecraft:brown_stained_glass","minecraft:waxed_oxidized_cut_copper","minecraft:waxed_oxidized_cut_copper_stairs","minecraft:waxed_oxidized_cut_copper_slab"],
|
||||
"steps":["Fit a seven-wide sample to the actual roof pitch; use cream side cheeks and three-wide recessed glass.","Build the green pitched cap with a one-block overhang.","Keep a solid back and floor closure behind the apparent window; retain the existing public ceiling.","Test one dormer from the main approach before any repetition. Consider 14-18 blocks between centers only where the measured structural grid permits it."],
|
||||
"clearance":"No opening into an unfinished attic, no overlap with tower faces or main roof ridge.",
|
||||
"variants":["One larger central feature on a secondary wing.","Omit the dormer if it competes with the clock silhouette."],
|
||||
"acceptance":["Tower remains the primary skyline feature.","Quiet copper roof planes remain visible between details.","Full closure survives the added roof cutout."],
|
||||
"avoid":["Treating proposed spacing as a universal rule.","Adding attic access as part of a cosmetic edit."],
|
||||
"source_ids":["S02","S03"]
|
||||
},
|
||||
{
|
||||
"id":"waiting-pocket","category":"interior","name":"Paired waiting benches",
|
||||
"size":{"width":9,"height":4,"depth":7},
|
||||
"purpose":"Create a believable place to wait beside the main hall.",
|
||||
"materials":["minecraft:spruce_stairs","minecraft:spruce_slab","minecraft:spruce_planks","minecraft:smooth_sandstone","minecraft:green_concrete","minecraft:lantern","minecraft:oak_leaves","minecraft:dirt"],
|
||||
"steps":["Place two three-seat spruce benches facing a shared open pocket, with cream end supports.","Keep a compact table at one side rather than on the main desire line.","Use a flush green paving inset to group the furniture; this substitutes for unsupported carpet.","Add one planter and one supported lantern near the back, leaving the front visually open."],
|
||||
"clearance":"Reserve an additional three-block-wide clear passing strip and four blocks of route headroom. Existing wider routes retain their width.",
|
||||
"variants":["Single bench and planter in a smaller side recess.","Mirror the group around the room axis while varying the small prop."],
|
||||
"acceptance":["Lift sign remains visible from the approach.","Seat fronts and table can be approached without stepping onto furniture.","Lamp and leaves remain stable after updates."],
|
||||
"avoid":["Rows of isolated decorative chairs.","Plants in front of navigation signs."],
|
||||
"source_ids":["S01","S06","S07"]
|
||||
},
|
||||
{
|
||||
"id":"information-counter","category":"interior","name":"Information counter",
|
||||
"size":{"width":9,"height":5,"depth":4},
|
||||
"purpose":"Provide a clear information point that belongs in a station.",
|
||||
"materials":["minecraft:spruce_planks","minecraft:spruce_slab","minecraft:smooth_sandstone","minecraft:smooth_sandstone_slab","minecraft:green_concrete","minecraft:glowstone","minecraft:brown_stained_glass"],
|
||||
"steps":["Frame a seven-block counter between cream end piers; use spruce body and a thin cream slab top.","Align one green sign backing and one lit recess behind the counter.","Keep the back wall solid. Any future staff door remains a finished wall panel until the room exists.","Add actual text only through an existing authorized label mechanism; the block catalog does not create text entities."],
|
||||
"clearance":"Reserve three clear queue blocks in front, separated from the entrance-to-lift route and its full width.",
|
||||
"variants":["Short five-wide counter for a side room.","A small gold emblem only above the principal information point."],
|
||||
"acceptance":["Queue does not cross the main route.","Counter is recognizable before reading the sign.","No false usable door into unfinished space."],
|
||||
"avoid":["Several equally bright competing headers.","Claiming matchmaking or staff interaction exists merely because the counter is built."],
|
||||
"source_ids":["S01","S05","S06"]
|
||||
},
|
||||
{
|
||||
"id":"smash-exhibit","category":"interior","name":"SMASH exhibit niche",
|
||||
"size":{"width":7,"height":6,"depth":5},
|
||||
"purpose":"Communicate floating islands and knockback through one readable display.",
|
||||
"materials":["minecraft:deepslate","minecraft:smooth_sandstone","minecraft:green_concrete","minecraft:gold_block","minecraft:grass_block","minecraft:dirt","minecraft:glowstone","minecraft:brown_stained_glass"],
|
||||
"steps":["Build a full dark base, full back and light stone frame around a five-wide exhibit.","Choose one miniature island or one abstract impact motif as the subject.","Put one small gold focal detail near the subject and keep the rest of the frame consistent with adjacent niches.","Light the subject from concealed side, base or overhead recesses; preserve full-block base and closure.","Use one shared frame for all six future arena places; vary the subject and real destination text only when available."],
|
||||
"clearance":"Reserve a three-block viewing strip outside the main circulation aisle; preserve the existing selection aisle.",
|
||||
"variants":["Closed trophy pedestal with a single object.","Small landscape model for a future named arena."],
|
||||
"acceptance":["Subject is legible from player eye level without entering the display.","No dark face hides the model's silhouette.","Placeholders clearly remain unassigned."],
|
||||
"avoid":["Six unrelated palettes.","A visual void that is also an accidental physical hole."],
|
||||
"source_ids":["S01","S06","S07"]
|
||||
},
|
||||
{
|
||||
"id":"avenue-edge","category":"path","name":"Formal avenue edge",
|
||||
"size":{"width":9,"height":1,"depth":12},
|
||||
"purpose":"Connect station stonework to the garden through a continuous legible route.",
|
||||
"materials":["minecraft:stone_bricks","minecraft:smooth_stone","minecraft:smooth_sandstone","minecraft:mossy_stone_bricks"],
|
||||
"steps":["Keep seven center columns as continuous clear paving and one flush border column on each side.","Survey elevations along the whole segment before decoration; grade transitions as separate geometry.","Place furniture in additional two-to-three-block shoulders or wider pockets, not inside the nine-block strip.","Keep the station approach clean. Introduce small connected worn patches at outer edges beside planting.","Widen at entrances and follow actual destinations rather than adding arbitrary bends."],
|
||||
"clearance":"Seven-block clear walking width and four-block headroom; this example must not narrow a wider established route.",
|
||||
"variants":["Use local gray stone farther from the station.","Curve the outer border while keeping the tread continuous."],
|
||||
"acceptance":["Entire route walkable at all grade changes.","Flush borders do not introduce a lip.","No lamp or planter creates a pinch point."],
|
||||
"avoid":["Uniform random scatter of five paving materials.","Applying an organic-path zigzag rule to a formal station axis."],
|
||||
"source_ids":["S09","S10","S13"]
|
||||
},
|
||||
{
|
||||
"id":"copper-lantern","category":"lighting","name":"Supported copper-arm lantern",
|
||||
"size":{"width":3,"height":7,"depth":1},
|
||||
"purpose":"Establish one recognizable outdoor fixture family.",
|
||||
"materials":["minecraft:stone_bricks","minecraft:stripped_spruce_log","minecraft:waxed_oxidized_cut_copper","minecraft:iron_chain","minecraft:lantern"],
|
||||
"steps":["With the sample's first above-ground cell at y=0, place a full stone foot at (0,0,0), rooted into surveyed ground below.","Raise a post at x=0,z=0 through y=1..5; place a full-block copper arm across x=0..2 at y=6.","Place a vertical iron chain at (2,5,0) attached beneath the full arm, then a hanging lantern at (2,4,0).",
|
||||
"Place support before attached details and inspect resulting block states.","Keep the entire post outside the clear path; use a mirrored pair only at a major entry."],
|
||||
"clearance":"Four empty cells below the hanging lantern if ground is level with the sample. Recalculate for actual terrain; do not assume the same clearance over a rising stair.",
|
||||
"variants":["Shorter ground-mounted fixture in a planted pocket with no walk-under route.","One gold marker at a major junction, outside this base recipe."],
|
||||
"acceptance":["Full arm, chain axis and hanging lantern state remain valid after updates.","Night view shows the next step, intersection and sign between lamps.","Post cannot be used to bypass an existing containment boundary."],
|
||||
"avoid":["Unsupported lanterns.","Treating a fixed lamp spacing as guaranteed illumination coverage."],
|
||||
"source_ids":["S11","S12","S14"]
|
||||
},
|
||||
{
|
||||
"id":"layered-planter","category":"planting","name":"Layered planting pocket",
|
||||
"size":{"width":7,"height":6,"depth":5},
|
||||
"purpose":"Produce a planted mass with a readable front, middle and background.",
|
||||
"materials":["minecraft:dirt","minecraft:grass_block","minecraft:spruce_log","minecraft:spruce_leaves","minecraft:oak_leaves","minecraft:white_tulip","minecraft:pink_tulip","minecraft:oxeye_daisy","minecraft:smooth_sandstone"],
|
||||
"steps":["Shape a low cream rim with solid soil behind it; support the entire pocket down to the existing terrain.","Put one small rooted tree toward the rear and two unequal shrub groups below it.","Use one dominant foliage species and two restrained flower colors near the front.","Leave patches of visible grass between groups; taper density toward the edge.","Save a seed and cluster anchors for repeatable variation; change group positions rather than every block independently."],
|
||||
"clearance":"Keep the highest mass outside entrance and sign sightlines; no leaves overhang the protected walking clearance.",
|
||||
"variants":["Formal clipped evergreen beside the station.","Looser shrubs without a tree beside the mountain path."],
|
||||
"acceptance":["Flowers have valid soil.","Leaves use persistent=true and a stable read-back distance state.","Trunk has stable soil under it.","Foliage tint checked in the actual biome."],
|
||||
"avoid":["A flower on every grass block.","Using leaves as the only containment boundary."],
|
||||
"source_ids":["S07","S09","S11"]
|
||||
},
|
||||
{
|
||||
"id":"shore-pocket","category":"landscape","name":"Quiet shore with a stony recess",
|
||||
"size":{"width":12,"height":4,"depth":8},
|
||||
"purpose":"Connect existing water and terrain with a believable local bank.",
|
||||
"materials":["minecraft:stone","minecraft:andesite","minecraft:cobblestone","minecraft:mossy_cobblestone","minecraft:dirt","minecraft:grass_block","minecraft:oak_leaves"],
|
||||
"steps":["Sample a short existing shoreline together with its water level and submerged bed.","Shape one quiet bank and one small stony recess; maintain continuous ground beneath the visible soil skin.","Put exposed rock at the outcrop and moss only in selected damp recesses.","Keep vegetation in sheltered land pockets and leave a clear view of the water.","This catalog specifies only land-side decor. Preserve water cells and the connected bed; stop if execution requires fluid placement or an unsupported brush through water."],
|
||||
"clearance":"Preserve public route width, foundations, water level and any existing waterfront containment.",
|
||||
"variants":["Grass bank with one isolated outcrop.","Stone recess beside a retaining wall, without changing its footing."],
|
||||
"acceptance":["Water network and visible level preserved.","No new flow reaches a road or foundation after updates.","A below-water view shows a continuous bed; top-down appearance alone is insufficient."],
|
||||
"avoid":["Repeating identical coves as tiles.","Assuming water exists in the MCP material palette."],
|
||||
"source_ids":["S09","S13"]
|
||||
},
|
||||
{
|
||||
"id":"retaining-stair","category":"path","name":"Retaining wall and short stair",
|
||||
"size":{"width":9,"height":5,"depth":10},
|
||||
"purpose":"Make a grade change feel supported and intentionally connected.",
|
||||
"materials":["minecraft:stone_bricks","minecraft:stone_brick_stairs","minecraft:stone_brick_slab","minecraft:smooth_sandstone","minecraft:smooth_sandstone_slab","minecraft:mossy_stone_bricks","minecraft:dirt"],
|
||||
"steps":["Survey lower and upper approaches first; adapt the proposed envelope to the actual rise.","Reserve five clear stair columns and two shoulder columns per side.","Use three or four one-block rises followed by a three-block-deep landing; fill every tread and retaining wall down to solid ground.","Return the side walls into the upper terrace instead of ending them as disconnected pillars.","Keep the cream cap continuous; limit moss to local soil-contact or shaded recesses."],
|
||||
"clearance":"Five clear stair blocks and four-block headroom along the full ascending path, including caps and any overhead fixture.",
|
||||
"variants":["A shorter flight for a lower terrace.","An additional landing and separate second flight when the actual rise requires it."],
|
||||
"acceptance":["Stair facing matches direction of ascent.","Approach and landing elevations agree with the survey.","No hollow exposed underside or unsupported cap.","Shoulders do not create a climb-over route around containment."],
|
||||
"avoid":["Solving a height mismatch with a decorative slab at the doorway.","Placing a standard stair module without measuring the destination."],
|
||||
"source_ids":["S09","S10","S13"]
|
||||
},
|
||||
{
|
||||
"id":"luggage-bench","category":"props","name":"Bench and luggage group",
|
||||
"size":{"width":7,"height":3,"depth":4},
|
||||
"purpose":"Tell a station story with a small, useful-looking prop group.",
|
||||
"materials":["minecraft:spruce_stairs","minecraft:spruce_planks","minecraft:stripped_spruce_log","minecraft:smooth_sandstone","minecraft:green_concrete","minecraft:gold_block"],
|
||||
"steps":["Put a three-seat bench against the back of a widened pocket, facing the plaza.","Group two wood-toned luggage shapes at one end, leaving seat fronts clear.","Reserve a green backing for one real destination or station identity plaque.","Use at most one small metallic accent in the group; omit it if it attracts more attention than the navigation."],
|
||||
"clearance":"Provide a separate three-block passing strip in front and preserve any wider existing route.",
|
||||
"variants":["Bench alone.","Bench with one planter instead of luggage."],
|
||||
"acceptance":["Object reads as a resting place from the walking path.","Luggage does not intrude into the bench approach.","No label advertises an unfinished accessible destination."],
|
||||
"avoid":["Crates in every empty corner.","Unsupported decorative item entities or invented text APIs."],
|
||||
"source_ids":["S01","S06","S10"]
|
||||
},
|
||||
{
|
||||
"id":"concealed-light-lens","category":"lighting","name":"Full-block concealed light lens",
|
||||
"size":{"width":3,"height":2,"depth":3},
|
||||
"purpose":"Illuminate a large room without adding a hanging fixture to every bay.",
|
||||
"materials":["minecraft:glowstone","minecraft:brown_stained_glass","minecraft:smooth_sandstone"],
|
||||
"steps":["Reserve a three-by-three floor sample outside patterned paving, tree roots, landings and interactive controls.","Keep full paving in the eight perimeter columns; in the center put glowstone below a full brown stained-glass block flush with the walking surface.","Keep supporting construction below the sample intact and record expected states before replacement.","Prototype one lens; increase coverage only after matching night and interior photographs reveal dark areas.","A ceiling variant reverses the emitter and visible lens positions within an intact full-block ceiling."],
|
||||
"clearance":"Top of the floor lens remains flush. Ceiling version preserves all existing route headroom and sealed upper rooms.",
|
||||
"variants":["Use ordinary full glass if the brown lens makes the sample too dark.","Concealed side lighting for an exhibit rather than more floor lights."],
|
||||
"acceptance":["Full-block collision remains at the floor/ceiling surface.","Actual captured room and exhibit faces become legible.","No new bright fixture competes with the entrance or lift."],
|
||||
"avoid":["Tinted glass: it blocks light and is not interchangeable with stained glass.","Assuming more fixtures automatically produce a better interior."],
|
||||
"source_ids":["S07","S14"],
|
||||
"local_precedent":"Stage 07 used concealed brown-glass lighting and real camera review; this reusable sample has not itself been instantiated."
|
||||
},
|
||||
{
|
||||
"id":"masonry-weathering","category":"surface","name":"Cause-based masonry weathering",
|
||||
"size":{"width":12,"height":6,"depth":1},
|
||||
"purpose":"Add age and material variation while retaining large calm wall fields.",
|
||||
"materials":["minecraft:stone_bricks","minecraft:cracked_stone_bricks","minecraft:mossy_stone_bricks","minecraft:andesite","minecraft:smooth_sandstone","minecraft:cut_sandstone"],
|
||||
"steps":["Choose either the gray retaining-wall family or the cream facade family; do not mix both randomly.","Save a mask for soil contact, sheltered joints and selected worn edges.","For gray masonry, use small connected mossy patches in the mask and a few connected cracked areas away from structural focal edges.","For cream masonry, keep the field mostly smooth and use cut sandstone to express masonry bands or quiet local variation; do not add green flecks everywhere.","Preserve trim, signs and dominant shapes, then inspect at near, approach and whole-building distances."],
|
||||
"clearance":"Surface substitution only; preserve structural collision, public closure and existing manual edits.",
|
||||
"variants":["Clean maintained station entrance with minimal weathering.","More aged retaining wall near shaded soil contact."],
|
||||
"acceptance":["Weathering locations have a visible cause.","Large forms still read from the approach.","Exact seed and patch anchors recorded for reproducibility."],
|
||||
"avoid":["Independent random choice at every voxel.","A universal percentage of damage on every material.","Replacing load-bearing full cubes with stairs merely for texture."],
|
||||
"source_ids":["S03","S13","S16"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"verified_at": "2026-09-13T13:47:22.221969+00:00",
|
||||
"server_version": "26.2-123-5001879 (MC: 26.2)",
|
||||
"catalog": {
|
||||
"items": 1537,
|
||||
"search_default_limit": 16,
|
||||
"blocks": 1196,
|
||||
"version": "00474abd322518089309784c",
|
||||
"materials": 1691,
|
||||
"search_max_limit": 32
|
||||
},
|
||||
"registry_probe": {
|
||||
"catalog_described": 1196,
|
||||
"max_description_bytes": 682,
|
||||
"total": 1196,
|
||||
"passed": 1196,
|
||||
"entity_defaults": 186,
|
||||
"property_cases": 5392,
|
||||
"fresh_property_passed": 5392,
|
||||
"same_material_property_passed": 5392,
|
||||
"private_undo_passed": 5392,
|
||||
"anchor_restored": true,
|
||||
"failure_count": 0
|
||||
},
|
||||
"scope": "All block defaults and independent property variants; not the Cartesian product or later simulation effects.",
|
||||
"live_integration": {
|
||||
"phase": "complete",
|
||||
"checks": [
|
||||
"bounded search/pagination",
|
||||
"property domains",
|
||||
"item-only distinction",
|
||||
"new blocks and fluids apply/undo",
|
||||
"paired doors",
|
||||
"private block-entity digests",
|
||||
"same-material inventory preservation",
|
||||
"manual-content stale snapshot and apply conflict",
|
||||
"no inventory drops",
|
||||
"schematic new-material native rotation",
|
||||
"schematic block-entity export rejection",
|
||||
"complete chest/sign/pot undo after Paper restart"
|
||||
],
|
||||
"catalog_summary_bytes": 130
|
||||
},
|
||||
"tests": {
|
||||
"maven": {
|
||||
"world-core": {
|
||||
"tests": 76,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"paper-plugin": {
|
||||
"tests": 43,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"terrain-world-plugin": {
|
||||
"tests": 26,
|
||||
"failures": 0,
|
||||
"errors": 0,
|
||||
"skipped": 0
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"tests": 47,
|
||||
"failures": 0
|
||||
}
|
||||
},
|
||||
"deployment": {
|
||||
"project": "shacraft-lobby-v2",
|
||||
"scope_and_history_preserved": true,
|
||||
"plugin_sha256": "7cb8abcc3e8127d60cbdb8925b531bfef856fe2dc4abdef9cc6191c5c715fc4d",
|
||||
"mcp": {
|
||||
"timestamp": "2026-09-13T13:45:31.378Z",
|
||||
"tool_count": 19,
|
||||
"checks": [
|
||||
{
|
||||
"tool": "project_context",
|
||||
"bytes": 4840,
|
||||
"catalog": {
|
||||
"materials": 1691,
|
||||
"version": "00474abd322518089309784c",
|
||||
"blocks": 1196,
|
||||
"search_default_limit": 16,
|
||||
"items": 1537,
|
||||
"search_max_limit": 32
|
||||
},
|
||||
"project_id": "shacraft-lobby-v2"
|
||||
},
|
||||
{
|
||||
"tool": "material_search",
|
||||
"bytes": 374
|
||||
},
|
||||
{
|
||||
"tool": "material_describe",
|
||||
"bytes": 324
|
||||
}
|
||||
]
|
||||
},
|
||||
"chat_bridge_restarted": true,
|
||||
"chatgpt_login_preserved": true,
|
||||
"pre_upgrade_backup": true
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 779 KiB |
|
After Width: | Height: | Size: 1010 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,589 @@
|
||||
{
|
||||
"stage": "03-arrival-garden",
|
||||
"writes": 4811,
|
||||
"batches": 12,
|
||||
"final_distinct_changed_blocks": 4734,
|
||||
"actual_block_checks": {
|
||||
"version": 1,
|
||||
"mode": "actual_after_snapshot",
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"world_edits": 0,
|
||||
"desired_blocks": 4734,
|
||||
"baseline_expected_states_verified": 4734,
|
||||
"actual_exact_state_mismatches": 0,
|
||||
"actual_mismatch_examples": [],
|
||||
"fixtures": {
|
||||
"passed": true,
|
||||
"checked": {
|
||||
"leaves": 537,
|
||||
"lanterns": 16,
|
||||
"flowers": 258,
|
||||
"rooted_trees": 12,
|
||||
"root_connected_logs": 73
|
||||
},
|
||||
"failures": []
|
||||
},
|
||||
"walking": {
|
||||
"checked_points": 4955,
|
||||
"checked_route_edges": 0,
|
||||
"failures": [],
|
||||
"passed": true,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"note": "Observed surface samples and vertical headroom; full cubes, slabs and explicit straight bottom-stair samples. Stair samples are a surface profile, not a full player-width collision simulation. Cached snapshot is not a live re-read."
|
||||
},
|
||||
"navigation": {
|
||||
"version": 1,
|
||||
"source": {
|
||||
"x": 0,
|
||||
"z": 9
|
||||
},
|
||||
"world_edits": 0,
|
||||
"method": "Four quarter-center surface samples per clear cell; cardinal half-block BFS, rise/drop <= 0.5 block.",
|
||||
"limitations": "Planned surface topology only. No headroom, body-width collision, material state or live-world verification. Width checks are cardinal cross-sections using the dominant local tangent; full declared masks are also checked.",
|
||||
"geometry_sha256": "b58820302ac02852731c49ac482b0b6684b2aa7fe319d7e9dfc3d8374c972491",
|
||||
"planned_columns": 16309,
|
||||
"clear_columns": 14478,
|
||||
"surface_samples": 57912,
|
||||
"reachable_samples": 57912,
|
||||
"unreachable_clear_columns": [],
|
||||
"unreachable_by_group": {},
|
||||
"station": {
|
||||
"name": "clock-station",
|
||||
"x": -6,
|
||||
"z": -105,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 119.0
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"id": "station-axis",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 593,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 486,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 47,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-axis:start",
|
||||
"x": 0,
|
||||
"z": -37,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "station-axis:end",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "south-axis-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 685,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 565,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 59,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "south-axis-local:start",
|
||||
"x": 0,
|
||||
"z": 57,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 48.0
|
||||
},
|
||||
{
|
||||
"name": "south-axis-local:end",
|
||||
"x": 1,
|
||||
"z": 110,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 102.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "portal-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 531,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 376,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 70,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "portal-radial-local:start",
|
||||
"x": -35,
|
||||
"z": -14,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 57.0
|
||||
},
|
||||
{
|
||||
"name": "portal-radial-local:end",
|
||||
"x": -72,
|
||||
"z": -46,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 126.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lake-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 5,
|
||||
"corridor_columns": 403,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 297,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 63,
|
||||
"minimum_structural_cross_section": 5,
|
||||
"minimum_clear_cross_section": 5,
|
||||
"minimum_reachable_cross_section": 5,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "lake-radial-local:start",
|
||||
"x": -42,
|
||||
"z": 18,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 50.5
|
||||
},
|
||||
{
|
||||
"name": "lake-radial-local:end",
|
||||
"x": -90,
|
||||
"z": 32,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 112.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "east-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 451,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 341,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 49,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "east-radial-local:start",
|
||||
"x": 42,
|
||||
"z": 16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 49.0
|
||||
},
|
||||
{
|
||||
"name": "east-radial-local:end",
|
||||
"x": 82,
|
||||
"z": 8,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 90.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northwest-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 468,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 350,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 53,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northwest-local:start",
|
||||
"x": -40,
|
||||
"z": -65,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 113.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northwest-local:end",
|
||||
"x": -80,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 165.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northeast-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 505,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 374,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 60,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northeast-local:start",
|
||||
"x": 40,
|
||||
"z": -95,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 151.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northeast-local:end",
|
||||
"x": 82,
|
||||
"z": -78,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 210.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "station-entrance-stair",
|
||||
"passed": true,
|
||||
"declared_clear_width": 21,
|
||||
"corridor_columns": 161,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 147,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 7,
|
||||
"minimum_structural_cross_section": 21,
|
||||
"minimum_clear_cross_section": 21,
|
||||
"minimum_reachable_cross_section": 21,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-entrance-stair:start",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
},
|
||||
{
|
||||
"name": "station-entrance-stair:end",
|
||||
"x": -6,
|
||||
"z": -83,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 97.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"decorated_ground_columns_excluded": 662,
|
||||
"bench_front_access": [
|
||||
{
|
||||
"name": "bench--17--17-front--1",
|
||||
"x": -16,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.0
|
||||
},
|
||||
{
|
||||
"name": "bench--17--17-front-0",
|
||||
"x": -17,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.0
|
||||
},
|
||||
{
|
||||
"name": "bench--17--17-front-1",
|
||||
"x": -18,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 42.0
|
||||
},
|
||||
{
|
||||
"name": "bench-17--17-front--1",
|
||||
"x": 18,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 42.5
|
||||
},
|
||||
{
|
||||
"name": "bench-17--17-front-0",
|
||||
"x": 17,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.5
|
||||
},
|
||||
{
|
||||
"name": "bench-17--17-front-1",
|
||||
"x": 16,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.5
|
||||
},
|
||||
{
|
||||
"name": "bench--32-2-front--1",
|
||||
"x": -31,
|
||||
"z": 1,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 38.0
|
||||
},
|
||||
{
|
||||
"name": "bench--32-2-front-0",
|
||||
"x": -31,
|
||||
"z": 2,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 37.0
|
||||
},
|
||||
{
|
||||
"name": "bench--32-2-front-1",
|
||||
"x": -31,
|
||||
"z": 3,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 36.0
|
||||
},
|
||||
{
|
||||
"name": "bench-32-2-front--1",
|
||||
"x": 31,
|
||||
"z": 3,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 36.5
|
||||
},
|
||||
{
|
||||
"name": "bench-32-2-front-0",
|
||||
"x": 31,
|
||||
"z": 2,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 37.5
|
||||
},
|
||||
{
|
||||
"name": "bench-32-2-front-1",
|
||||
"x": 31,
|
||||
"z": 1,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 38.5
|
||||
},
|
||||
{
|
||||
"name": "bench--22-35-front--1",
|
||||
"x": -23,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 47.5
|
||||
},
|
||||
{
|
||||
"name": "bench--22-35-front-0",
|
||||
"x": -22,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.5
|
||||
},
|
||||
{
|
||||
"name": "bench--22-35-front-1",
|
||||
"x": -21,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "bench-22-35-front--1",
|
||||
"x": 21,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.0
|
||||
},
|
||||
{
|
||||
"name": "bench-22-35-front-0",
|
||||
"x": 22,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 47.0
|
||||
},
|
||||
{
|
||||
"name": "bench-22-35-front-1",
|
||||
"x": 23,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 48.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"passed": true,
|
||||
"note": "Static support, leaf-distance and point-sampled navigation checks; not a complete moving-player collision simulation. Actual mode uses sequential observed snapshots.",
|
||||
"input_sha256": {
|
||||
"before": "770a08974e5bf9793c56be780576947dcceb2cd5b8400f479a5356e8509b3146",
|
||||
"layout": "699060010db084dc05c47400572ac4440784d1a0c3dbd877122decd183048923",
|
||||
"metadata": "400f672864117b62ed173600932ca04ab15a0c548587bb3d39f34eaa91484e89",
|
||||
"walk": "453c5ee1e699a59be845db22abc5e481915768ab229a79c0cc6019d8b3e39d0d",
|
||||
"navigation": "1baf3c4ef2c1ddfca22240395fcde97b5210d97c9afe5cb2b6c3275154cfe044",
|
||||
"after": "5310988e559868a7e18a98d2e951d87e789798d6a1027df491ede65bb4bcdff8"
|
||||
}
|
||||
},
|
||||
"actual_surface_checks": {
|
||||
"version": 1,
|
||||
"world": "shacraft_lobby_v2",
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"source": "Full live Paper surface maps plus observed baseline voxels and desired edit overlay",
|
||||
"verified_surface_columns": 589824,
|
||||
"affected_columns": 2823,
|
||||
"outside_edit_columns_verified": 587001,
|
||||
"changed_surface_columns": 2680,
|
||||
"surface_mismatches": 0,
|
||||
"outside_edit_surface_mismatches": 0,
|
||||
"mismatch_examples": [],
|
||||
"original_visible_water_columns": 41467,
|
||||
"visible_water_surface_changes": 0,
|
||||
"water_surface_change_examples": [],
|
||||
"expected_states_checked_against_baseline": 4734,
|
||||
"placed_or_replaced_voxels": 4702,
|
||||
"removed_voxels": 32,
|
||||
"lower_snapshot_voxels_consulted_after_cuts": 12,
|
||||
"observed_water_voxels_replaced": [],
|
||||
"water_preservation_passed": true,
|
||||
"passed": true,
|
||||
"capture_started_at": "2026-09-13T02:35:54.623721211Z",
|
||||
"capture_finished_at": "2026-09-13T02:37:49.861979626Z",
|
||||
"atomic_snapshot": false,
|
||||
"note": "Sequential map captures; edits must be idle during each capture. This checks every visible surface and the observed water blocks in the edit. Hidden untouched blocks outside the voxel survey are not inferred. Live voxel and headroom checks are separate.",
|
||||
"inputs_sha256": {
|
||||
"before_map": "25b7eb12af89e0f9931a56f31cc8eca952b817a34218ffdc4fbf3636e1a3e917",
|
||||
"after_map": "d0dde2583dfbb2f5b37b45a2d1cd5c239b8bfd2e72d4c1434ac280e99f8ed2af",
|
||||
"baseline_voxels": "770a08974e5bf9793c56be780576947dcceb2cd5b8400f479a5356e8509b3146",
|
||||
"desired_layout": "699060010db084dc05c47400572ac4440784d1a0c3dbd877122decd183048923"
|
||||
}
|
||||
},
|
||||
"camera_readiness": "Real Fabric frames; heuristic render readiness, server block state verified separately."
|
||||
}
|
||||
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,695 @@
|
||||
{
|
||||
"version": 1,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"mode": "actual_after_snapshot",
|
||||
"passed": true,
|
||||
"world_edits": 0,
|
||||
"desired_blocks": 710,
|
||||
"guard": {
|
||||
"passed": true,
|
||||
"columns": 302,
|
||||
"piers": 46,
|
||||
"cardinal_components": [
|
||||
31,
|
||||
46,
|
||||
71,
|
||||
74,
|
||||
80
|
||||
],
|
||||
"grounded_footings": 22,
|
||||
"adjacent_road_rails": 6,
|
||||
"failures": []
|
||||
},
|
||||
"protected_road_columns": 2927,
|
||||
"protection_failures": [],
|
||||
"preserved_fixture_states": 1172,
|
||||
"lost_fixtures": [],
|
||||
"fixtures": {
|
||||
"passed": true,
|
||||
"checked": {
|
||||
"leaves": 537,
|
||||
"lanterns": 16,
|
||||
"flowers": 258,
|
||||
"rooted_trees": 12,
|
||||
"root_connected_logs": 73
|
||||
},
|
||||
"failures": []
|
||||
},
|
||||
"walking": {
|
||||
"checked_points": 4815,
|
||||
"checked_route_edges": 0,
|
||||
"failures": [],
|
||||
"passed": true,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"note": "Observed surface samples and vertical headroom; full cubes, slabs and explicit straight bottom-stair samples. Stair samples are a surface profile, not a full player-width collision simulation. Cached snapshot is not a live re-read."
|
||||
},
|
||||
"navigation": {
|
||||
"version": 1,
|
||||
"source": {
|
||||
"x": 0,
|
||||
"z": 9
|
||||
},
|
||||
"world_edits": 0,
|
||||
"method": "Four quarter-center surface samples per clear cell; cardinal half-block BFS, rise/drop <= 0.5 block.",
|
||||
"limitations": "Planned surface topology only. No headroom, body-width collision, material state or live-world verification. Width checks are cardinal cross-sections using the dominant local tangent; full declared masks are also checked.",
|
||||
"geometry_sha256": "4a2b951946c91a09dcef9606bfebd8eba51c34f21e827b019dc0a5f768cf4148",
|
||||
"planned_columns": 16309,
|
||||
"clear_columns": 14338,
|
||||
"surface_samples": 57352,
|
||||
"reachable_samples": 57352,
|
||||
"unreachable_clear_columns": [],
|
||||
"unreachable_by_group": {},
|
||||
"station": {
|
||||
"name": "clock-station",
|
||||
"x": -6,
|
||||
"z": -105,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 119.0
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"id": "station-axis",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 593,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 486,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 47,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-axis:start",
|
||||
"x": 0,
|
||||
"z": -37,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "station-axis:end",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "south-axis-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 685,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 565,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 59,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "south-axis-local:start",
|
||||
"x": 0,
|
||||
"z": 57,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 48.0
|
||||
},
|
||||
{
|
||||
"name": "south-axis-local:end",
|
||||
"x": 1,
|
||||
"z": 110,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 102.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "portal-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 531,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 376,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 70,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "portal-radial-local:start",
|
||||
"x": -35,
|
||||
"z": -14,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 57.0
|
||||
},
|
||||
{
|
||||
"name": "portal-radial-local:end",
|
||||
"x": -72,
|
||||
"z": -46,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 126.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lake-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 5,
|
||||
"corridor_columns": 403,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 297,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 63,
|
||||
"minimum_structural_cross_section": 5,
|
||||
"minimum_clear_cross_section": 5,
|
||||
"minimum_reachable_cross_section": 5,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "lake-radial-local:start",
|
||||
"x": -42,
|
||||
"z": 18,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 50.5
|
||||
},
|
||||
{
|
||||
"name": "lake-radial-local:end",
|
||||
"x": -90,
|
||||
"z": 32,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 112.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "east-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 451,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 341,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 49,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "east-radial-local:start",
|
||||
"x": 42,
|
||||
"z": 16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 49.0
|
||||
},
|
||||
{
|
||||
"name": "east-radial-local:end",
|
||||
"x": 82,
|
||||
"z": 8,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 90.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northwest-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 468,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 350,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 53,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northwest-local:start",
|
||||
"x": -40,
|
||||
"z": -65,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 113.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northwest-local:end",
|
||||
"x": -80,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 165.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northeast-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 505,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 374,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 60,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northeast-local:start",
|
||||
"x": 40,
|
||||
"z": -95,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 151.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northeast-local:end",
|
||||
"x": 82,
|
||||
"z": -78,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 210.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "station-entrance-stair",
|
||||
"passed": true,
|
||||
"declared_clear_width": 21,
|
||||
"corridor_columns": 161,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 147,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 7,
|
||||
"minimum_structural_cross_section": 21,
|
||||
"minimum_clear_cross_section": 21,
|
||||
"minimum_reachable_cross_section": 21,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-entrance-stair:start",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
},
|
||||
{
|
||||
"name": "station-entrance-stair:end",
|
||||
"x": -6,
|
||||
"z": -83,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 97.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"bench_access": [
|
||||
{
|
||||
"name": "bench-access--31-1",
|
||||
"x": -31,
|
||||
"z": 1,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 38.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--31-2",
|
||||
"x": -31,
|
||||
"z": 2,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 37.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--31-3",
|
||||
"x": -31,
|
||||
"z": 3,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 36.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--23-32",
|
||||
"x": -23,
|
||||
"z": 32,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--23-33",
|
||||
"x": -23,
|
||||
"z": 33,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--23-34",
|
||||
"x": -23,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 47.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--22-33",
|
||||
"x": -22,
|
||||
"z": 33,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--22-34",
|
||||
"x": -22,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--21-34",
|
||||
"x": -21,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access--18--16",
|
||||
"x": -18,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 42.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--18--15",
|
||||
"x": -18,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--18--14",
|
||||
"x": -18,
|
||||
"z": -14,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--17--16",
|
||||
"x": -17,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--17--15",
|
||||
"x": -17,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--16--16",
|
||||
"x": -16,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access--16--15",
|
||||
"x": -16,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 39.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-16--16",
|
||||
"x": 16,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-16--15",
|
||||
"x": 16,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 39.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-17--16",
|
||||
"x": 17,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-17--15",
|
||||
"x": 17,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-18--16",
|
||||
"x": 18,
|
||||
"z": -16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 42.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-18--15",
|
||||
"x": 18,
|
||||
"z": -15,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 41.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-18--14",
|
||||
"x": 18,
|
||||
"z": -14,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 40.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-21-34",
|
||||
"x": 21,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-22-33",
|
||||
"x": 22,
|
||||
"z": 33,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-22-34",
|
||||
"x": 22,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 47.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-23-32",
|
||||
"x": 23,
|
||||
"z": 32,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 46.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-23-33",
|
||||
"x": 23,
|
||||
"z": 33,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 47.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-23-34",
|
||||
"x": 23,
|
||||
"z": 34,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 48.0
|
||||
},
|
||||
{
|
||||
"name": "bench-access-31-1",
|
||||
"x": 31,
|
||||
"z": 1,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 38.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-31-2",
|
||||
"x": 31,
|
||||
"z": 2,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 37.5
|
||||
},
|
||||
{
|
||||
"name": "bench-access-31-3",
|
||||
"x": 31,
|
||||
"z": 3,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 36.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"volume": {
|
||||
"passed": true,
|
||||
"outside_desired_voxels": 555598,
|
||||
"desired_voxels": 710,
|
||||
"mismatch_examples": [],
|
||||
"scope_note": "Every voxel inside the supplied before snapshot; no claim for unobserved exterior voxels."
|
||||
},
|
||||
"note": "Point-sampled body headroom and navigation; not a full moving-player collision simulation.",
|
||||
"input_sha256": {
|
||||
"layout": "6ec0e65027080207dc8e5f7b19afaa7b33f6ddb723de5c3632cc6097592da581",
|
||||
"metadata": "242507f39a354f04281bdf4a32a2fe05d9c144af9d1b7c7e7e8ddecc17976114",
|
||||
"garden_plan": "699060010db084dc05c47400572ac4440784d1a0c3dbd877122decd183048923",
|
||||
"garden_metadata": "400f672864117b62ed173600932ca04ab15a0c548587bb3d39f34eaa91484e89",
|
||||
"garden_walk": "453c5ee1e699a59be845db22abc5e481915768ab229a79c0cc6019d8b3e39d0d",
|
||||
"navigation": "1baf3c4ef2c1ddfca22240395fcde97b5210d97c9afe5cb2b6c3275154cfe044",
|
||||
"before": "3172d03627e625d759a3b9ce881282e26db2926c35dd68ac0036047ae33c3488",
|
||||
"after": "c77e8f953f257822f6f6489d397ab39f40459c18183743dbdbea3e168a193c65"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 669 KiB |
@@ -0,0 +1,551 @@
|
||||
{
|
||||
"stage": "02-foundations-zones-01-02",
|
||||
"total_checked_writes": 79081,
|
||||
"journalled_batches": 67,
|
||||
"final_changed_voxels": 78673,
|
||||
"checks": {
|
||||
"surface-verification": {
|
||||
"version": 1,
|
||||
"world": "shacraft_lobby_v2",
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"source": "Full live Paper surface maps plus observed baseline voxels and desired edit overlay",
|
||||
"verified_surface_columns": 589824,
|
||||
"affected_columns": 16901,
|
||||
"outside_edit_columns_verified": 572923,
|
||||
"changed_surface_columns": 16901,
|
||||
"surface_mismatches": 0,
|
||||
"outside_edit_surface_mismatches": 0,
|
||||
"mismatch_examples": [],
|
||||
"original_visible_water_columns": 41467,
|
||||
"visible_water_surface_changes": 0,
|
||||
"water_surface_change_examples": [],
|
||||
"expected_states_checked_against_baseline": 78673,
|
||||
"placed_or_replaced_voxels": 77950,
|
||||
"removed_voxels": 723,
|
||||
"lower_snapshot_voxels_consulted_after_cuts": 12,
|
||||
"observed_water_voxels_replaced": [],
|
||||
"water_preservation_passed": true,
|
||||
"passed": true,
|
||||
"capture_started_at": "2026-09-13T01:47:40.367423160Z",
|
||||
"capture_finished_at": "2026-09-13T01:49:35.666216379Z",
|
||||
"atomic_snapshot": false,
|
||||
"note": "Sequential map captures; edits must be idle during each capture. This checks every visible surface and the observed water blocks in the edit. Hidden untouched blocks outside the voxel survey are not inferred. Live voxel and headroom checks are separate.",
|
||||
"inputs_sha256": {
|
||||
"before_map": "45612085877b44b0fc58a67f3c140049452b3fc8e8798ff816bd368852a2eaa2",
|
||||
"after_map": "b4bc17c20864543fab5b03dd1f6ccecba2b79c261fa45f3c8497f1824fb8ccfa",
|
||||
"baseline_voxels": "d53662eac3c06ed79d86ba5da9ce0e95c32ade146f6eb9f4f30782ec51ae8677",
|
||||
"desired_layout": "22da24b01fa5d14f278253e48d97721ffb5341543b46827c17617020c1b47374"
|
||||
}
|
||||
},
|
||||
"walk-verification": {
|
||||
"checked_points": 15708,
|
||||
"checked_route_edges": 0,
|
||||
"failures": [],
|
||||
"passed": true,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"note": "Observed surface samples and vertical headroom; full cubes, slabs and explicit straight bottom-stair samples. Stair samples are a surface profile, not a full player-width collision simulation. Cached snapshot is not a live re-read."
|
||||
},
|
||||
"block-verification": {
|
||||
"version": 1,
|
||||
"source": "Complete final observed block-state survey",
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"verified_desired_blocks": 78673,
|
||||
"mismatches": 0,
|
||||
"mismatch_examples": [],
|
||||
"passed": true,
|
||||
"verified_by_desired_material": {
|
||||
"minecraft:deepslate_bricks": 2983,
|
||||
"minecraft:polished_andesite": 2635,
|
||||
"minecraft:stone_brick_stairs": 724,
|
||||
"minecraft:stone": 53085,
|
||||
"minecraft:stone_bricks": 4399,
|
||||
"minecraft:smooth_sandstone": 5776,
|
||||
"minecraft:smooth_stone": 6343,
|
||||
"minecraft:stone_brick_wall": 670,
|
||||
"minecraft:grass_block": 561,
|
||||
"minecraft:air": 723,
|
||||
"minecraft:chiseled_stone_bricks": 27,
|
||||
"minecraft:smooth_stone_slab": 27,
|
||||
"minecraft:lantern": 27,
|
||||
"minecraft:smooth_quartz": 236,
|
||||
"minecraft:green_concrete": 457
|
||||
},
|
||||
"verified_by_group": {
|
||||
"lake-radial-local-foundation": 963,
|
||||
"lake-radial-local-paving": 403,
|
||||
"edge-balustrades": 651,
|
||||
"remove-obsolete-survey": 572,
|
||||
"ring-northwest-local-foundation": 1116,
|
||||
"ring-northwest-local-paving": 468,
|
||||
"ring-northwest-local-clearance": 30,
|
||||
"station-west-pilasters": 184,
|
||||
"clock-station-foundation": 31398,
|
||||
"station-west-arch-stones": 20,
|
||||
"clock-station-paving": 5508,
|
||||
"lamp-piers": 81,
|
||||
"lamps": 27,
|
||||
"station-west-blind-arcade": 146,
|
||||
"portal-radial-local-foundation": 1054,
|
||||
"portal-radial-local-paving": 531,
|
||||
"portal-radial-local-clearance": 219,
|
||||
"station-structural-bands": 256,
|
||||
"lake-radial-local-clearance": 4,
|
||||
"arrival-hex-foundation": 14211,
|
||||
"arrival-hex-paving": 4741,
|
||||
"station-forecourt-foundation": 3113,
|
||||
"station-forecourt-paving": 1298,
|
||||
"clock-station-clearance": 44,
|
||||
"station-entrance-stair-foundation": 418,
|
||||
"station-entrance-stair-paving": 161,
|
||||
"station-forecourt-clearance": 86,
|
||||
"arrival-medallion": 693,
|
||||
"arrival-hex-clearance": 203,
|
||||
"station-axis-foundation": 1166,
|
||||
"station-axis-paving": 582,
|
||||
"station-axis-clearance": 83,
|
||||
"south-axis-local-foundation": 1658,
|
||||
"south-axis-local-paving": 685,
|
||||
"south-axis-local-clearance": 27,
|
||||
"ring-northeast-local-foundation": 2663,
|
||||
"station-ne-connection-foundation": 143,
|
||||
"station-ne-connection-paving": 27,
|
||||
"ring-northeast-local-paving": 505,
|
||||
"east-radial-local-foundation": 1963,
|
||||
"east-radial-local-paving": 451,
|
||||
"ring-northeast-local-clearance": 8,
|
||||
"east-radial-local-clearance": 8,
|
||||
"temporary-bridge-threshold": 86,
|
||||
"temporary-bridge-gates": 19
|
||||
},
|
||||
"desired_layout_sha256": "22da24b01fa5d14f278253e48d97721ffb5341543b46827c17617020c1b47374",
|
||||
"snapshot_started_at": "2026-09-13T01:47:42.024943+00:00",
|
||||
"snapshot_finished_at": "2026-09-13T01:48:27.217492+00:00",
|
||||
"atomic_snapshot": false,
|
||||
"note": "Full block states include slab/stair orientation, waterlogging and other properties. Sequential read snapshot; edits were completed before capture."
|
||||
},
|
||||
"navigation-verification": {
|
||||
"version": 1,
|
||||
"source": {
|
||||
"x": 0,
|
||||
"z": 9
|
||||
},
|
||||
"world_edits": 0,
|
||||
"method": "Four quarter-center surface samples per clear cell; cardinal half-block BFS, rise/drop <= 0.5 block.",
|
||||
"limitations": "Planned surface topology only. No headroom, body-width collision, material state or live-world verification. Width checks are cardinal cross-sections using the dominant local tangent; full declared masks are also checked.",
|
||||
"geometry_sha256": "e5c78e645100261d1720e50ea23eaa65650eb796341facb9a50bed0ab392e736",
|
||||
"planned_columns": 16309,
|
||||
"clear_columns": 15140,
|
||||
"surface_samples": 60560,
|
||||
"reachable_samples": 60560,
|
||||
"unreachable_clear_columns": [],
|
||||
"unreachable_by_group": {},
|
||||
"station": {
|
||||
"name": "clock-station",
|
||||
"x": -6,
|
||||
"z": -105,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 119.0
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"id": "station-axis",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 593,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 486,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 47,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-axis:start",
|
||||
"x": 0,
|
||||
"z": -37,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 45.5
|
||||
},
|
||||
{
|
||||
"name": "station-axis:end",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "south-axis-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 9,
|
||||
"corridor_columns": 685,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 565,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 59,
|
||||
"minimum_structural_cross_section": 9,
|
||||
"minimum_clear_cross_section": 9,
|
||||
"minimum_reachable_cross_section": 9,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "south-axis-local:start",
|
||||
"x": 0,
|
||||
"z": 57,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 48.0
|
||||
},
|
||||
{
|
||||
"name": "south-axis-local:end",
|
||||
"x": 1,
|
||||
"z": 110,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 102.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "portal-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 531,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 376,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 70,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "portal-radial-local:start",
|
||||
"x": -35,
|
||||
"z": -14,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 57.0
|
||||
},
|
||||
{
|
||||
"name": "portal-radial-local:end",
|
||||
"x": -72,
|
||||
"z": -46,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 126.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lake-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 5,
|
||||
"corridor_columns": 403,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 297,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 63,
|
||||
"minimum_structural_cross_section": 5,
|
||||
"minimum_clear_cross_section": 5,
|
||||
"minimum_reachable_cross_section": 5,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "lake-radial-local:start",
|
||||
"x": -42,
|
||||
"z": 18,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 50.5
|
||||
},
|
||||
{
|
||||
"name": "lake-radial-local:end",
|
||||
"x": -90,
|
||||
"z": 32,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 112.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "east-radial-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 451,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 341,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 49,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "east-radial-local:start",
|
||||
"x": 42,
|
||||
"z": 16,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 49.0
|
||||
},
|
||||
{
|
||||
"name": "east-radial-local:end",
|
||||
"x": 82,
|
||||
"z": 8,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 90.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northwest-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 468,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 350,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 53,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northwest-local:start",
|
||||
"x": -40,
|
||||
"z": -65,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 113.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northwest-local:end",
|
||||
"x": -80,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 165.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ring-northeast-local",
|
||||
"passed": true,
|
||||
"declared_clear_width": 7,
|
||||
"corridor_columns": 505,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 374,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 60,
|
||||
"minimum_structural_cross_section": 7,
|
||||
"minimum_clear_cross_section": 7,
|
||||
"minimum_reachable_cross_section": 7,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "ring-northeast-local:start",
|
||||
"x": 40,
|
||||
"z": -95,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 151.0
|
||||
},
|
||||
{
|
||||
"name": "ring-northeast-local:end",
|
||||
"x": 82,
|
||||
"z": -78,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 210.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "station-entrance-stair",
|
||||
"passed": true,
|
||||
"declared_clear_width": 21,
|
||||
"corridor_columns": 161,
|
||||
"missing_corridor_columns": [],
|
||||
"declared_clear_columns": 147,
|
||||
"nonclear_declared_columns": [],
|
||||
"unreachable_declared_columns": [],
|
||||
"cross_sections": 7,
|
||||
"minimum_structural_cross_section": 21,
|
||||
"minimum_clear_cross_section": 21,
|
||||
"minimum_reachable_cross_section": 21,
|
||||
"narrow_cross_sections": [],
|
||||
"centerline_invalid_steps": [],
|
||||
"centerline_maximum_boundary_step": 0.5,
|
||||
"centerline_jumps": [],
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "station-entrance-stair:start",
|
||||
"x": -6,
|
||||
"z": -77,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 91.0
|
||||
},
|
||||
{
|
||||
"name": "station-entrance-stair:end",
|
||||
"x": -6,
|
||||
"z": -83,
|
||||
"surface_samples": 4,
|
||||
"reachable_samples": 4,
|
||||
"passed": true,
|
||||
"shortest_surface_route_blocks": 97.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"passed": true,
|
||||
"final_walkable_mask": "Only columns explicitly covered by the passed actual support/headroom verification",
|
||||
"non_walkable_perimeter_floor_columns_excluded": 454,
|
||||
"non_walkable_perimeter_floor_examples": [
|
||||
[
|
||||
-51,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-50,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-49,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-48,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-47,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-46,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-45,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-24,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-23,
|
||||
-145
|
||||
],
|
||||
[
|
||||
-22,
|
||||
-145
|
||||
]
|
||||
]
|
||||
},
|
||||
"lake-native-toe-verification": {
|
||||
"checked_points": 3,
|
||||
"checked_route_edges": 0,
|
||||
"failures": [],
|
||||
"passed": true,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"note": "Observed surface samples and vertical headroom; full cubes, slabs and explicit straight bottom-stair samples. Stair samples are a surface profile, not a full player-width collision simulation. Cached snapshot is not a live re-read.",
|
||||
"observed_supports": [
|
||||
{
|
||||
"x": -91,
|
||||
"z": 31,
|
||||
"y": 83,
|
||||
"state": "minecraft:white_concrete"
|
||||
},
|
||||
{
|
||||
"x": -91,
|
||||
"z": 32,
|
||||
"y": 83,
|
||||
"state": "minecraft:grass_block[snowy=false]"
|
||||
},
|
||||
{
|
||||
"x": -91,
|
||||
"z": 33,
|
||||
"y": 83,
|
||||
"state": "minecraft:grass_block[snowy=false]"
|
||||
}
|
||||
],
|
||||
"source": "live targeted RPC scan of native shore immediately outside the edit boundary"
|
||||
}
|
||||
},
|
||||
"archive": "/home/emil/Desktop/Shacraft-Lobby-Archive",
|
||||
"world_backup": "/home/emil/Desktop/minecraft-builder-mcp/.runtime/projects/shacraft-foundations-01-02-20260913"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 791 KiB |
|
After Width: | Height: | Size: 814 KiB |
|
After Width: | Height: | Size: 486 KiB |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"world": "shacraft_lobby_v2",
|
||||
"source": "live Paper surface maps + bounded RPC water reads",
|
||||
"verified_surface_columns": 589824,
|
||||
"surface_mismatches": 0,
|
||||
"unique_marker_blocks": 16967,
|
||||
"planned_writes": 17040,
|
||||
"original_water_columns": 42399,
|
||||
"water_columns_obscured_by_markers": 932,
|
||||
"water_obscured_by_markers_verified_intact": true,
|
||||
"height_unchanged_columns": 587098,
|
||||
"capture_started_at": "2026-09-13T01:03:10.136933937Z",
|
||||
"capture_finished_at": "2026-09-13T01:05:10.066341053Z",
|
||||
"atomic_snapshot": false,
|
||||
"note": "World edits were idle during each capture. Terrain follows its original heights; raised markers represent future structures, not finished traversable paths."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recipe_id": "b013cd330993b262ed0192659c9276af33e1c6fdb749a02ad332086a6d99e56a",
|
||||
"chunks": 2304,
|
||||
"mismatches": 0,
|
||||
"water_columns": 59609,
|
||||
"min_surface_y": 48,
|
||||
"max_surface_y": 203,
|
||||
"elapsed_ms": 144136,
|
||||
"world": "shacraft_lobby",
|
||||
"verified_columns": 589824
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"note": "Target surface only; no live world reads. Bounds clip writes, not height calculations. Cache holds 32 recipes until restart; save the recipe to regenerate the same ID.",
|
||||
"world_verified": false,
|
||||
"tile_budget": 4096,
|
||||
"sampled_height_min": 22,
|
||||
"tile_count": 27648,
|
||||
"mode": "sculpt",
|
||||
"status": "completed",
|
||||
"tile_order": "x_then_z_then_y",
|
||||
"tile_edge": 16,
|
||||
"bounds": {
|
||||
"worldId": "terrain",
|
||||
"min": {
|
||||
"x": -384,
|
||||
"y": 16,
|
||||
"z": -384
|
||||
},
|
||||
"max": {
|
||||
"x": 383,
|
||||
"y": 207,
|
||||
"z": 383
|
||||
}
|
||||
},
|
||||
"sampled_height_max": 203,
|
||||
"terrain_id": "b013cd330993b262ed0192659c9276af33e1c6fdb749a02ad332086a6d99e56a",
|
||||
"sample_grid": {
|
||||
"height": 256,
|
||||
"width": 256
|
||||
},
|
||||
"legend": "Dark green \u003d low; pale stone \u003d high; pink \u003d preserved at sampled surface",
|
||||
"kind": "terrain_heightmap_preview",
|
||||
"clipped_samples": 0,
|
||||
"mimeType": "image/png",
|
||||
"orientation": "north (-Z) up; east (+X) right"
|
||||
}
|
||||
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"elapsed_ms": 274728,
|
||||
"max_surface_y": 204,
|
||||
"min_surface_y": 48,
|
||||
"water_columns": 42399,
|
||||
"mismatches": 0,
|
||||
"chunks": 2304,
|
||||
"recipe_id": "fed10bf28fa544e7dba08d10c76a1130c67292856999b06a9fd672feeed39296",
|
||||
"verified_columns": 589824,
|
||||
"world": "shacraft_lobby_v2"
|
||||
}
|
||||
|
After Width: | Height: | Size: 946 KiB |
|
After Width: | Height: | Size: 648 KiB |
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"note": "Target surface only; no live world reads. Bounds clip writes, not height calculations. Cache holds 32 recipes until restart; save the recipe to regenerate the same ID.",
|
||||
"world_verified": false,
|
||||
"tile_budget": 4096,
|
||||
"sampled_height_min": -43,
|
||||
"tile_count": 27648,
|
||||
"mode": "sculpt",
|
||||
"status": "completed",
|
||||
"tile_order": "x_then_z_then_y",
|
||||
"tile_edge": 16,
|
||||
"bounds": {
|
||||
"worldId": "terrain",
|
||||
"min": {
|
||||
"x": -384,
|
||||
"y": -48,
|
||||
"z": -384
|
||||
},
|
||||
"max": {
|
||||
"x": 383,
|
||||
"y": 143,
|
||||
"z": 383
|
||||
}
|
||||
},
|
||||
"sampled_height_max": 139,
|
||||
"terrain_id": "5f0c33cd7152806995906c6b9ee51024ddbe90c5e7f6585e653552df2f4aabf8",
|
||||
"sample_grid": {
|
||||
"height": 256,
|
||||
"width": 256
|
||||
},
|
||||
"legend": "Dark green \u003d low; pale stone \u003d high; pink \u003d preserved at sampled surface",
|
||||
"kind": "terrain_heightmap_preview",
|
||||
"clipped_samples": 0,
|
||||
"mimeType": "image/png",
|
||||
"orientation": "north (-Z) up; east (+X) right"
|
||||
}
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"world_applied": false,
|
||||
"water_level": 48,
|
||||
"fields": [
|
||||
{
|
||||
"name": "V1 \u2014 rectangular platforms",
|
||||
"min_ground_y": 22.0,
|
||||
"max_ground_y": 203.0,
|
||||
"water_columns": 59609,
|
||||
"slope_p95": 3.0
|
||||
},
|
||||
{
|
||||
"name": "Study \u2014 ridges, soft hills, winding water",
|
||||
"min_ground_y": 31.35810089111328,
|
||||
"max_ground_y": 204.61903381347656,
|
||||
"water_columns": 42399,
|
||||
"slope_p95": 3.5214755985851562
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 820 KiB |
|
After Width: | Height: | Size: 936 KiB |
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": 1,
|
||||
"scope": {
|
||||
"project_id": "shacraft-lobby-v2",
|
||||
"world_id": "6679385a-b67f-4e88-b6be-33836ced6f2d",
|
||||
"world_epoch": "b7626ccf-9c75-4b00-9cf3-b8a42231c754"
|
||||
},
|
||||
"world_edits": 0,
|
||||
"mode": "actual_after_snapshot",
|
||||
"desired_blocks": 11502,
|
||||
"interior_columns": 5888,
|
||||
"independently_derived_wall_columns": 276,
|
||||
"interior_voxels": 117673,
|
||||
"interior_excluded_voxels": 87,
|
||||
"volume_components": {
|
||||
"components": [
|
||||
{
|
||||
"voxels": 117673,
|
||||
"nonfull_voxels": 117473,
|
||||
"contains_source": true
|
||||
}
|
||||
],
|
||||
"source_in_volume": true,
|
||||
"note": "Disconnected sealed components are reported, not treated as escapes. The entire required membrane and exterior flood are checked."
|
||||
},
|
||||
"membrane": {
|
||||
"passed": true,
|
||||
"required_voxels": 17276,
|
||||
"parts": {
|
||||
"wall": 5457,
|
||||
"floor": 5870,
|
||||
"folded_boundary": 61,
|
||||
"roof": 5888
|
||||
},
|
||||
"observed_materials": {
|
||||
"barrier": 11396,
|
||||
"smooth_sandstone": 4080,
|
||||
"smooth_stone": 447,
|
||||
"cut_sandstone": 425,
|
||||
"polished_andesite": 137,
|
||||
"stone_bricks": 16,
|
||||
"waxed_oxidized_cut_copper": 12,
|
||||
"chiseled_stone_bricks": 16,
|
||||
"grass_block": 377,
|
||||
"dirt": 12,
|
||||
"green_concrete": 356,
|
||||
"gold_block": 2
|
||||
},
|
||||
"nonfull_voxels": 0,
|
||||
"examples": [],
|
||||
"method": "Every voxel in N6(interior volume) minus interior volume must be an observed full collision cube."
|
||||
},
|
||||
"exterior_flood": {
|
||||
"passed": true,
|
||||
"observed_voxels": 220584,
|
||||
"exterior_reached_voxels": 78339,
|
||||
"interior_reached_from_exterior": 0,
|
||||
"source_reached_from_exterior": false,
|
||||
"unknown_or_partial_blocks_treated_as_empty": {
|
||||
"stone_brick_wall": 293,
|
||||
"stone_brick_stairs": 42,
|
||||
"smooth_sandstone_slab": 505,
|
||||
"iron_bars": 128,
|
||||
"waxed_oxidized_cut_copper_stairs": 64,
|
||||
"iron_chain": 16,
|
||||
"spruce_leaves": 479,
|
||||
"lantern": 16,
|
||||
"oxeye_daisy": 60,
|
||||
"azure_bluet": 39,
|
||||
"white_tulip": 62,
|
||||
"oak_leaves": 58,
|
||||
"allium": 50,
|
||||
"spruce_fence": 18,
|
||||
"spruce_stairs": 18,
|
||||
"pink_tulip": 49,
|
||||
"smooth_sandstone_stairs": 8,
|
||||
"waxed_oxidized_cut_copper_slab": 3
|
||||
},
|
||||
"leak_examples": [],
|
||||
"method": "Six-neighbor free-voxel exterior flood; only known full collision cubes obstruct it."
|
||||
},
|
||||
"player_probes": {
|
||||
"passed": true,
|
||||
"player_width": 0.6,
|
||||
"player_height": 1.8,
|
||||
"source_headroom_observed_air": true,
|
||||
"swept_crossing_probes": {
|
||||
"diagonal": 3288,
|
||||
"cardinal": 2280,
|
||||
"folded_boundary": 178,
|
||||
"floor": 1,
|
||||
"roof": 1
|
||||
},
|
||||
"unblocked_probes": 0,
|
||||
"examples": [],
|
||||
"note": "Supplementary swept-AABB boundary probes; the independently derived full membrane is the complete static containment criterion."
|
||||
},
|
||||
"preserved_decor": {
|
||||
"passed": true,
|
||||
"protected_observed_states": 1016,
|
||||
"changed_states": 0,
|
||||
"examples": []
|
||||
},
|
||||
"volume": {
|
||||
"passed": true,
|
||||
"outside_desired_voxels": 610596,
|
||||
"desired_voxels": 11502,
|
||||
"mismatch_examples": [],
|
||||
"scope_note": "Every voxel inside the supplied before snapshot; no claim for unobserved exterior voxels."
|
||||
},
|
||||
"passed": true,
|
||||
"limitations": "Static continuous collision containment for normal nonspectator players only. No protection against spectator, breaking/removing blocks, operator commands, plugin teleports or arbitrary discontinuous teleport/pearl behavior. Actual snapshots are sequential, not atomic.",
|
||||
"input_sha256": {
|
||||
"before": "9332c546f50b174ec39b01430525eb903c926b43cbdfaa956a539759fbf0f7b2",
|
||||
"after": "5fdbffc42df7a799e792bf5b31c1c45f5a7e02e09844860898bcdd2f79e199b3",
|
||||
"layout": "95b430dd0815c8b0d1b87932e9ac1033413f3e82766dd1d4e67aefe4cbaecfa8",
|
||||
"metadata": "12caa7f14f15128736a0525a765f67a9d9f6b343fc912b36cd41d2e76ab39904"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 698 KiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"capturedAt": "2026-09-13T12:33:57.784380033Z",
|
||||
"dimension": "minecraft:shacraft_lobby_v2",
|
||||
"x": -6.0,
|
||||
"y": 113.0,
|
||||
"z": -24.0,
|
||||
"eyeY": 114.61999988555908,
|
||||
"yaw": 180.0,
|
||||
"pitch": -8.0,
|
||||
"fov": 95,
|
||||
"readiness": "local_chunks_and_render_queue_stable",
|
||||
"serverRevisionVerified": false,
|
||||
"loadedChunkRadius": 1,
|
||||
"stabilizationTicks": 40,
|
||||
"stabilizationFrames": 3,
|
||||
"status": "completed",
|
||||
"mimeType": "image/png",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"sourceWidth": 1280,
|
||||
"sourceHeight": 720,
|
||||
"captureId": "4a5e4d45-78f8-4f3b-9c2a-a0a665c29fe4",
|
||||
"imageSha256": "b3f36b132e62a661e8269b405dcc6e3e48ab3c20eb58894e333d2bbf92cbf262",
|
||||
"imageBytes": 856573,
|
||||
"elapsedSeconds": 5.063,
|
||||
"transport": "authenticated Paper HTTP camera_capture",
|
||||
"testMode": "one owner/spectator client"
|
||||
}
|
||||
|
After Width: | Height: | Size: 836 KiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"capturedAt": "2026-09-13T12:37:51.019066137Z",
|
||||
"dimension": "minecraft:shacraft_lobby_v2",
|
||||
"x": 70.0,
|
||||
"y": 151.0,
|
||||
"z": -39.0,
|
||||
"eyeY": 152.61999988555908,
|
||||
"yaw": 133.0,
|
||||
"pitch": 22.0,
|
||||
"fov": 75,
|
||||
"readiness": "local_chunks_and_render_queue_stable",
|
||||
"serverRevisionVerified": false,
|
||||
"loadedChunkRadius": 1,
|
||||
"stabilizationTicks": 90,
|
||||
"stabilizationFrames": 3,
|
||||
"status": "completed",
|
||||
"mimeType": "image/png",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"sourceWidth": 1280,
|
||||
"sourceHeight": 720,
|
||||
"captureId": "c352f761-9dd9-409a-94f1-fc4fd364d7cc",
|
||||
"imageSha256": "c06c1fbd498b0376f639e788f142c91bdb0e3509fa7ff3ce5846748311dd444e",
|
||||
"imageBytes": 1302879,
|
||||
"elapsedSeconds": 9.867,
|
||||
"transport": "authenticated Paper HTTP camera_capture",
|
||||
"testMode": "one owner/spectator client"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"capturedAt": "2026-09-13T12:37:05.830431206Z",
|
||||
"dimension": "minecraft:shacraft_lobby_v2",
|
||||
"x": -46.0,
|
||||
"y": 119.0,
|
||||
"z": -104.0,
|
||||
"eyeY": 120.61999988555908,
|
||||
"yaw": 225.0,
|
||||
"pitch": 12.0,
|
||||
"fov": 85,
|
||||
"readiness": "local_chunks_and_render_queue_stable",
|
||||
"serverRevisionVerified": false,
|
||||
"loadedChunkRadius": 1,
|
||||
"stabilizationTicks": 40,
|
||||
"stabilizationFrames": 3,
|
||||
"status": "completed",
|
||||
"mimeType": "image/png",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"sourceWidth": 1280,
|
||||
"sourceHeight": 720,
|
||||
"captureId": "74fc1c80-5328-4dd4-90c7-4ce53661d814",
|
||||
"imageSha256": "b5d319d2f216b6d6c8f9496e612a2e0066718c660ccad09fc959fedd9ed14354",
|
||||
"imageBytes": 1259152,
|
||||
"elapsedSeconds": 5.135,
|
||||
"transport": "authenticated Paper HTTP camera_capture",
|
||||
"testMode": "one owner/spectator client"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"capturedAt": "2026-09-13T12:29:28.293904723Z",
|
||||
"dimension": "minecraft:shacraft_lobby_v2",
|
||||
"x": -5.5,
|
||||
"y": 100.0,
|
||||
"z": -91.5,
|
||||
"eyeY": 101.61999988555908,
|
||||
"yaw": 180.0,
|
||||
"pitch": -6.0,
|
||||
"fov": 85,
|
||||
"readiness": "local_chunks_and_render_queue_stable",
|
||||
"serverRevisionVerified": false,
|
||||
"loadedChunkRadius": 1,
|
||||
"stabilizationTicks": 150,
|
||||
"stabilizationFrames": 3,
|
||||
"status": "completed",
|
||||
"mimeType": "image/png",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"sourceWidth": 1280,
|
||||
"sourceHeight": 720,
|
||||
"captureId": "14496563-40e4-4c8b-a7cc-d111ad178b9d",
|
||||
"imageSha256": "b420b4b71b22812a4f5bfc9464d1192856dd8d166e66526b579ff6c3e19ea905",
|
||||
"imageBytes": 1024197,
|
||||
"elapsedSeconds": 16.526,
|
||||
"transport": "authenticated Paper HTTP camera_capture",
|
||||
"testMode": "one owner/spectator client"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1000 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
@@ -0,0 +1,40 @@
|
||||
# Shacraft clock station: vestibule and SMASH
|
||||
|
||||
Design v1, 13 September 2026. **Proposal only; no world edits were made.**
|
||||
|
||||
This design fits the saved, existing zone 02 foundation in `shacraft_lobby_v2`. The current station has a deck and setting-out bands, not a completed above-ground building. Its fixed overall envelope is 115 by 63 blocks; the occupied footprint is irregular. The final stage 02 geometry contains 5,764 station columns, including the east-edge recess. The earlier ideal outline contained 5,823 columns and is not the exact placement authority.
|
||||
|
||||
## Drawings
|
||||
|
||||
- [Coordinate plans and section](00-coordinate-plan.png) · [printable PDF](00-coordinate-plan.pdf)
|
||||
- [First-floor vestibule](01-vestibule.png)
|
||||
- [Second-floor SMASH hall](02-smash-floor.png)
|
||||
- [Clock station cutaway](03-cutaway.png)
|
||||
- [Exact coordinate specification and checks](layout.json)
|
||||
- [Full image prompts](prompts.json)
|
||||
|
||||
The three perspective images were generated with the **built-in image_gen tool**. They establish architectural character and atmosphere. The coordinate plan and JSON govern dimensions and placement: generated ornament, block counts, lettering, map thumbnails and roof shapes are illustrative. In particular, the invented slogans in the SMASH image are not approved Shacraft copy. The technical plan was drawn deterministically with Pillow from the saved foundation cells.
|
||||
|
||||
## Spatial arrangement
|
||||
|
||||
The south entrance keeps the existing approach axis at X=-6. The exterior staircase is 21 blocks clear; the entrance and principal approach aisle are seven blocks clear. A green copper and warm brass-colored lift surround is visible from the doorway, with a small directory beside it. Benches and planting occupy the side galleries. Column positions align vertically; pale stone arches and spruce ceiling panels continue the original clock-station reference.
|
||||
|
||||
The first-floor paving remains at block Y98, with feet at Y99. The two-block intermediate deck occupies Y111 and Y112; SMASH is walked at Y113. This gives 12 blocks of structural clear height below the intermediate deck and 11 above it, before the next ceiling begins at Y124. Hanging lamps and decorative beams may reduce local headroom; keep at least four clear blocks over public circulation. The proposed main roof has eaves at Y126 and ridge at Y140; the clock-tower peak is Y158. These are proposed heights, not measurements of an existing shell.
|
||||
|
||||
The lift housing occupies X=-10..-2, Z=-124..-116 on both floors. Two-block walls leave a 5 by 5 clear cabin, with a five-block opening facing south. The suggested eventual behavior is a personal floor selection and transition between enclosed stationary cabins, preventing one player's selection from moving everyone else. Lift controls, animation and world transfers are not implemented by this design. Future floors can reuse the shaft alignment, but their height, roof integration and circulation need a further design pass.
|
||||
|
||||
## SMASH floor
|
||||
|
||||
The user confirmed the Shotbow-style mode: accumulating damage increases knockback, players double-jump and attempt to knock opponents off the arena. See [Shotbow's getting-started guide](https://wiki.shotbow.net/SMASH_Getting_Started).
|
||||
|
||||
Six seven-by-five selection alcoves line the north wall, with three blocks between adjacent alcoves and a seven-block clear aisle in front. Each contains an arena illustration and an ordinary sign at reachable height. Slots 01–06 are **unassigned placeholders**: arena names, destinations and live status are not invented here. Later, each sign can connect to its arena world or queue.
|
||||
|
||||
A 19 by 15 decorative island diorama occupies the western part of the main hall. Its small islands and static figures explain knockback visually. The display sits above a solid floor, behind a stone-and-glass enclosure. It is scenery, not a playable arena or a hole through the building. The west gallery explains damage, knockback and double jumping; the east gallery provides waiting space. Burnt-orange and dark-red accents distinguish SMASH while retaining Shacraft's green copper, cream stone, spruce and warm lighting.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`draw-plan.py` checks all specified columns, benches, the lift housing, display and selection bays against a two-block setback inside the saved foundation. It checks for furnishing overlaps and keeps the seven-block entrance aisle and bay-front aisle clear. These are design checks, not a collision test of a constructed building.
|
||||
|
||||
Sources are `.runtime/foundations-stage02/foundations-final.geometry.json` and the saved `zone01-complete.json` surface export; its capture time is recorded in `layout.json`. That export is not an atomic or fresh live scan. It shows only deck, parapet and lamp heights in this footprint. Before building, inspect current voxels again, preserve manual changes, save a backup and implement with conflict-aware batches. Zone 01's existing containment remains in place until a separate, checked opening of the station route.
|
||||
|
||||
For a design rebuild in the main repository, run `python3 docs/references/zone02-interior-v1/draw-plan.py` with Pillow available and the source geometry/export retained. Roof overhangs, arches, window bays and decorative block palettes remain to be detailed during the building pass within the selected region. No additional minigame rooms or playable arenas are included in this stage.
|
||||
@@ -0,0 +1,10 @@
|
||||
0aa7b7be08876532e84c3567ab1c4ee1fc6a55f8232e6ad47398f7de43505653 00-coordinate-plan.pdf
|
||||
9b549bd047b54521e6d2edaa3d09bd7c128cb1d31fd1e170b16d0a9b00d93dc3 00-coordinate-plan.png
|
||||
5ba4142fa4e0cbcc21770e438ca963762c157e242598908c9326f0479b3c3d86 01-vestibule.png
|
||||
c23c425ffefa5626e56ab83548e1b2c3f8ed38ab7f686d2d00da93e15d86d41b 02-smash-floor.png
|
||||
cd8232d0c27ea25b60510128ebdca629b8872288e9ee95db6fa919c34ae0273f 03-cutaway.png
|
||||
37702b2c8337cba6d301d37c7d88151ad930fb6c474954733cfe0771a820a75a README.md
|
||||
1c90cf98558c079ba3ef3ebd3cd2d8dee17cecc01535875a9ab1f23a99577d03 draw-plan.py
|
||||
6341286d9f66171788fc06dfa8368d0ce9cc195de398963465a87337dff5c091 generation-sources.json
|
||||
135b16fcaaab87030cd78bed3129a0edfeb2aaa8de519245e1746879e45d6b83 layout.json
|
||||
a361e3016e38e95d8dbc865c70d8d256f48f085ac8f1f37c866ec403b445d605 prompts.json
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Render the proposed station layout against the saved, block-exact foundation.
|
||||
|
||||
Read-only design utility; it never connects to or writes the Minecraft world.
|
||||
All X/Z boxes are inclusive block coordinates. Pillow is required.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
ROOT = OUT.parents[2]
|
||||
SOURCE = ROOT / '.runtime/foundations-stage02/foundations-final.geometry.json'
|
||||
geometry = json.loads(SOURCE.read_text())
|
||||
foot = {(c['x'], c['z']) for c in geometry['cells'] if c['group'] == 'clock-station'}
|
||||
inner = {p for p in foot if all((p[0]+dx,p[1]+dz) in foot for dx in range(-2,3) for dz in range(-2,3))}
|
||||
walls = foot-inner
|
||||
|
||||
def cells(box):
|
||||
a,b,c,d = box
|
||||
return {(x,z) for x in range(a,b+1) for z in range(c,d+1)}
|
||||
|
||||
core = [-10,-2,-124,-116]
|
||||
cabin = [-8,-4,-122,-118]
|
||||
bays = [[x-3,x+3,-140,-136] for x in [-42,-32,-22,-12,-2,8]]
|
||||
display = [-42,-24,-124,-110]
|
||||
pillars = [[x,x+1,z,z+1] for x in [-69,-49,-19,17,31] for z in [-127,-104]]
|
||||
benches = [[x,x+7,z,z+1] for x in [-67,24] for z in [-132,-108]]
|
||||
features = {'lift housing':core,'lift cabin':cabin,'display':display}
|
||||
features.update({f'arena bay {i+1}':b for i,b in enumerate(bays)})
|
||||
features.update({f'column {i+1}':b for i,b in enumerate(pillars)})
|
||||
features.update({f'bench {i+1}':b for i,b in enumerate(benches)})
|
||||
for name,box in features.items():
|
||||
assert cells(box) <= inner, (name,'outside two-block perimeter setback')
|
||||
solid_features = [core,display,*bays,*pillars,*benches]
|
||||
for i,a in enumerate(solid_features):
|
||||
for b in solid_features[i+1:]:
|
||||
assert not cells(a)&cells(b), ('furniture overlap',a,b)
|
||||
route = cells([-9,-3,-115,-85])
|
||||
assert route <= foot
|
||||
assert not route & set().union(*(cells(b) for b in solid_features))
|
||||
bay_aisle = cells([-46,11,-135,-129])
|
||||
assert bay_aisle <= inner
|
||||
assert not bay_aisle & set().union(*(cells(b) for b in solid_features))
|
||||
map_path = ROOT / '.runtime/server/plugins/ShacraftTerrain/maps/zone01-complete.json'
|
||||
surface=json.loads(map_path.read_text())
|
||||
hist=Counter(surface['surface_y'][(z-surface['min_z'])*surface['width']+x-surface['min_x']] for x,z in foot)
|
||||
design = {
|
||||
'status':'PROPOSAL ONLY. Foundation is surveyed; walls, interiors and upper levels are not built.',
|
||||
'world':'shacraft_lobby_v2','coordinate_convention':'X/Z boxes include both endpoints. Floor Y is block Y; walk Y is top of that floor.',
|
||||
'foundation_source':str(SOURCE.relative_to(ROOT)), 'surface_capture_finished_at':surface['capture_finished_at'],
|
||||
'foundation_columns':len(foot),'foundation_bounds':[-74,40,-145,-83], 'foundation_surface_height_counts':dict(hist),
|
||||
'floors':[{'name':'1 Vestibule','floor_block_y':98,'walk_y':99,'ceiling_underside_y':111,'clear_height':12},
|
||||
{'name':'2 Smash','floor_block_y':112,'walk_y':113,'ceiling_underside_y':124,'clear_height':11}],
|
||||
'intermediate_deck_block_y':[111,112], 'perimeter_wall_thickness':2,
|
||||
'main_roof_eaves_y':126,'main_roof_ridge_y':140,'clocktower_peak_y':158,
|
||||
'entrance_axis_x':-6,'entrance_opening_x':[-9,-3],'entrance_threshold_z':[-84,-83],
|
||||
'lift':{'housing':core,'clear_cabin':cabin,'door_clear_x':[-8,-4],'door_z':[-117,-116],'opens':'south','same_location_on_both_floors':True},
|
||||
'smash_display':display,'smash_selection_bays':bays,'aligned_columns':pillars,'wing_benches':benches,
|
||||
'reserved_clear_aisles':{'entry':[-9,-3,-115,-85],'bay_front':[-46,11,-135,-129]},
|
||||
'checks':{'features_inside_two_block_setback':True,'solid_features_do_not_overlap':True,'entry_route_clear_width':7,'bay_front_clear_depth':7},
|
||||
'limitations':['Saved surface export is not a fresh live scan.', 'Containment and overlap checks cover the proposed plan, not constructed voxels.',
|
||||
'Generated perspectives are artistic references; this coordinate specification governs placement.', 'Arena sign slots are unassigned, not working destinations.',
|
||||
'Future floors need an explicit roof/height design before construction. Only the lift alignment is reserved.']}
|
||||
(OUT/'layout.json').write_text(json.dumps(design,indent=2)+'\n')
|
||||
|
||||
W,H=2000,1530
|
||||
im=Image.new('RGB',(W,H),'#f5f1e7'); d=ImageDraw.Draw(im)
|
||||
REG='/usr/share/fonts/truetype/ibm-plex/IBMPlexSans-Regular.ttf'
|
||||
BOLD='/usr/share/fonts/truetype/ibm-plex/IBMPlexSans-SemiBold.ttf'
|
||||
SERIF='/usr/share/fonts/truetype/ibm-plex/IBMPlexSerif-Regular.ttf'
|
||||
def text(x,y,s,size=22,color='#203e36',bold=False,anchor=None):
|
||||
d.text((x,y),s,font=ImageFont.truetype(BOLD if bold else REG,size),fill=color,anchor=anchor)
|
||||
text(65,35,'SHACRAFT / CLOCK STATION',44,bold=True)
|
||||
text(65,93,'01 VESTIBULE + 02 SMASH / COORDINATE PLAN / DESIGN v1',22)
|
||||
text(65,133,'Fixed foundation: 115 x 63 blocks overall. North is up. Both plans use the same 1-block grid.',21)
|
||||
green='#376956';orange='#bd633b';wall='#58635c';floor='#e8dfcc';gold='#d0aa54'
|
||||
|
||||
def plan(ox,title,upper=False):
|
||||
oy=255; sc=7
|
||||
def xy(x,z): return ox+(x+74)*sc,oy+(z+145)*sc
|
||||
def box(b,fill,outline=None):
|
||||
a,c=xy(b[0],b[2]);bb,dd=xy(b[1]+1,b[3]+1)
|
||||
d.rectangle((a,c,bb-1,dd-1),fill=fill,outline=outline)
|
||||
def label(x,z,s,size=17,col='#203e36'):
|
||||
a,b=xy(x,z);text(a,b,s,size,col,anchor='mm')
|
||||
text(ox,196,title,28,bold=True)
|
||||
for x,z in foot: box([x,x,z,z], wall if (x,z) in walls else floor)
|
||||
# Discrete occupied columns avoid polygon boundary ambiguity.
|
||||
for x,z in inner:
|
||||
a,b=xy(x,z)
|
||||
d.line((a,b,a+sc,b),fill='#ddd5c4')
|
||||
d.line((a,b,a,b+sc),fill='#ddd5c4')
|
||||
for b in pillars: box(b,'#7b8177')
|
||||
for b in benches: box(b,'#957753')
|
||||
box(core,green);box(cabin,'#cbdcd0')
|
||||
box([-8,-4,-117,-116],'#cbdcd0')
|
||||
label(-5.5,-120,'L',21)
|
||||
if upper:
|
||||
box(display,orange)
|
||||
label(-32.5,-118,'DISPLAY',16,'#ffffff')
|
||||
label(-32.5,-114,'19 x 15',16,'#ffffff')
|
||||
for i,b in enumerate(bays):
|
||||
box(b,gold);label((b[0]+b[1]+1)/2,-137.5,f'{i+1:02}',16)
|
||||
label(-17,-132,'7-BLOCK CLEAR AISLE',17)
|
||||
label(-60,-118,'RULES',17)
|
||||
label(28,-118,'WAIT',17)
|
||||
label(-5,-94,'SOUTH GALLERY',17)
|
||||
else:
|
||||
label(-58,-118,'WAITING',17)
|
||||
label(28,-118,'WAITING',17)
|
||||
label(-34,-117,'VESTIBULE',19)
|
||||
box([1,7,-113,-111],gold)
|
||||
label(4,-107,'DIRECTORY',15)
|
||||
# South doorway through both perimeter wall blocks.
|
||||
box([-9,-3,-84,-83],floor)
|
||||
a,b=xy(-5.5,-88);c,e=xy(-5.5,-111)
|
||||
d.line((a,b,c,e),fill=green,width=4)
|
||||
d.polygon([(c,e),(c-8,e+14),(c+8,e+14)],fill=green)
|
||||
label(-5.5,-97,'7 wide',15)
|
||||
# Outside dimensions and orientation.
|
||||
text(ox+402,oy-21,'115 blocks',19,anchor='mm')
|
||||
d.line((ox,oy-8,ox+805,oy-8),fill=wall,width=2)
|
||||
for x in [ox,ox+805]: d.line((x,oy-14,x,oy-2),fill=wall,width=2)
|
||||
text(ox+842,oy+213,'63',21,anchor='mm');text(ox+842,oy+240,'blocks',16,anchor='mm')
|
||||
d.line((ox+817,oy,ox+817,oy+441),fill=wall,width=2)
|
||||
for z in [-145,-125,-105,-83]:
|
||||
a,b=xy(-74,z);text(a-12,b,str(z),15,anchor='rm')
|
||||
for x in [-74,-50,-25,0,40]:
|
||||
a,b=xy(x,-82);text(a,b+18,str(x),16,anchor='mm')
|
||||
text(ox+23,oy+398,'N',23,bold=True)
|
||||
d.line((ox+31,oy+397,ox+31,oy+358),fill=green,width=3)
|
||||
d.polygon([(ox+31,oy+352),(ox+23,oy+366),(ox+39,oy+366)],fill=green)
|
||||
text(ox,oy+491,'X: east-west / Z: north-south / unit: one block',18)
|
||||
|
||||
plan(105,'01 / VESTIBULE • WALK Y99')
|
||||
plan(1100,'02 / SMASH • WALK Y113',True)
|
||||
d.line((65,792,1935,792),fill='#bdb8aa',width=2)
|
||||
text(65,825,'SECTION / ENTRANCE AXIS X = -6',27,bold=True)
|
||||
text(1060,825,'DESIGN RULES',27,bold=True)
|
||||
# North at left, south at right; Y shown as world surface elevations.
|
||||
sx,sy,scale=105,1200,11
|
||||
def section_box(z0,z1,y0,y1,fill):
|
||||
d.rectangle((sx+(z0+145)*scale,sy-(y1-98)*scale,sx+(z1+145)*scale-1,sy-(y0-98)*scale-1),fill=fill)
|
||||
section_box(-145,-82,98,99,wall)
|
||||
section_box(-145,-82,111,113,wall)
|
||||
section_box(-145,-82,124,126,wall)
|
||||
section_box(-145,-143,99,124,wall)
|
||||
section_box(-84,-82,99,124,wall)
|
||||
section_box(-84,-82,99,105,'#f5f1e7')
|
||||
for y in [99,113]:
|
||||
top=111 if y==99 else 124
|
||||
section_box(-124,-122,y,top,green)
|
||||
section_box(-118,-116,y+6,top,green)
|
||||
section_box(-122,-118,y,y+0.25,gold)
|
||||
for y,lab in [(99,'Y99 / level 1'),(111,'Y111 / ceiling'),(113,'Y113 / level 2'),(124,'Y124 / ceiling')]:
|
||||
yy=sy-(y-98)*scale
|
||||
d.line((sx-12,yy,sx+720,yy),fill='#b4afa2',width=1)
|
||||
text(sx+735,yy,lab,18,anchor='lm')
|
||||
text(455,1072,'12 clear',23,anchor='mm')
|
||||
text(455,924,'11 clear',23,anchor='mm')
|
||||
text(105,1234,'NORTH',18);text(735,1234,'SOUTH / ENTRY',18)
|
||||
notes=[
|
||||
'Fixed footprint; 2-block perimeter walls.',
|
||||
'Lift: 9 x 9 housing; 5 x 5 clear cabin.',
|
||||
'Same shaft and south-facing door on both floors.',
|
||||
'Six 7 x 5 sign bays; destinations remain unassigned.',
|
||||
'SMASH display is scenery on a solid, enclosed floor.',
|
||||
'Gameplay takes place in separate arena worlds.',
|
||||
'Future stories are not furnished or dimensioned here.',
|
||||
'Concept images illustrate style; layout.json governs placement.'
|
||||
]
|
||||
for i,s in enumerate(notes): text(1060,878+i*43,s,21)
|
||||
text(65,1320,'VERIFIED AGAINST THE SAVED FOUNDATION',22,bold=True)
|
||||
text(65,1358,f'{len(foot):,} foundation columns • all specified furnishings inside the wall setback • no furnishing overlaps',21)
|
||||
text(65,1394,'This is a design drawing, not a live scan or a completed building. Above-ground shell and floors are proposed.',21)
|
||||
text(65,1452,'SOURCE: foundations-final.geometry.json + zone01-complete surface export / 13 SEP 2026',18)
|
||||
im.save(OUT/'00-coordinate-plan.png')
|
||||
im.save(OUT/'00-coordinate-plan.pdf',resolution=150)
|
||||
print(json.dumps({'saved':str(OUT),'foundation_columns':len(foot),'height_counts':dict(hist),'checks':'passed'}))
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"01-vestibule": "/home/emil/.codex/generated_images/01a096c0-89c4-73b3-b5c7-69c847aebebf/exec-20fd3d79-6c5c-47f8-b312-d0a557f7ca01.png",
|
||||
"02-smash-floor": "/home/emil/.codex/generated_images/01a096c0-89c4-73b3-b5c7-69c847aebebf/exec-65601ecd-0dd8-46fd-bb8c-7a5a1793cc52.png",
|
||||
"03-cutaway": "/home/emil/.codex/generated_images/01a096c0-89c4-73b3-b5c7-69c847aebebf/exec-875ff00a-540a-41e2-bf0c-b26fac132054.png"
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"status": "PROPOSAL ONLY. Foundation is surveyed; walls, interiors and upper levels are not built.",
|
||||
"world": "shacraft_lobby_v2",
|
||||
"coordinate_convention": "X/Z boxes include both endpoints. Floor Y is block Y; walk Y is top of that floor.",
|
||||
"foundation_source": ".runtime/foundations-stage02/foundations-final.geometry.json",
|
||||
"surface_capture_finished_at": "2026-09-13T11:16:27.647422004Z",
|
||||
"foundation_columns": 5764,
|
||||
"foundation_bounds": [
|
||||
-74,
|
||||
40,
|
||||
-145,
|
||||
-83
|
||||
],
|
||||
"foundation_surface_height_counts": {
|
||||
"98": 5500,
|
||||
"99": 252,
|
||||
"102": 12
|
||||
},
|
||||
"floors": [
|
||||
{
|
||||
"name": "1 Vestibule",
|
||||
"floor_block_y": 98,
|
||||
"walk_y": 99,
|
||||
"ceiling_underside_y": 111,
|
||||
"clear_height": 12
|
||||
},
|
||||
{
|
||||
"name": "2 Smash",
|
||||
"floor_block_y": 112,
|
||||
"walk_y": 113,
|
||||
"ceiling_underside_y": 124,
|
||||
"clear_height": 11
|
||||
}
|
||||
],
|
||||
"intermediate_deck_block_y": [
|
||||
111,
|
||||
112
|
||||
],
|
||||
"perimeter_wall_thickness": 2,
|
||||
"main_roof_eaves_y": 126,
|
||||
"main_roof_ridge_y": 140,
|
||||
"clocktower_peak_y": 158,
|
||||
"entrance_axis_x": -6,
|
||||
"entrance_opening_x": [
|
||||
-9,
|
||||
-3
|
||||
],
|
||||
"entrance_threshold_z": [
|
||||
-84,
|
||||
-83
|
||||
],
|
||||
"lift": {
|
||||
"housing": [
|
||||
-10,
|
||||
-2,
|
||||
-124,
|
||||
-116
|
||||
],
|
||||
"clear_cabin": [
|
||||
-8,
|
||||
-4,
|
||||
-122,
|
||||
-118
|
||||
],
|
||||
"door_clear_x": [
|
||||
-8,
|
||||
-4
|
||||
],
|
||||
"door_z": [
|
||||
-117,
|
||||
-116
|
||||
],
|
||||
"opens": "south",
|
||||
"same_location_on_both_floors": true
|
||||
},
|
||||
"smash_display": [
|
||||
-42,
|
||||
-24,
|
||||
-124,
|
||||
-110
|
||||
],
|
||||
"smash_selection_bays": [
|
||||
[
|
||||
-45,
|
||||
-39,
|
||||
-140,
|
||||
-136
|
||||
],
|
||||
[
|
||||
-35,
|
||||
-29,
|
||||
-140,
|
||||
-136
|
||||
],
|
||||
[
|
||||
-25,
|
||||
-19,
|
||||
-140,
|
||||
-136
|
||||
],
|
||||
[
|
||||
-15,
|
||||
-9,
|
||||
-140,
|
||||
-136
|
||||
],
|
||||
[
|
||||
-5,
|
||||
1,
|
||||
-140,
|
||||
-136
|
||||
],
|
||||
[
|
||||
5,
|
||||
11,
|
||||
-140,
|
||||
-136
|
||||
]
|
||||
],
|
||||
"aligned_columns": [
|
||||
[
|
||||
-69,
|
||||
-68,
|
||||
-127,
|
||||
-126
|
||||
],
|
||||
[
|
||||
-69,
|
||||
-68,
|
||||
-104,
|
||||
-103
|
||||
],
|
||||
[
|
||||
-49,
|
||||
-48,
|
||||
-127,
|
||||
-126
|
||||
],
|
||||
[
|
||||
-49,
|
||||
-48,
|
||||
-104,
|
||||
-103
|
||||
],
|
||||
[
|
||||
-19,
|
||||
-18,
|
||||
-127,
|
||||
-126
|
||||
],
|
||||
[
|
||||
-19,
|
||||
-18,
|
||||
-104,
|
||||
-103
|
||||
],
|
||||
[
|
||||
17,
|
||||
18,
|
||||
-127,
|
||||
-126
|
||||
],
|
||||
[
|
||||
17,
|
||||
18,
|
||||
-104,
|
||||
-103
|
||||
],
|
||||
[
|
||||
31,
|
||||
32,
|
||||
-127,
|
||||
-126
|
||||
],
|
||||
[
|
||||
31,
|
||||
32,
|
||||
-104,
|
||||
-103
|
||||
]
|
||||
],
|
||||
"wing_benches": [
|
||||
[
|
||||
-67,
|
||||
-60,
|
||||
-132,
|
||||
-131
|
||||
],
|
||||
[
|
||||
-67,
|
||||
-60,
|
||||
-108,
|
||||
-107
|
||||
],
|
||||
[
|
||||
24,
|
||||
31,
|
||||
-132,
|
||||
-131
|
||||
],
|
||||
[
|
||||
24,
|
||||
31,
|
||||
-108,
|
||||
-107
|
||||
]
|
||||
],
|
||||
"reserved_clear_aisles": {
|
||||
"entry": [
|
||||
-9,
|
||||
-3,
|
||||
-115,
|
||||
-85
|
||||
],
|
||||
"bay_front": [
|
||||
-46,
|
||||
11,
|
||||
-135,
|
||||
-129
|
||||
]
|
||||
},
|
||||
"checks": {
|
||||
"features_inside_two_block_setback": true,
|
||||
"solid_features_do_not_overlap": true,
|
||||
"entry_route_clear_width": 7,
|
||||
"bay_front_clear_depth": 7
|
||||
},
|
||||
"limitations": [
|
||||
"Saved surface export is not a fresh live scan.",
|
||||
"Containment and overlap checks cover the proposed plan, not constructed voxels.",
|
||||
"Generated perspectives are artistic references; this coordinate specification governs placement.",
|
||||
"Arena sign slots are unassigned, not working destinations.",
|
||||
"Future floors need an explicit roof/height design before construction. Only the lift alignment is reserved."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mode": "built-in image_gen",
|
||||
"prompts": {
|
||||
"01-vestibule": "Use case: stylized-concept.\nAsset type: architectural Minecraft building reference for SHACRAFT, design proposal, not a game screenshot.\nProject: Alpine clock station / minigame selection hub on an already built irregular foundation. All geometry must look buildable using Minecraft blocks, stairs, slabs, fences and panes. Pale sandstone and cream masonry, dark stone plinths, spruce, oxidized green copper, muted brass, warm lanterns, forest-green SHACRAFT banners with a simple gold S. Restrained excellent craftsmanship, architectural clarity, no visual clutter. No sci-fi, no neon holograms, no modern shopping mall. Tiny Minecraft players only as 2-block height scale figures.\nHard spatial brief: foundation east-west 115 blocks overall and north-south 63 blocks maximum. Broad main hall 68 blocks wide by 48 deep, west pavilion 23 wide and east pavilion24 wide, both42 deep. A south entrance projection39 wide by16 deep is offset 11 blocks east of the whole building center. Entrance faces south and has a21-block-wide external staircase rising3 blocks. First floor feetY99, next floor feetY113; a two-block deck atY111..112 gives12 blocks of first-floor clear height. Second floor has11 blocks clear height, next ceiling beginsY124. Lift housing9x9 blocks with usable cabin5x5, on entrance axis x=-6, z=-120, opens south on BOTH floors. Interior rooms have2-block-thick perimeter walls and believable aligned masonry columns. Main ridgeY140; clocktower peakY158. Only two floors are furnished. Future floors remain a design reservation, no extra themed rooms.\n\nPrimary request: Draw ONE large, beautiful interior architectural perspective of the FIRST-FLOOR VESTIBULE, landscape16:9. Camera from south entrance facing north, eye height about3 blocks above floor, moderate wide-angle with straight credible verticals. In the center background, clearly visible forest-green/brass lift doorway9 blocks wide with5-block-wide opening, housed in a square structural core. A long7-block-clear cream stone aisle goes directly from entrance to lift. Immediately beside lift a small readable directory says only \"1 LOBBY\" and \"2 SMASH\". Show a substantial ceiling12 blocks above floor, ribbed masonry arches and deep spruce ceiling panels, no giant open roof or infinite atrium. Side aisles lead left and right into quieter waiting galleries with spruce benches, planters in stone urns and warm lamps. Floor has a restrained forest-green geometric compass mosaic OFF the straight route; do not block path with statue or fountain. Foreground side corners frame pale stone door jambs. Warm daylight through tall side windows. Keep actual usable scale of main hall, the view shows a section of its width rather than a falsely narrow corridor. Small centered title in a slim cream margin: \"SHACRAFT / 01 VESTIBULE\". Beautiful plausible Minecraft block design, not a real-world cathedral.",
|
||||
"02-smash-floor": "Use case: stylized-concept.\nAsset type: architectural Minecraft building reference for SHACRAFT, design proposal, not a game screenshot.\nProject: Alpine clock station / minigame selection hub on an already built irregular foundation. All geometry must look buildable using Minecraft blocks, stairs, slabs, fences and panes. Pale sandstone and cream masonry, dark stone plinths, spruce, oxidized green copper, muted brass, warm lanterns, forest-green SHACRAFT banners with a simple gold S. Restrained excellent craftsmanship, architectural clarity, no visual clutter. No sci-fi, no neon holograms, no modern shopping mall. Tiny Minecraft players only as 2-block height scale figures.\nHard spatial brief: foundation east-west 115 blocks overall and north-south 63 blocks maximum. Broad main hall 68 blocks wide by 48 deep, west pavilion 23 wide and east pavilion24 wide, both42 deep. A south entrance projection39 wide by16 deep is offset 11 blocks east of the whole building center. Entrance faces south and has a21-block-wide external staircase rising3 blocks. First floor feetY99, next floor feetY113; a two-block deck atY111..112 gives12 blocks of first-floor clear height. Second floor has11 blocks clear height, next ceiling beginsY124. Lift housing9x9 blocks with usable cabin5x5, on entrance axis x=-6, z=-120, opens south on BOTH floors. Interior rooms have2-block-thick perimeter walls and believable aligned masonry columns. Main ridgeY140; clocktower peakY158. Only two floors are furnished. Future floors remain a design reservation, no extra themed rooms.\n\nPrimary request: Draw ONE large, beautiful interior architectural perspective of the SECOND-FLOOR SMASH SELECTION HALL, landscape16:9. Camera near south-east part of main hall looking north-west. Include the green/brass9x9 lift core on right, its opening on its south face visible obliquely, with clear sign \"1 LOBBY\". Left foreground is a19x15-block enclosed decorative display on an intact solid floor: three small voxel floating rock islands mounted over a dark inset plinth, two static small Minecraft figures, one knocked away on a restrained gold block motion arc. This is a MUSEUM DIORAMA with low glass-and-stone protective railing, not an actual combat arena, no lethal pit, no broken public floor. A seven-block-clear aisle passes around display.\nAlong north wall in background EXACTLY SIX recessed selection bays, each7 blocks wide, separated by3 blocks; they have spruce Minecraft signboards at player reach height with a small thumbnail map plaque above and numbered headings \"01\", \"02\", \"03\", \"04\", \"05\", \"06\". These are placeholders for future arenas, no invented live player counts or glowing portals. A large tasteful block-built \"SMASH\" header above back bays. West wall small visual plaques illustrate damage, knockback, double jump using voxel pictograms. SMASH accent colors burnt orange and dark red banners, balanced with Shacraft green. Keep the same cream stone columns, spruce, green copper and brass as vestibule. Ceiling is11 blocks above floor, visible solid ceiling and rafters, not infinitely tall. Daylight, warm readable practical space. Title in slim cream margin: \"SHACRAFT / 02 SMASH\".",
|
||||
"03-cutaway": "Use case: stylized-concept.\nAsset type: architectural Minecraft building reference for SHACRAFT, design proposal, not a game screenshot.\nProject: Alpine clock station / minigame selection hub on an already built irregular foundation. All geometry must look buildable using Minecraft blocks, stairs, slabs, fences and panes. Pale sandstone and cream masonry, dark stone plinths, spruce, oxidized green copper, muted brass, warm lanterns, forest-green SHACRAFT banners with a simple gold S. Restrained excellent craftsmanship, architectural clarity, no visual clutter. No sci-fi, no neon holograms, no modern shopping mall. Tiny Minecraft players only as 2-block height scale figures.\nHard spatial brief: foundation east-west 115 blocks overall and north-south 63 blocks maximum. Broad main hall 68 blocks wide by 48 deep, west pavilion 23 wide and east pavilion24 wide, both42 deep. A south entrance projection39 wide by16 deep is offset 11 blocks east of the whole building center. Entrance faces south and has a21-block-wide external staircase rising3 blocks. First floor feetY99, next floor feetY113; a two-block deck atY111..112 gives12 blocks of first-floor clear height. Second floor has11 blocks clear height, next ceiling beginsY124. Lift housing9x9 blocks with usable cabin5x5, on entrance axis x=-6, z=-120, opens south on BOTH floors. Interior rooms have2-block-thick perimeter walls and believable aligned masonry columns. Main ridgeY140; clocktower peakY158. Only two floors are furnished. Future floors remain a design reservation, no extra themed rooms.\n\nPrimary request: Create ONE polished landscape architectural cutaway axonometric reference sheet, wide3:2 format, ivory background, thin dark green labels. A dominant three-quarter view looking from south-east shows the WHOLE clock station, broad horizontal wings, original grand green copper roofs, prominent slightly off-center front clock tower. Remove front and right portions of wall and selected roof to reveal EXACTLY TWO furnished stacked floors without concealing their connection. Ground floor is a cream stone vestibule with central arrival aisle, benches in wings, square green/brass lift core at rear of arrival aisle. Upper floor directly above has same aligned core, a small contained floating-island display in left half, six signboard alcoves along rear wall. Floor slab between them clearly shown, consistent14-block floor-to-floor spacing. Remaining roofs are translucent linework only where needed. Upper clock tower contains clockwork silhouette, not extra furnished game rooms. Foundation shape must remain broad main hall plus end pavilions, shallow rear mainhall projection and south projecting tower entrance, not a rectangular skyscraper. No extra buildings or landscape.\nAdd three neat callouts only: \"01 VESTIBULE / WALK Y99\", \"02 SMASH / WALK Y113\", \"LIFT / 5 x 5 CLEAR\". Add small title \"SHACRAFT / CLOCK STATION\" and subtitle \"Two-floor interior concept\". Add note at bottom \"Concept view - coordinate plan governs dimensions\". Strong visually legible separation of floors, genuine Minecraft voxel scale, warm copper and stone, refined presentation, no dense fake dimensions."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"min": {"x": -4, "y": 80, "z": -4},
|
||||
"max": {"x": 4, "y": 92, "z": 4},
|
||||
"center": {"x": 0, "z": 0},
|
||||
"radius": 3,
|
||||
"action": "raise",
|
||||
"amount": 3,
|
||||
"strength": 1,
|
||||
"falloff": 0.5,
|
||||
"preserve": []
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"version": 1,
|
||||
"min": {
|
||||
"x": -384,
|
||||
"y": 16,
|
||||
"z": -384
|
||||
},
|
||||
"max": {
|
||||
"x": 383,
|
||||
"y": 207,
|
||||
"z": 383
|
||||
},
|
||||
"base_height": 59,
|
||||
"seed": 28092005,
|
||||
"mode": "sculpt",
|
||||
"noise": {
|
||||
"amplitude": 26,
|
||||
"scale": 90
|
||||
},
|
||||
"palette": {
|
||||
"rock": "minecraft:stone",
|
||||
"soil": "minecraft:dirt",
|
||||
"surface": "minecraft:grass_block",
|
||||
"soil_depth": 3
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": -350,
|
||||
"z": -285
|
||||
},
|
||||
{
|
||||
"x": -190,
|
||||
"z": -335
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"z": -305
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"z": -330
|
||||
},
|
||||
{
|
||||
"x": 330,
|
||||
"z": -290
|
||||
}
|
||||
],
|
||||
"width": 15,
|
||||
"height": 100,
|
||||
"falloff": 110
|
||||
},
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": -330,
|
||||
"z": -220
|
||||
},
|
||||
{
|
||||
"x": -350,
|
||||
"z": -40
|
||||
},
|
||||
{
|
||||
"x": -330,
|
||||
"z": 180
|
||||
},
|
||||
{
|
||||
"x": -290,
|
||||
"z": 310
|
||||
}
|
||||
],
|
||||
"width": 8,
|
||||
"height": 60,
|
||||
"falloff": 70
|
||||
},
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": 335,
|
||||
"z": -210
|
||||
},
|
||||
{
|
||||
"x": 340,
|
||||
"z": 30
|
||||
},
|
||||
{
|
||||
"x": 315,
|
||||
"z": 210
|
||||
}
|
||||
],
|
||||
"width": 10,
|
||||
"height": 66,
|
||||
"falloff": 80
|
||||
},
|
||||
{
|
||||
"type": "hill",
|
||||
"center": {
|
||||
"x": 0,
|
||||
"z": 0
|
||||
},
|
||||
"radius": 50,
|
||||
"height": 45,
|
||||
"falloff": 140
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -65,
|
||||
"z": -55
|
||||
},
|
||||
"max": {
|
||||
"x": 65,
|
||||
"z": 60
|
||||
},
|
||||
"height": 106,
|
||||
"falloff": 28
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -82,
|
||||
"z": -165
|
||||
},
|
||||
"max": {
|
||||
"x": 82,
|
||||
"z": -80
|
||||
},
|
||||
"height": 118,
|
||||
"falloff": 24
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -245,
|
||||
"z": -185
|
||||
},
|
||||
"max": {
|
||||
"x": -125,
|
||||
"z": -105
|
||||
},
|
||||
"height": 110,
|
||||
"falloff": 22
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 130,
|
||||
"z": -200
|
||||
},
|
||||
"max": {
|
||||
"x": 255,
|
||||
"z": -100
|
||||
},
|
||||
"height": 124,
|
||||
"falloff": 20
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 135,
|
||||
"z": -60
|
||||
},
|
||||
"max": {
|
||||
"x": 250,
|
||||
"z": 5
|
||||
},
|
||||
"height": 110,
|
||||
"falloff": 15
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 145,
|
||||
"z": 40
|
||||
},
|
||||
"max": {
|
||||
"x": 250,
|
||||
"z": 95
|
||||
},
|
||||
"height": 97,
|
||||
"falloff": 15
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 160,
|
||||
"z": 130
|
||||
},
|
||||
"max": {
|
||||
"x": 235,
|
||||
"z": 180
|
||||
},
|
||||
"height": 84,
|
||||
"falloff": 14
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -240,
|
||||
"z": 90
|
||||
},
|
||||
"max": {
|
||||
"x": -115,
|
||||
"z": 215
|
||||
},
|
||||
"height": 92,
|
||||
"falloff": 25
|
||||
},
|
||||
{
|
||||
"type": "basin",
|
||||
"center": {
|
||||
"x": -220,
|
||||
"z": -5
|
||||
},
|
||||
"radius": 47,
|
||||
"height": 38,
|
||||
"falloff": 30
|
||||
},
|
||||
{
|
||||
"type": "channel",
|
||||
"points": [
|
||||
{
|
||||
"x": -40,
|
||||
"z": 125
|
||||
},
|
||||
{
|
||||
"x": -25,
|
||||
"z": 200
|
||||
},
|
||||
{
|
||||
"x": 45,
|
||||
"z": 260
|
||||
},
|
||||
{
|
||||
"x": 15,
|
||||
"z": 383
|
||||
}
|
||||
],
|
||||
"width": 20,
|
||||
"height": 22,
|
||||
"falloff": 16
|
||||
},
|
||||
{
|
||||
"type": "channel",
|
||||
"points": [
|
||||
{
|
||||
"x": 100,
|
||||
"z": -250
|
||||
},
|
||||
{
|
||||
"x": 110,
|
||||
"z": -100
|
||||
},
|
||||
{
|
||||
"x": 100,
|
||||
"z": 70
|
||||
},
|
||||
{
|
||||
"x": 80,
|
||||
"z": 190
|
||||
}
|
||||
],
|
||||
"width": 10,
|
||||
"height": 30,
|
||||
"falloff": 12
|
||||
}
|
||||
],
|
||||
"preserve": []
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"version": 1,
|
||||
"min": {
|
||||
"x": -384,
|
||||
"y": -48,
|
||||
"z": -384
|
||||
},
|
||||
"max": {
|
||||
"x": 383,
|
||||
"y": 143,
|
||||
"z": 383
|
||||
},
|
||||
"base_height": -5,
|
||||
"seed": 28092005,
|
||||
"mode": "sculpt",
|
||||
"noise": {
|
||||
"amplitude": 26,
|
||||
"scale": 90
|
||||
},
|
||||
"palette": {
|
||||
"rock": "minecraft:stone",
|
||||
"soil": "minecraft:dirt",
|
||||
"surface": "minecraft:grass_block",
|
||||
"soil_depth": 3
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": -350,
|
||||
"z": -285
|
||||
},
|
||||
{
|
||||
"x": -190,
|
||||
"z": -335
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"z": -305
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
"z": -330
|
||||
},
|
||||
{
|
||||
"x": 330,
|
||||
"z": -290
|
||||
}
|
||||
],
|
||||
"width": 15,
|
||||
"height": 100,
|
||||
"falloff": 110
|
||||
},
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": -330,
|
||||
"z": -220
|
||||
},
|
||||
{
|
||||
"x": -350,
|
||||
"z": -40
|
||||
},
|
||||
{
|
||||
"x": -330,
|
||||
"z": 180
|
||||
},
|
||||
{
|
||||
"x": -290,
|
||||
"z": 310
|
||||
}
|
||||
],
|
||||
"width": 8,
|
||||
"height": 60,
|
||||
"falloff": 70
|
||||
},
|
||||
{
|
||||
"type": "ridge",
|
||||
"points": [
|
||||
{
|
||||
"x": 335,
|
||||
"z": -210
|
||||
},
|
||||
{
|
||||
"x": 340,
|
||||
"z": 30
|
||||
},
|
||||
{
|
||||
"x": 315,
|
||||
"z": 210
|
||||
}
|
||||
],
|
||||
"width": 10,
|
||||
"height": 66,
|
||||
"falloff": 80
|
||||
},
|
||||
{
|
||||
"type": "hill",
|
||||
"center": {
|
||||
"x": 0,
|
||||
"z": 0
|
||||
},
|
||||
"radius": 50,
|
||||
"height": 45,
|
||||
"falloff": 140
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -65,
|
||||
"z": -55
|
||||
},
|
||||
"max": {
|
||||
"x": 65,
|
||||
"z": 60
|
||||
},
|
||||
"height": 42,
|
||||
"falloff": 28
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -82,
|
||||
"z": -165
|
||||
},
|
||||
"max": {
|
||||
"x": 82,
|
||||
"z": -80
|
||||
},
|
||||
"height": 54,
|
||||
"falloff": 24
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -245,
|
||||
"z": -185
|
||||
},
|
||||
"max": {
|
||||
"x": -125,
|
||||
"z": -105
|
||||
},
|
||||
"height": 46,
|
||||
"falloff": 22
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 130,
|
||||
"z": -200
|
||||
},
|
||||
"max": {
|
||||
"x": 255,
|
||||
"z": -100
|
||||
},
|
||||
"height": 60,
|
||||
"falloff": 20
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 135,
|
||||
"z": -60
|
||||
},
|
||||
"max": {
|
||||
"x": 250,
|
||||
"z": 5
|
||||
},
|
||||
"height": 46,
|
||||
"falloff": 15
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 145,
|
||||
"z": 40
|
||||
},
|
||||
"max": {
|
||||
"x": 250,
|
||||
"z": 95
|
||||
},
|
||||
"height": 33,
|
||||
"falloff": 15
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": 160,
|
||||
"z": 130
|
||||
},
|
||||
"max": {
|
||||
"x": 235,
|
||||
"z": 180
|
||||
},
|
||||
"height": 20,
|
||||
"falloff": 14
|
||||
},
|
||||
{
|
||||
"type": "plateau",
|
||||
"min": {
|
||||
"x": -240,
|
||||
"z": 90
|
||||
},
|
||||
"max": {
|
||||
"x": -115,
|
||||
"z": 215
|
||||
},
|
||||
"height": 28,
|
||||
"falloff": 25
|
||||
},
|
||||
{
|
||||
"type": "basin",
|
||||
"center": {
|
||||
"x": -220,
|
||||
"z": -5
|
||||
},
|
||||
"radius": 47,
|
||||
"height": -26,
|
||||
"falloff": 30
|
||||
},
|
||||
{
|
||||
"type": "channel",
|
||||
"points": [
|
||||
{
|
||||
"x": -40,
|
||||
"z": 125
|
||||
},
|
||||
{
|
||||
"x": -25,
|
||||
"z": 200
|
||||
},
|
||||
{
|
||||
"x": 45,
|
||||
"z": 260
|
||||
},
|
||||
{
|
||||
"x": 15,
|
||||
"z": 383
|
||||
}
|
||||
],
|
||||
"width": 20,
|
||||
"height": -42,
|
||||
"falloff": 16
|
||||
},
|
||||
{
|
||||
"type": "channel",
|
||||
"points": [
|
||||
{
|
||||
"x": 100,
|
||||
"z": -250
|
||||
},
|
||||
{
|
||||
"x": 110,
|
||||
"z": -100
|
||||
},
|
||||
{
|
||||
"x": 100,
|
||||
"z": 70
|
||||
},
|
||||
{
|
||||
"x": 80,
|
||||
"z": 190
|
||||
}
|
||||
],
|
||||
"width": 10,
|
||||
"height": -34,
|
||||
"falloff": 12
|
||||
}
|
||||
],
|
||||
"preserve": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 1,
|
||||
"min": {
|
||||
"x": -384,
|
||||
"y": 16,
|
||||
"z": -384
|
||||
},
|
||||
"max": {
|
||||
"x": 383,
|
||||
"y": 239,
|
||||
"z": 383
|
||||
},
|
||||
"base_height": 62,
|
||||
"seed": 28092005,
|
||||
"mode": "sculpt",
|
||||
"noise": {
|
||||
"amplitude": 0,
|
||||
"scale": 220
|
||||
},
|
||||
"palette": {
|
||||
"rock": "minecraft:stone",
|
||||
"soil": "minecraft:dirt",
|
||||
"surface": "minecraft:grass_block",
|
||||
"soil_depth": 3
|
||||
},
|
||||
"features": [],
|
||||
"preserve": []
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": 1,
|
||||
"min": {
|
||||
"x": 0,
|
||||
"y": 80,
|
||||
"z": 0
|
||||
},
|
||||
"max": {
|
||||
"x": 7,
|
||||
"y": 87,
|
||||
"z": 7
|
||||
},
|
||||
"base_height": 82,
|
||||
"seed": 2809,
|
||||
"mode": "sculpt",
|
||||
"noise": {
|
||||
"amplitude": 1,
|
||||
"scale": 12
|
||||
},
|
||||
"palette": {
|
||||
"rock": "minecraft:stone",
|
||||
"soil": "minecraft:dirt",
|
||||
"surface": "minecraft:grass_block",
|
||||
"soil_depth": 3
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"type": "hill",
|
||||
"center": {
|
||||
"x": 4,
|
||||
"z": 4
|
||||
},
|
||||
"radius": 1,
|
||||
"height": 3,
|
||||
"falloff": 3
|
||||
}
|
||||
],
|
||||
"preserve": []
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Private journal representation. MCP responses expose only block data and an opaque digest. */
|
||||
final class BlockSnapshots {
|
||||
private static final String PREFIX = "\u0000mcb-block-v1:";
|
||||
static final int MAX_SNAPSHOT_BYTES = 1024 * 1024;
|
||||
record Value(String state, String nbt) { }
|
||||
static boolean captured(String value) { return value != null && value.startsWith(PREFIX); }
|
||||
static String encode(String state, String nbt) {
|
||||
Objects.requireNonNull(state); Objects.requireNonNull(nbt);
|
||||
if (state.isBlank() || state.length() > 1024 || state.indexOf('\n') >= 0 || state.indexOf('\u0000') >= 0)
|
||||
throw new IllegalArgumentException("Invalid captured block data");
|
||||
String result = PREFIX + state + "\n" + nbt;
|
||||
requireBounded(result);
|
||||
return result;
|
||||
}
|
||||
static Value decode(String value) {
|
||||
Objects.requireNonNull(value);
|
||||
if (!captured(value)) return new Value(value, null);
|
||||
requireBounded(value);
|
||||
int split = value.indexOf('\n', PREFIX.length());
|
||||
if (split <= PREFIX.length() || split - PREFIX.length() > 1024 || split == value.length() - 1)
|
||||
throw new IllegalArgumentException("Invalid stored block snapshot");
|
||||
return new Value(value.substring(PREFIX.length(), split), value.substring(split + 1));
|
||||
}
|
||||
static String state(String value) { return decode(value).state(); }
|
||||
static String snapshotId(String value) {
|
||||
if (!captured(value)) return null;
|
||||
decode(value);
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
|
||||
}
|
||||
static Map<String, String> publicView(String value) {
|
||||
String id = snapshotId(value);
|
||||
return id == null ? Map.of("state", state(value)) : Map.of("state", state(value), "snapshot_id", id);
|
||||
}
|
||||
private static void requireBounded(String value) {
|
||||
if (value.length() > MAX_SNAPSHOT_BYTES || value.getBytes(StandardCharsets.UTF_8).length > MAX_SNAPSHOT_BYTES)
|
||||
throw new IllegalArgumentException("snapshot_budget_exceeded: block entity exceeds the 1 MiB snapshot limit");
|
||||
}
|
||||
private BlockSnapshots() { }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import io.github.minecraftbuilder.core.TerrainBrush;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/** Exact sampled columns, with a shared elevation scale for before/after panels. */
|
||||
public final class BrushPreview {
|
||||
private BrushPreview() { }
|
||||
public static Map<String,Object> render(TerrainBrush.Result brush) throws IOException {
|
||||
int rows=brush.before().length,cols=brush.before()[0].length;
|
||||
int low=Integer.MAX_VALUE,high=Integer.MIN_VALUE;
|
||||
for(int z=0;z<rows;z++)for(int x=0;x<cols;x++){
|
||||
low=Math.min(low,Math.min(brush.before()[z][x],brush.after()[z][x]));
|
||||
high=Math.max(high,Math.max(brush.before()[z][x],brush.after()[z][x]));
|
||||
}
|
||||
BufferedImage image=new BufferedImage(780,300,BufferedImage.TYPE_INT_RGB);Graphics2D g=image.createGraphics();
|
||||
try {
|
||||
g.setColor(new Color(0x172126));g.fillRect(0,0,780,300);g.setColor(new Color(0xECF0E9));g.setFont(new Font(Font.SANS_SERIF,Font.BOLD,16));
|
||||
String[] labels={"BEFORE","AFTER","HEIGHT CHANGE"};
|
||||
for(int panel=0;panel<3;panel++){
|
||||
int left=panel*260+10;g.drawString(labels[panel],left,24);
|
||||
double cell=Math.min(240.0/cols,240.0/rows);
|
||||
for(int z=0;z<rows;z++)for(int x=0;x<cols;x++){
|
||||
int old=brush.before()[z][x],next=brush.after()[z][x],rgb;
|
||||
if(panel==2){int delta=next-old;rgb=delta==0?0x34464A:delta>0?0x89CC70:0xE5A35B;}
|
||||
else{double t=((panel==0?old:next)-low)/(double)Math.max(1,high-low);rgb=((int)(45+160*t)<<16)|((int)(83+120*t)<<8)|(int)(65+107*t);}
|
||||
g.setColor(new Color(rgb));int x0=left+(int)(x*cell),z0=36+(int)(z*cell);
|
||||
g.fillRect(x0,z0,(int)((x+1)*cell)-(int)(x*cell),(int)((z+1)*cell)-(int)(z*cell));
|
||||
}
|
||||
g.setColor(new Color(0xECF0E9));
|
||||
}
|
||||
g.setFont(new Font(Font.SANS_SERIF,Font.PLAIN,12));g.drawString("N (-Z) up | Y "+low+" .. "+high+" | Green: raised Orange: lowered Grey: unchanged",10,289);
|
||||
}finally{g.dispose();}
|
||||
ByteArrayOutputStream out=new ByteArrayOutputStream();ImageIO.write(image,"png",out);
|
||||
return Map.ofEntries(Map.entry("status","completed"),Map.entry("kind","terrain_brush_preview"),Map.entry("source","live_snapshot"),
|
||||
Map.entry("world_edited",false),Map.entry("scan_bounds",brush.bounds()),Map.entry("changed_columns",brush.changedColumns()),
|
||||
Map.entry("raised_volume",brush.raisedBlocks()),Map.entry("lowered_volume",brush.loweredBlocks()),Map.entry("dependency_blocks",brush.dependencies().size()),
|
||||
Map.entry("height_min",low),Map.entry("height_max",high),Map.entry("mimeType","image/png"),Map.entry("imageBase64",Base64.getEncoder().encodeToString(out.toByteArray())));
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,14 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
private EditEngine engine;
|
||||
private World world;
|
||||
private BuildingWorld access;
|
||||
private MaterialCatalog materials;
|
||||
private SchematicAssets assets;
|
||||
private Region region;
|
||||
private String projectId, epoch, owner, adminToken, agentToken;
|
||||
private volatile boolean ioBusy, halted;
|
||||
private boolean writesPaused;
|
||||
private int maxBlocks;
|
||||
private final Map<String,TerrainRecipe> terrains = Collections.synchronizedMap(new LinkedHashMap<>());
|
||||
private final Map<String,JsonObject> messages = new LinkedHashMap<>();
|
||||
private final Map<String,Part> parts = new LinkedHashMap<>();
|
||||
private final Map<String,JsonObject> cameras = new LinkedHashMap<>();
|
||||
@@ -65,10 +67,11 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
region=new Region(world.getUID().toString(), configPos("region.min"), configPos("region.max"));
|
||||
maxBlocks=Math.min(4096,Math.max(1,getConfig().getInt("max-plan-blocks",4096)));
|
||||
access=new BuildingWorld(world);
|
||||
assets=new SchematicAssets(getDataFolder().toPath().resolve("schematics"));
|
||||
materials=new MaterialCatalog();
|
||||
assets=new SchematicAssets(getDataFolder().toPath().resolve("schematics"),BuilderPlugin::rotateSchematicState);
|
||||
loadMetadata();
|
||||
disk=Executors.newSingleThreadExecutor(r->{Thread t=new Thread(r,"mcb-journal");t.setDaemon(true);return t;});
|
||||
Limits limits=new Limits(maxBlocks,512,Math.min(128,Math.max(1,getConfig().getInt("slice-blocks",128))),
|
||||
Limits limits=new Limits(maxBlocks,4096,Math.min(128,Math.max(1,getConfig().getInt("slice-blocks",128))),
|
||||
Math.min(5,Math.max(1,getConfig().getInt("slice-millis",5)))*1_000_000L,600_000,32);
|
||||
engine=new EditEngine(access,access,new ContextGuard(){
|
||||
public void check(Plan plan){guard(plan);}
|
||||
@@ -157,7 +160,10 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
if(world.getNearbyEntities(box).stream().anyMatch(e->!(e instanceof Player)))throw new Fault("unsupported_entity","Export region contains entities; this prototype exports blocks only");
|
||||
Map<BlockPos,String> data=new LinkedHashMap<>();
|
||||
for(int y=area.min().y();y<=area.max().y();y++)for(int z=area.min().z();z<=area.max().z();z++)for(int x=area.min().x();x<=area.max().x();x++){
|
||||
BlockPos at=new BlockPos(x,y,z);String state=access.getBlock(at);if(!access.supports(state))throw new Fault("unsupported_block","Export contains unsupported block "+state);data.put(at,state);
|
||||
BlockPos at=new BlockPos(x,y,z);String state=access.getBlock(at);
|
||||
if(world.getBlockAt(x,y,z).getState() instanceof org.bukkit.block.TileState)
|
||||
throw new Fault("unsupported_block_entity","Schematic export cannot retain block-entity data at "+at+"; no asset was written");
|
||||
if(!access.supports(state))throw new Fault("unsupported_block","Export contains unsupported block "+state);data.put(at,state);
|
||||
}return data;
|
||||
});
|
||||
return assets.exportSnapshot(required(p,"name"),snapshot,p.has("origin")?pos(p.getAsJsonObject("origin")):pos(p.getAsJsonObject("min")),main(()->Bukkit.getUnsafe().getDataVersion()));
|
||||
@@ -168,8 +174,62 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
try{engine.persistPlan(plan.id());return planSummary(plan);}finally{main(()->{ioBusy=false;return null;});}
|
||||
}
|
||||
case "project_context": return main(this::context);
|
||||
case "material_search": return main(()->materials.search(str(p,"query",""),str(p,"kind","block"),p.has("limit")?integer(p,"limit"):null,p.has("cursor")?str(p,"cursor",""):null));
|
||||
case "material_describe": return main(()->materials.describe(required(p,"id")));
|
||||
case "region_inspect": return main(()->inspect(p));
|
||||
case "region_changes": return Map.of("status","resync_required","reason","Prototype uses fresh bounded reads; complete event delta journal is not implemented");
|
||||
case "terrain_brush_prepare": {
|
||||
TerrainBrush.Spec brush=TerrainBrush.parse(p.getAsJsonObject("brush"));
|
||||
record PreparedBrush(TerrainBrush.Result result,Plan plan) {}
|
||||
PreparedBrush prepared=main(()->{
|
||||
available();Region scan=brush.bounds();
|
||||
if(!region.contains(scan.min())||!region.contains(scan.max()))throw new Fault("out_of_bounds","Brush scan including halo exceeds project area");
|
||||
Map<BlockPos,String> snapshot=new LinkedHashMap<>();
|
||||
for(int y=scan.min().y();y<=scan.max().y();y++)for(int z=scan.min().z();z<=scan.max().z();z++)for(int x=scan.min().x();x<=scan.max().x();x++){
|
||||
BlockPos at=new BlockPos(x,y,z);snapshot.put(at,access.getBlock(at));
|
||||
}
|
||||
TerrainBrush.Result result=TerrainBrush.compile(brush,snapshot,maxBlocks);
|
||||
if(result.desired().isEmpty())return new PreparedBrush(result,null);
|
||||
Map<BlockPos,String> desired=new LinkedHashMap<>(result.desired());desired.replaceAll((at,state)->access.canonical(state));
|
||||
for(BlockPos at:desired.keySet())checkSurroundings(at,desired);
|
||||
Plan plan=engine.prepare(projectId,epoch,region,desired,result.dependencies());ioBusy=true;
|
||||
return new PreparedBrush(result,plan);
|
||||
});
|
||||
if(prepared.plan()==null){Map<String,Object> preview=new LinkedHashMap<>(BrushPreview.render(prepared.result()));preview.put("plan_state","empty");preview.put("changed_blocks",0);return preview;}
|
||||
try{
|
||||
engine.persistPlan(prepared.plan().id());Map<String,Object> preview=new LinkedHashMap<>(BrushPreview.render(prepared.result()));
|
||||
preview.putAll(planSummary(prepared.plan()));preview.put("plan_state","prepared");return preview;
|
||||
}finally{main(()->{ioBusy=false;return null;});}
|
||||
}
|
||||
case "terrain_preview": {
|
||||
TerrainRecipe terrain=new TerrainRecipe(p.getAsJsonObject("recipe"));
|
||||
Object preview=TerrainPreview.render(terrain,p.has("resolution")?integer(p,"resolution"):128,maxBlocks);
|
||||
synchronized(terrains){terrains.put(terrain.id(),terrain);while(terrains.size()>32)terrains.remove(terrains.keySet().iterator().next());}
|
||||
return preview;
|
||||
}
|
||||
case "terrain_prepare": {
|
||||
String terrainId=required(p,"terrain_id");TerrainRecipe terrain=terrains.get(terrainId);
|
||||
if(terrain==null)throw new Fault("not_found","Terrain recipe expired or server restarted; call terrain_preview again with the saved recipe");
|
||||
int tileIndex=integer(p,"tile_index");TerrainRecipe.Tile tile=terrain.tile(tileIndex,maxBlocks);
|
||||
Map<BlockPos,String> desired=new LinkedHashMap<>(tile.blocks());
|
||||
Plan plan=main(()->{
|
||||
available();
|
||||
if(!region.contains(tile.bounds().min())||!region.contains(tile.bounds().max()))throw new Fault("out_of_bounds","Terrain tile exceeds current project area");
|
||||
desired.replaceAll((at,state)->access.canonical(state));
|
||||
boolean changed=false;
|
||||
for(BlockPos at:desired.keySet()){
|
||||
String before=access.getBlock(at);changed|=!before.equals(desired.get(at));
|
||||
if(!TerrainRecipe.replaceable(before))throw new Fault("protected_terrain","Tile contains a building or non-terrain block at "+at+"; preserve it explicitly or choose another tile");
|
||||
checkSurroundings(at,desired);
|
||||
}
|
||||
if(!changed)return null;
|
||||
Plan value=engine.prepare(projectId,epoch,region,desired,Set.of());ioBusy=true;return value;
|
||||
});
|
||||
if(plan==null)return Map.of("status","empty","terrain_id",terrainId,"tile_index",tileIndex,"tile_bounds",tile.bounds(),"changed_blocks",0,"reason","No changed target blocks (unchanged or preserved)");
|
||||
try{engine.persistPlan(plan.id());Map<String,Object> summary=new LinkedHashMap<>(planSummary(plan));
|
||||
summary.put("terrain_id",terrainId);summary.put("tile_index",tileIndex);summary.put("tile_bounds",tile.bounds());return summary;
|
||||
}finally{main(()->{ioBusy=false;return null;});}
|
||||
}
|
||||
case "build_prepare": {
|
||||
JsonObject recipe=p.getAsJsonObject("recipe");
|
||||
Map<BlockPos,String> desired=new LinkedHashMap<>(RecipeCompiler.compile(recipe,maxBlocks));
|
||||
@@ -184,6 +244,7 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
if(!part.positions().containsAll(desired.keySet()))throw new Fault("out_of_bounds","Patch exceeds the exact part mask; create a new part for an extension");
|
||||
}
|
||||
for(BlockPos at:desired.keySet())checkSurroundings(at,desired);
|
||||
if(p.has("expected_blocks"))ExpectedBlocks.check(p.getAsJsonArray("expected_blocks"),desired.keySet(),access::canonical,access::getBlock,at->BuildingWorld.snapshotId(access.captureBlock(at)));
|
||||
Plan value=engine.prepare(projectId,epoch,region,desired,dependencies);ioBusy=true;return value;
|
||||
});
|
||||
try { engine.persistPlan(plan.id()); return planSummary(plan); }
|
||||
@@ -228,11 +289,12 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
}
|
||||
private Object context() {
|
||||
return Map.ofEntries(Map.entry("schema_version",1),Map.entry("project_id",projectId),Map.entry("world_id",world.getUID().toString()),Map.entry("world_epoch",epoch),Map.entry("region",region),
|
||||
Map.entry("max_plan_blocks",maxBlocks),Map.entry("parts",parts.values().stream().limit(64).map(p->Map.of("part_id",p.id(),"name",p.name(),"protected",p.locked(),"block_count",p.positions().size())).toList()),
|
||||
Map.entry("max_plan_blocks",maxBlocks),Map.entry("checked_expected_blocks",true),Map.entry("parts",parts.values().stream().limit(64).map(p->Map.of("part_id",p.id(),"name",p.name(),"protected",p.locked(),"block_count",p.positions().size())).toList()),
|
||||
Map.entry("parts_total",parts.size()),Map.entry("operations",engine.recentOperations(20).stream().map(v->Map.of("operation_id",v.id(),"plan_id",v.planId(),"status",v.status().name().toLowerCase(Locale.ROOT),"written",v.written(),"total_changes",v.totalChanges())).toList()),
|
||||
Map.entry("operations_total",engine.operationCount()),Map.entry("truncated",parts.size()>64||engine.operationCount()>20),
|
||||
Map.entry("supported_materials",BuildingWorld.supportedMaterials()),Map.entry("recipe",Map.of("version",1,"operations",List.of("box","line","cylinder","repeat"))),Map.entry("capabilities",List.of("region_inspect","build_prepare","build_apply","operation_status","operation_cancel","operation_undo_prepare","part_define","part_get","camera_list","camera_capture","asset_list","schematic_export","schematic_import_prepare")),
|
||||
Map.entry("limitations",List.of("One configured owner and project","Loaded chunks only","No automatic recipe merge","Complete delta journal is not implemented","Camera readiness is heuristic","Sponge schematic v2 only; no entities or block entities")));
|
||||
Map.entry("terrain",Map.of("version",1,"features",List.of("hill","ridge","plateau","channel","basin","terrace"),"modes",List.of("sculpt","fill","cut"),"max_cached_recipes",32,"fluid_placement",false,"brush_actions",List.of("raise","lower","flatten","smooth"),"brush_max_scan_blocks",4096)),Map.entry("material_catalog",materials.summary()),Map.entry("recipe",Map.of("version",1,"operations",List.of("box","line","cylinder","repeat"))),Map.entry("capabilities",List.of("material_search","material_describe","region_inspect","build_prepare","build_apply","operation_status","operation_cancel","operation_undo_prepare","part_define","part_get","camera_list","camera_capture","asset_list","schematic_export","schematic_import_prepare","terrain_preview","terrain_prepare","terrain_brush_prepare")),
|
||||
Map.entry("block_data",Map.of("all_registered_block_states",true,"fluid_placement",true,"block_entity_snapshots",true,"raw_nbt_editing",false,"item_inventory_editing",false)),
|
||||
Map.entry("limitations",List.of("One configured owner and project","Loaded chunks only","No automatic recipe merge","Complete delta journal is not implemented","Camera readiness is heuristic","Schematic v2: entity and block-entity payloads rejected","Later world simulation is not a journaled direct edit")));
|
||||
}
|
||||
private Object inspect(JsonObject p) {
|
||||
Region area=new Region(world.getUID().toString(),pos(p.getAsJsonObject("min")),pos(p.getAsJsonObject("max")));
|
||||
@@ -240,8 +302,14 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
if(area.volume()>4096)throw new Fault("budget_exceeded","Read at most 4096 blocks per request");
|
||||
Map<String,Integer> palette=new TreeMap<>();List<Object> blocks=new ArrayList<>();
|
||||
boolean exact=str(p,"detail","summary").equals("blocks");
|
||||
long snapshotBytes=0;
|
||||
for(int y=area.min().y();y<=area.max().y();y++)for(int z=area.min().z();z<=area.max().z();z++)for(int x=area.min().x();x<=area.max().x();x++){
|
||||
BlockPos at=new BlockPos(x,y,z);String state=access.getBlock(at);palette.merge(state,1,Integer::sum);if(exact)blocks.add(Map.of("pos",at,"state",state));
|
||||
BlockPos at=new BlockPos(x,y,z);String state=access.getBlock(at);palette.merge(state,1,Integer::sum);
|
||||
if(exact){
|
||||
String captured=access.captureBlock(at);snapshotBytes+=captured.getBytes(StandardCharsets.UTF_8).length;
|
||||
if(snapshotBytes>8_388_608)throw new Fault("budget_exceeded","Block-entity snapshots exceed 8 MiB; inspect a smaller area");
|
||||
Map<String,Object> value=new LinkedHashMap<>(BuildingWorld.publicSnapshot(captured));value.put("pos",at);blocks.add(value);
|
||||
}
|
||||
}
|
||||
return Map.of("region",area,"palette",palette,"blocks",blocks,"sampled_at",System.currentTimeMillis(),"world_epoch",epoch,"truncated",false);
|
||||
}
|
||||
@@ -249,7 +317,22 @@ public final class BuilderPlugin extends JavaPlugin {
|
||||
return Map.of("plan_id",plan.id(),"plan_hash",hash(plan),"changed_blocks",plan.changes().stream().filter(c->!c.expected().equals(c.desired())).count(),"region",plan.region(),"expires_at",plan.expiresAtMillis());
|
||||
}
|
||||
private Map<String,Object> status(OperationView v) {
|
||||
return Map.ofEntries(Map.entry("operation_id",v.id()),Map.entry("plan_id",v.planId()),Map.entry("status",v.status().name().toLowerCase(Locale.ROOT)),Map.entry("written",v.written()),Map.entry("total_changes",v.totalChanges()),Map.entry("processed",v.processed()),Map.entry("conflicts",v.conflicts()),Map.entry("message",Objects.toString(v.message(),"")));
|
||||
return Map.ofEntries(Map.entry("operation_id",v.id()),Map.entry("plan_id",v.planId()),Map.entry("status",v.status().name().toLowerCase(Locale.ROOT)),Map.entry("written",v.written()),Map.entry("total_changes",v.totalChanges()),Map.entry("processed",v.processed()),Map.entry("conflicts",v.conflicts().stream().map(BuilderPlugin::publicConflict).toList()),Map.entry("message",Objects.toString(v.message(),"")));
|
||||
}
|
||||
private static Map<String,Object> publicConflict(Conflict conflict) {
|
||||
Map<String,Object> result=new LinkedHashMap<>();result.put("pos",conflict.pos());result.put("reason",conflict.reason());
|
||||
for(var entry:Map.of("expected",conflict.expected(),"current",conflict.current(),"desired",conflict.desired()).entrySet()){
|
||||
var snapshot=BuildingWorld.publicSnapshot(entry.getValue());result.put(entry.getKey(),snapshot.get("state"));
|
||||
if(snapshot.containsKey("snapshot_id"))result.put(entry.getKey()+"_snapshot_id",snapshot.get("snapshot_id"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private static String rotateSchematicState(String state,int degrees)throws IOException {
|
||||
try{
|
||||
var data=Bukkit.createBlockData(state);
|
||||
data.rotate(switch(degrees){case 0->org.bukkit.block.structure.StructureRotation.NONE;case 90->org.bukkit.block.structure.StructureRotation.CLOCKWISE_90;case 180->org.bukkit.block.structure.StructureRotation.CLOCKWISE_180;case 270->org.bukkit.block.structure.StructureRotation.COUNTERCLOCKWISE_90;default->throw new IllegalArgumentException("Unsupported rotation");});
|
||||
return data.getAsString();
|
||||
}catch(IllegalArgumentException failure){throw new IOException("Invalid runtime schematic block state",failure);}
|
||||
}
|
||||
private String hash(Plan p) {
|
||||
try{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json.toJson(p).getBytes(StandardCharsets.UTF_8)));}catch(NoSuchAlgorithmException e){throw new AssertionError(e);}
|
||||
|
||||
@@ -2,38 +2,38 @@ package io.github.minecraftbuilder.paper;
|
||||
|
||||
import io.github.minecraftbuilder.core.*;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import java.util.*;
|
||||
|
||||
/** Small tested policy; unsupported existing contents are protected too. */
|
||||
/** Live registry block data with private, lossless block-entity snapshots for guarded edits and undo. */
|
||||
final class BuildingWorld implements WorldAccess, BlockPolicy {
|
||||
private final World world;
|
||||
private final Map<String, BlockData> parsed = new HashMap<>();
|
||||
private static final Set<String> MATERIALS = Set.of("air", "stone", "cobblestone", "mossy_cobblestone",
|
||||
"stone_bricks", "mossy_stone_bricks", "cracked_stone_bricks", "chiseled_stone_bricks", "smooth_stone",
|
||||
"granite", "polished_granite", "diorite", "polished_diorite", "andesite", "polished_andesite",
|
||||
"deepslate", "cobbled_deepslate", "polished_deepslate", "deepslate_bricks", "deepslate_tiles",
|
||||
"bricks", "quartz_block", "quartz_pillar", "smooth_quartz", "sandstone", "cut_sandstone", "smooth_sandstone",
|
||||
"red_sandstone", "terracotta", "white_terracotta", "black_terracotta", "orange_terracotta",
|
||||
"white_concrete", "gray_concrete", "black_concrete", "glass", "tinted_glass", "obsidian",
|
||||
"dirt", "grass_block", "bedrock", "oak_planks", "spruce_planks", "birch_planks", "dark_oak_planks",
|
||||
"oak_log", "spruce_log", "birch_log", "dark_oak_log", "stripped_oak_log", "stripped_spruce_log",
|
||||
"stone_brick_stairs", "cobblestone_stairs", "oak_stairs", "spruce_stairs", "deepslate_tile_stairs",
|
||||
"stone_brick_slab", "cobblestone_slab", "oak_slab", "spruce_slab", "smooth_stone_slab",
|
||||
"lantern", "iron_chain", "iron_bars", "stone_brick_wall", "oak_leaves", "moss_block",
|
||||
"gray_stained_glass", "brown_stained_glass", "glowstone", "gold_block");
|
||||
private final PaperBlockStateCodec snapshots = new PaperBlockStateCodec();
|
||||
private final Map<String, BlockData> parsed = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override protected boolean removeEldestEntry(Map.Entry<String, BlockData> entry) { return size() > 4096; }
|
||||
};
|
||||
|
||||
BuildingWorld(World world) { this.world = world; }
|
||||
static List<String> supportedMaterials() { return MATERIALS.stream().sorted().map(s->"minecraft:"+s).toList(); }
|
||||
static List<String> supportedMaterials() { return Registry.BLOCK.stream().map(type -> type.getKey().toString()).sorted().toList(); }
|
||||
BlockData data(String state) { return parsed.computeIfAbsent(state, Bukkit::createBlockData).clone(); }
|
||||
String canonical(String state) { if (!supports(state)) throw new RpcServer.Fault("unsupported_block", "Unsupported block: " + state); return data(state).getAsString(); }
|
||||
public boolean supports(String state) {
|
||||
try {
|
||||
BlockData data = data(state);
|
||||
return MATERIALS.contains(data.getMaterial().getKey().getKey())
|
||||
&& !(data instanceof org.bukkit.block.data.Waterlogged w && w.isWaterlogged())
|
||||
&& !(data instanceof org.bukkit.block.data.type.Leaves leaves && !leaves.isPersistent());
|
||||
} catch (IllegalArgumentException e) { return false; }
|
||||
String canonical(String state) {
|
||||
// Opaque journal payloads are internal: model-supplied data is always parsed as BlockData.
|
||||
try { return data(state).getAsString(); }
|
||||
catch (IllegalArgumentException e) { throw new RpcServer.Fault("unsupported_block", "Invalid registered block or block properties"); }
|
||||
}
|
||||
public boolean supports(String state) {
|
||||
try { return supportsData(data(BlockSnapshots.state(state))); }
|
||||
catch (IllegalArgumentException | NullPointerException e) { return false; }
|
||||
}
|
||||
static boolean supportsData(BlockData data) {
|
||||
// A parsed BlockData already represents a registered block, unlike item-only Materials.
|
||||
return data != null && data.getMaterial() != null && !data.getMaterial().isLegacy();
|
||||
}
|
||||
static Map<String, String> publicSnapshot(String value) { return BlockSnapshots.publicView(value); }
|
||||
static String snapshotId(String value) { return BlockSnapshots.snapshotId(value); }
|
||||
static String canonicalSnapshotState(String value) { return BlockSnapshots.state(value); }
|
||||
|
||||
private void ready(BlockPos p) {
|
||||
if (!Bukkit.isPrimaryThread()) throw new IllegalStateException("World access outside server thread");
|
||||
if (p.y() < world.getMinHeight() || p.y() >= world.getMaxHeight()) throw new RpcServer.Fault("out_of_bounds", "Position exceeds world height");
|
||||
@@ -41,15 +41,26 @@ final class BuildingWorld implements WorldAccess, BlockPolicy {
|
||||
if (!world.getWorldBorder().isInside(new Location(world,p.x()+0.5,p.y(),p.z()+0.5))) throw new RpcServer.Fault("out_of_bounds", "Position exceeds world border");
|
||||
}
|
||||
public String getBlock(BlockPos p) { ready(p); return world.getBlockAt(p.x(),p.y(),p.z()).getBlockData().getAsString(); }
|
||||
public void setBlock(BlockPos p, String state) {
|
||||
public String captureBlock(BlockPos p) {
|
||||
ready(p);
|
||||
// A neighbour may have changed since prepare. Never remove supports next to
|
||||
// dynamic/unsupported blocks merely because the target itself still matches.
|
||||
for (BlockPos d : List.of(new BlockPos(1,0,0),new BlockPos(-1,0,0),new BlockPos(0,1,0),new BlockPos(0,-1,0),new BlockPos(0,0,1),new BlockPos(0,0,-1))) {
|
||||
BlockPos n=p.add(d);
|
||||
if(n.y()>=world.getMinHeight()&&n.y()<world.getMaxHeight()&&!supports(getBlock(n)))
|
||||
throw new RpcServer.Fault("unsupported_block","Adjacent environment changed at "+n);
|
||||
}
|
||||
world.getBlockAt(p.x(),p.y(),p.z()).setBlockData(data(state), false);
|
||||
return snapshots.capture(world.getBlockAt(p.x(),p.y(),p.z()).getState());
|
||||
}
|
||||
public String prepareBlock(BlockPos p, String desired, String capturedBefore) {
|
||||
if (BlockSnapshots.captured(desired)) return desired; // Exact private undo snapshot, never merge it.
|
||||
BlockData after = data(desired);
|
||||
BlockSnapshots.Value before = BlockSnapshots.decode(capturedBefore);
|
||||
if (before.nbt() != null && data(before.state()).getMaterial() == after.getMaterial())
|
||||
return BlockSnapshots.encode(after.getAsString(), snapshots.prepareData(before.nbt(), data(before.state()), after));
|
||||
return snapshots.capture(after.createBlockState());
|
||||
}
|
||||
public void setBlock(BlockPos p, String state) {
|
||||
setCapturedBlock(p, prepareBlock(p, canonical(state), captureBlock(p)));
|
||||
}
|
||||
public void setCapturedBlock(BlockPos p, String capturedState) {
|
||||
ready(p);
|
||||
BlockSnapshots.Value value = BlockSnapshots.decode(capturedState);
|
||||
BlockState state = data(value.state()).createBlockState().copy(new Location(world, p.x(), p.y(), p.z()));
|
||||
if (value.nbt() != null) snapshots.restoreData(state, value.nbt());
|
||||
snapshots.place(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.github.minecraftbuilder.paper;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.github.minecraftbuilder.core.BlockPos;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/** Optional caller snapshot, checked in the same server task that creates the plan. */
|
||||
final class ExpectedBlocks {
|
||||
private ExpectedBlocks() { }
|
||||
|
||||
static void check(JsonArray entries, Set<BlockPos> desired, Function<String,String> canonical,
|
||||
Function<BlockPos,String> read) {
|
||||
check(entries, desired, canonical, read, ignored -> null);
|
||||
}
|
||||
|
||||
static void check(JsonArray entries, Set<BlockPos> desired, Function<String,String> canonical,
|
||||
Function<BlockPos,String> read, Function<BlockPos,String> snapshotId) {
|
||||
if (entries.size() != desired.size() || entries.size() > 4096)
|
||||
throw new RpcServer.Fault("invalid_request", "expected_blocks must cover every desired position exactly once");
|
||||
Map<BlockPos,String> expected = new LinkedHashMap<>();
|
||||
Map<BlockPos,String> snapshots = new HashMap<>();
|
||||
for (JsonElement entry : entries) {
|
||||
JsonObject value = entry.getAsJsonObject();
|
||||
JsonObject p = value.getAsJsonObject("pos");
|
||||
BlockPos at = new BlockPos(integer(p,"x"), integer(p,"y"), integer(p,"z"));
|
||||
if (!desired.contains(at) || expected.containsKey(at))
|
||||
throw new RpcServer.Fault("invalid_request", "Unexpected or duplicate expected_blocks position");
|
||||
expected.put(at, canonical.apply(value.get("state").getAsString()));
|
||||
if (value.has("snapshot_id")) {
|
||||
JsonElement id=value.get("snapshot_id");
|
||||
if(!id.isJsonPrimitive() || !id.getAsJsonPrimitive().isString() || !id.getAsString().matches("[a-f0-9]{64}"))
|
||||
throw new RpcServer.Fault("invalid_request", "snapshot_id must be a SHA-256 digest");
|
||||
snapshots.put(at,id.getAsString());
|
||||
}
|
||||
}
|
||||
for (var entry : expected.entrySet()) {
|
||||
if (!entry.getValue().equals(read.apply(entry.getKey())))
|
||||
throw new RpcServer.Fault("stale_snapshot", "Caller snapshot changed at " + entry.getKey());
|
||||
String currentId=snapshotId.apply(entry.getKey()), expectedId=snapshots.get(entry.getKey());
|
||||
if(currentId!=null && expectedId==null)
|
||||
throw new RpcServer.Fault("invalid_request", "expected_blocks needs snapshot_id for block-entity data at " + entry.getKey() + "; inspect this block again");
|
||||
if(expectedId!=null && !expectedId.equals(currentId))
|
||||
throw new RpcServer.Fault("stale_snapshot", "Caller block-entity snapshot changed at " + entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static int integer(JsonObject value, String name) {
|
||||
try { return value.get(name).getAsBigDecimal().intValueExact(); }
|
||||
catch (ArithmeticException | NumberFormatException ex) {
|
||||
throw new RpcServer.Fault("invalid_request", "Expected position coordinates must be integers");
|
||||
}
|
||||
}
|
||||
}
|
||||