Skip to content

Releases: FinnSetchell/Structure-Editor-MCP

Structure Editor v2.0.0 (Minecraft 26.3)

Choose a tag to compare

@FinnSetchell FinnSetchell released this 18 Sep 12:51

Minecraft 26.3 support. This build runs only on 26.3 — for a 1.21.10 server use the 1.21.10 branch, which still has the last 1.4.x build.

Not a version bump: 26.1 was the first unobfuscated Minecraft release and Fabric discontinued Yarn, so the mod was migrated to Mojang mappings and the non-remapping net.fabricmc.fabric-loom. Gradle 9.5.1, Java 25, fabric-loader 0.19.5, fabric-api 0.160.7+26.3.

Verified

Against a real 26.3 dedicated server with no players online:

  • 33/33 endpoint checks — scans, containers, block-entity NBT, jigsaw/structure edits, set_blocks/replace_blocks/undo, selections, save/palette/download, file write, and the three SBS-backed lookups.
  • 3/3 cold-chunk entity saves — a persistent named mob in an unloaded chunk is captured every time (FRESH → LOADED, ~60 ms), confirmed in the written NBT. The empty-server pause fix carries over intact.
  • Dev client starts clean with no mixin failures.

Bugs found and fixed during the port

Each of these compiled fine and would have failed only at runtime:

  • Screen guards targeted an inherited method. The jigsaw/structure GUI guards injected into tick(), which neither screen declares — it comes from Screen. They would have thrown the moment you opened a jigsaw block. Replaced with Fabric's ScreenEvents, leaving only compile-validated @Accessor mixins.
  • get_structure_palette returned empty for everything. 26.3 writes palette entries as {id, properties}; the reader still asked for Name. Now accepts both.
  • download_structure 404'd on every structure. The generated folder went singular (generated/<ns>/structure/). Falls back to the plural form for older worlds.
  • Stale @Inject strings after the mappings migration, including getPauseWhenEmptySeconds — left alone, that would have silently reverted the empty-server pause fix and reintroduced the entity-loss bug from v1.4.7.

Notable API changes handled

ResourceLocationIdentifier (Mojang adopted Yarn's name), ChunkPos became a record (unpack/pack, accessor components), displayClientMessage split into overlay/system, StateHolder.getValues() removed, PayloadTypeRegistry.playS2CclientboundPlay. Client rendering moved to vanilla's new gizmo system, since GuiGraphics and MultiBufferSource no longer exist.

Notes

  • StructureBlockSaver needs its own 26.3 port. Without it on the world, list_structure_blocks, find_structure_bounds and set_selection_to_structure return sbs_tracker_missing. The reader itself is verified working on 26.3 against a tracker file.
  • The in-world selection-box rendering is not visually verified — a dedicated-server harness can't draw. It compiles and initialises cleanly; worth a look in-game.
  • The MCP server half is version-agnostic (it only speaks HTTP to the mod), so it does not need rebuilding when switching Minecraft versions.

Structure Editor v1.4.9

Choose a tag to compare

@FinnSetchell FinnSetchell released this 11 Sep 07:06

scan_entities now sees cold chunks, plus a race fix that also hardens saves.

scan_entities force-loads. It only ever reported entity sections already resident, so a cold area read as empty even when full of armour stands, item frames and named mobs. It now tickets every chunk in the bounds, waits for the entity manager to load them, reads, and releases the tickets — the same sequence save_structures uses. Response adds chunks, entity_sections_loaded, entity_wait_ms, and warning_entities if any chunk never finished.

Race fix (scan and save). Testing this surfaced a real race: the first cold scan after a fresh start returned zero while every chunk reported loaded. The poll was calling readIfFresh on not-yet-loaded chunks, which requests the entity-file read before the chunk map raises the chunk's visibility; the entities then land in sections created HIDDENisLoaded goes true, but entity queries skip hidden sections until the next chunk-map tick. Vanilla never loads entities ahead of visibility. Fix: never request the read ourselves, and treat a chunk as ready only when it is LOADED and its visibility is present and not HIDDEN. Applied to save_structures too; the readIfFresh invoker is removed.

Verified locally: five consecutive cold scans each found all three entities (including a 1.8 s first read after startup that previously raced to zero); a cold save captured all three.

Deploy: replace the mst jar (exactly one copy in mods/) and restart.

Structure Editor v1.4.8

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 13:30

Small robustness follow-up to 1.4.7 — no change to saves. Deploy whenever convenient; 1.4.7 is fully working on the build server.

Fixed: read_server_log / GET /log/tail could fail on a hosted server's latest.log. readAllLines(UTF_8) throws on the first non-UTF-8 byte, and stray control characters from other mods inside the JSON strings broke strict parsers (the tool failed to parse its own payload on Bisect). The endpoint now decodes leniently (bad bytes → U+FFFD) and strips C0 control characters other than tab; the MCP tool falls back to returning the raw text if the payload still isn't strict JSON instead of failing the call.

Verification recap for 1.4.7 on the build server (dedicated, no players, >80 s after start): no pausing line; cold save of mns:mega_fortress/mobs/fortress_guard went FRESH → LOADED in 54 ms, entities_captured: 1, and the downloaded template contains the persistent named wither skeleton; a second cold save on the same anchor and a 9-chunk save on mos:acacia_long_ship/hull_2d both reported every chunk LOADED.

Structure Editor v1.4.7

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 13:02

The actual fix for cold-chunk saves losing entities. Root cause found with the 1.4.6 diagnostics on Bisect and reproduced/verified locally.

Root cause: the server was still pausing. The v1.3.4 pause fix injected into MinecraftServer.getPauseWhenEmptySeconds, but MinecraftDedicatedServer overrides that getter (reading server.properties directly) and the pause check calls it virtually — so on every dedicated server the injection never ran. Bisect's log: Server empty for 60 seconds, pausing.

While paused, the task queue still drains, so mst's requests appear to work and chunk tickets still load block chunks — but world ticks stop, and entityManager.tick → processPendingLoads is what collects finished entity-file reads. Every chunk sits at PENDING forever (and can never unload), vanilla's saveStructure iterates an empty entity store, and the template is written with zero entities and success: true. A nearby player unpauses the server (why it always worked with someone standing there); the first save after a restart lands inside the grace window (why v1.4.3 "worked once").

Fix: MinecraftDedicatedServerMixin applies the same injection to the override. Both mixins are required. Gated by keep_server_ticking (default on).

Verified on a local Fabric 1.21.10 dedicated server with pause-when-empty-seconds=5 and StructureBlockSaver installed:

  • fix on: 3/3 cold saves of a persistent named wither skeleton captured, no pausing line in 61 s of empty uptime
  • fix off (negative control): server paused at 5 s, same save sat PENDING for 8 s and wrote 0 entities — Bisect's exact fingerprint

Deploy: replace the mst jar (exactly one copy in mods/) and restart. After startup, read_server_log(grep: "pausing") must return nothing; get_chunk_state on a cold chunk after a save must show entity_load: LOADED. Belt-and-braces alternative if you ever run without this mod: pause-when-empty-seconds=0 in server.properties.

Structure Editor v1.4.6

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 12:30

Verified locally against the cold-chunk entity-save repro, plus a shutdown fix found while doing it.

Cold-chunk entity save: verified

Stood up a private Fabric 1.21.10 dedicated server (fabric-api 0.138.4, StructureBlockSaver 1.2.4, flat world, no players) with a 1x4x1 SAVE anchor 3000 blocks from spawn holding a persistent named wither skeleton. Ran three save rounds, each one waiting until the new /chunk-state endpoint reported the chunk genuinely cold (block chunk not resident, entity visibility HIDDEN, load state FRESH) — i.e. the after-release path that regressed under v1.4.3. All three saves captured the mob (entities_captured matched the generated NBT) with the entity sections reaching LOADED in 60–185 ms. The v1.4.4/1.4.5 ticket + readIfFresh + poll logic is correct, including with SBS installed.

The remaining Bisect failure was observed on v1.4.4 (the 1.4.5 jar was never actually loaded there — /log/tail returned 404) and is environment-specific; read_server_log and chunk_states on this build will pin it down.

Fixed: server could not exit after /stop

EditorHttpServer ran on a non-daemon thread pool and nothing called stop(). After /stop the game server ended but the JVM stayed alive holding the port — a zombie answering serverReady:false that also blocks the next instance's bind (which then logs Failed to start HTTP server: Address already in use and runs with no MCP bridge). Executor threads are now daemon and SERVER_STOPPED shuts the bridge down. Verified: port released and JVM exited on its own 3 s after /stop.

New

  • get_chunk_state({x, z}) / GET /chunk-state — read-only chunk diagnostics: block chunk resident, block-ticking, entity load state (FRESH/PENDING/LOADED), entity visibility (HIDDEN/TRACKED/TICKING/ABSENT). Loads nothing.

Deploy note: make sure exactly one structure-editor-*.jar is in mods/ — remove 1.4.4/1.4.5 when adding 1.4.6.

Structure Editor v1.4.5

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 09:54

Diagnostic build for the cold-chunk entity save bug. v1.4.4 on fresh-from-restart chunks reported chunks_ticking_ready: 1 but isLoaded never went true in 8 s and the save captured zero — where v1.4.3 had captured the mob on that same first-save case. A boolean can't say why, and the server log can, so this build adds instruments rather than a fourth theory.

Reading the vanilla PersistentEntitySectionManager source (mojmap, identical 1.21.10 code): load goes FRESH → requestChunkLoad sets PENDING and reads the entity file async → processPendingLoads on the next tick sets LOADED. Unload refuses while PENDING. So a stuck isLoaded means either the chunk is parked in PENDING (read never landed; vanilla logs Failed to read chunk) or it never left FRESH (the visibility callback never fired).

New

  • read_server_log({lines?, grep?}) MCP tool and GET /log/tail — last N lines of logs/latest.log, optional case-insensitive substring filter applied before the cap.
  • save_structures response now includes chunk_states per structure block: for every ticketed chunk, entity_load (FRESH/PENDING/LOADED), entity_visibility (HIDDEN/TRACKED/TICKING/ABSENT), block_ticking. The poll-timeout warning in the log carries the same per stuck chunk.

Fixed

  • FORCED is the persisted /forceload ticket type. Anything that threw before removeTicket leaked a chunk that stayed force-loaded across restarts. Ticket release is now unconditional (finally in the save phase; partial release if enumeration throws mid-way).

Structure Editor v1.4.4

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 09:40

Follow-up to v1.4.3, which was tested against the 1x4x1 fortress_guard repro with nobody online: the first cold save captured the mob, but the next two saves on the same chunks (tickets released, area reading cold again) captured zero. So it was not a timing race a sleep could paper over — once a chunk has been loaded and released, re-adding a FORCED ticket does not reliably get its entity sections re-read.

What changed

The wait between the ticket phase and the save phase is now a poll on the actual condition instead of a fixed 1 s sleep. Two new mixins expose ServerWorld.entityManager and ServerEntityManager.isLoaded(long) / readIfFresh(long). Every 50 ms the poll hops to the server thread, checks isLoaded for every chunk any target needs, and calls readIfFresh on any that isn't — that schedules the entity-file read only when the section is still FRESH, so it is a no-op on an already-loaded chunk and cannot duplicate entities. Bounded at 8 s.

Diagnostics in the response

Top level: entity_sections_loaded (bool), entity_wait_ms.
Per structure block: entities_captured, chunks, chunks_entity_sections_loaded, chunks_ticking_ready.

A zero entities_captured now arrives with the chunk state that explains it, instead of a silent success: true.

Caveat: the mixin target names come straight from the 1.21.10 yarn mapping, but @Accessor/@Invoker targets are only fully validated when the mixin applies at server start. If the server refuses to start on this build, it's a target-name mismatch and a one-line fix — report the startup log line.

Structure Editor v1.4.3

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 09:30

Fix: save_structures on cold chunks. Third try, this one actually addresses the level of the chunk load rather than dancing around it.

What was wrong

v1.4.2 iterated entities via getOtherEntities in Phase 1 to try to trigger the async entity load. That method only iterates already-loaded sections; it does not tell PersistentEntitySectionManager to read the entity file. So on cold chunks the iteration returned zero, kicked nothing, and the subsequent save still saw an empty entity store. Finn's 1x4x1 A/B repro (single chunk, entity inside the SB's own chunk) confirmed the problem is level/timing of the chunk load, not bounds coverage.

What now happens

The actual vanilla path: chunks reach ENTITY_TICKING level via a chunk ticket, at which point ServerChunkLoadingManager tells the entity manager to load the entity file. FORCED tickets (what /forceload uses) reach that level.

save_structures now:

  1. Phase 1 (server thread) — enumerate every SB in scope, compute each SB's captured AABB, walk the chunks it overlaps, and add a FORCED chunk-loading ticket for every one of those chunks via world.getChunkManager().addTicket(ChunkTicketType.FORCED, cp, 2).
  2. Sleep 1s on the HTTP thread so ticket propagation and entity file reads complete.
  3. Phase 2 (server thread) — re-fetch each SB, count entities in the bounds, call saveStructure, then removeTicket for every ticket added in Phase 1 (FORCED has NO_EXPIRATION and would otherwise pin those chunks forever).

Safety net kept: the response still carries entities_captured per SB.

Test: run Finn's minimal repro (1x4x1 fortress_guard anchor, cold chunks, no player nearby). Save. Check entities_captured: 1 in the response and confirm the downloaded template contains the wither skeleton.

Structure Editor v1.4.2

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 08:28

Fix: save_structures was silently dropping entities from cold chunks even when the structure's bounds fit entirely inside the SB's own chunk.

The v1.4.1 fix force-loaded every chunk that overlapped the structure's bounds. That got block chunks resident, but not entity chunks: on 1.17+ entities live in a separate per-chunk store managed by PersistentEntitySectionManager and loaded asynchronously, independent of block sections. world.getChunk doesn't touch it, and vanilla saveStructure then iterated entities from a store that hadn't loaded yet — templates came back with zero entities, block counts byte-identical to the correct save.

The 1x4x1 anchor case that reproduces it (single chunk, entity inside the SB's own chunk) ruled out any bounds-computation issue.

Fix: save_structures now runs in two phases:

  1. Server thread — enumerate every SB in scope, force-load block chunks, and iterate entities in each SB's AABB via getOtherEntities to kick the async entity-section load.
  2. Wait 300ms (~6 ticks) so PersistentEntitySectionManager finishes deserialising the entity files.
  3. Server thread — re-fetch each SB and call saveStructure.

Safety net: the response now includes entities_captured per SB — the entity count in the bounds at save time. Callers can spot a zero-that-should-not-be-zero (e.g., re-saving an anchor known to hold a mob) and retry rather than silently overwriting a shipped template.

Per Finn's report this bug hit 22 of 261 pieces in a region-wide save before it was caught by diffing entity counts against previously shipped NBTs.

Structure Editor v1.4.1

Choose a tag to compare

@FinnSetchell FinnSetchell released this 10 Sep 07:58

Fix: save_structures was silently dropping entities that lived in cold chunks around a structure block.

The save path only force-loaded the chunk holding the structure block itself. Vanilla's saveStructure then iterates entities across the structure's captured volume, but the entity sections in the neighbouring chunks were never loaded, so armour stands, item frames, and any mobs baked into a piece disappeared from the save. A workaround was to scan the area first (which loaded the chunks) and re-save.

Now save_structures computes the world-space bounds from the SB's offset + sizeX/Y/Z and force-loads every chunk that overlaps them before triggering the save, matching the pattern SBS itself uses in GlobalSaveTask. LOAD/CORNER/DATA-mode structure blocks are skipped since they carry no bounds.

No API changes.