Skip to content

In-world editor

The in-world editor is the largest part of conduit-world: a WorldEdit-style block editor usable in any world, plus the durable storage and import paths behind it. It’s six classes working together:

ClassJob
WorldEditThe wand callbacks, the editor command tree, and the selection-outline ticker.
WandSelectionPer-player, session-scoped pos1/pos2 selection state.
ArenaSnapshotImmutable palette + index capture of a region, the payload behind clipboard, presets, and .schem import.
ArenaStoreNamed, restart-durable arena presets on disk (and jar-bundled defaults).
SchematicIoSponge Schematic (.schem) v2/v3 import and v3 export, bridged to ArenaSnapshot.
ArenaMarkersReads glazed-terracotta marker blocks out of a build into gameplay anchors.

All editor commands are gated on the ConduitPermissions.CAP_EDITOR capability ("conduit.editor"). The module’s ModInitializer calls WorldEdit.register() and ArenaStore.init() for you at startup.

A WorldEdit-style editor usable in any world. The wand is a vanilla golden axe: while an op holds it, left-clicking a block marks pos1 and right-clicking marks pos2 (both cancel the vanilla break/place). Without the cap or the wand, both clicks pass through untouched so normal play is unaffected. A cyan dust outline tracks the live selection, redrawn every few ticks.

Each op is its own top-level command (no shared prefix), each gated on CAP_EDITOR:

/wand give the golden-axe wand
/pos1 | /pos2 mark a corner at the player's feet
/sel print the selection bounds + block count
/set <block> fill the selection
/walls <block> fill the four vertical walls
/replace <from> <to> swap one block for another in the selection
/copy capture the selection into the clipboard
/paste paste the clipboard at the player's feet
/undo undo the last mutating op
/schem list list .schem files in conduit/schematics/
/schem paste <name> paste a .schem at pos1 (else the player's feet)
/schem save <name> write the current selection out as a .schem

/set, /walls, and /replace delegate to Regions. /copy, /paste, /schem paste, and /undo all run through ArenaSnapshot. Every mutating op (including schem paste and paste) snapshots the affected region first, so /undo restores it; the undo history is bounded to 16 entries per player. /paste and /schem paste use the air-faithful pasteExact path and auto-select the pasted region.

package me.zlex.conduit.editor;
public final class WorldEdit {
/** Wires the wand, the editor command tree and the outline ticker. Idempotent. */
public static void register();
/** Drops a player's clipboard + undo history. Call on disconnect to avoid leaks. */
public static void clearPlayer(UUID player);
}

Per-player WorldEdit-style selection state, the two corner positions a player has marked. Selections are session-scoped: they live only in memory and are forgotten on restart. Corners are stored exactly as clicked; min/max normalise the pair so min is the lowest corner on every axis and max the highest. All methods are keyed by UUID and safe to call from the server thread.

package me.zlex.conduit.editor;
public final class WandSelection {
public static void setPos1(UUID player, BlockPos pos);
public static void setPos2(UUID player, BlockPos pos);
public static void clear(UUID player);
public static @Nullable BlockPos pos1(UUID player); // raw first corner, or null
public static @Nullable BlockPos pos2(UUID player); // raw second corner, or null
public static boolean hasSelection(UUID player); // true only when both set
public static @Nullable BlockPos min(UUID player); // normalised low corner, or null
public static @Nullable BlockPos max(UUID player); // normalised high corner, or null
}

An immutable capture of a rectangular region of blocks, the durable payload behind both the editor clipboard and saved arena presets. It stores the box dimensions (sizeX × sizeY × sizeZ) plus a palette/index pair: palette is the distinct BlockStates in the region, and indices is one palette index per cell.

Cells are laid out in x → z → y order, the order capture walks and paste replays. (The Sponge .schem format uses YZX instead; SchematicIo reorders on import/export.) A single-block-type region stays tiny on disk: the palette has one entry and the index array compresses well.

Captures are bounded by MAX_VOLUME ≈ 16.7M cells (16_777_216); capture throws IllegalArgumentException on a larger selection, and SchematicIo bounds its ingest identically.

public void paste(ServerLevel level, BlockPos origin); // fast, skips air
public void pasteExact(ServerLevel level, BlockPos origin); // faithful, overwrites with air
  • paste, the fast game-build path. Skips air cells (writes non-air with flag 2, no neighbour cascades). Because games paste into freshly-prepared/void instance worlds where the destination is already empty, leaving air unwritten is safe and keeps million-cell builds pasteable in well under a second.
  • pasteExact, the faithful path used by the editor’s /paste, /schem paste, and /undo. Writes air too (air with flag 3 to re-fire lighting/neighbour updates; non-air with flag 2), so the pasted region matches the source exactly even over non-air terrain.

Both variants force-load the chunks the region spans for the write and release them afterwards, so the blocks land even with no players nearby.

package me.zlex.conduit.editor;
public final class ArenaSnapshot {
// Construct (prefer capture)
public ArenaSnapshot(int sizeX, int sizeY, int sizeZ,
List<BlockState> palette, int[] indices);
public ArenaSnapshot(int sizeX, int sizeY, int sizeZ,
List<BlockState> palette, int[] indices,
List<SpawnPoint> spawns);
public static ArenaSnapshot capture(ServerLevel level, BlockPos from, BlockPos to);
// Accessors
public int sizeX();
public int sizeY();
public int sizeZ();
public List<BlockState> palette();
public int[] indices(); // a fresh copy each call (stays immutable)
public List<SpawnPoint> spawns(); // never null
public boolean hasSpawns();
// Spawns
public ArenaSnapshot withSpawns(List<SpawnPoint> newSpawns);
public List<PlacedSpawn> worldSpawns(BlockPos origin);
// Paste
public void paste(ServerLevel level, BlockPos origin);
public void pasteExact(ServerLevel level, BlockPos origin);
public static final Codec<ArenaSnapshot> CODEC;
// Nested records
public record SpawnPoint(double dx, double dy, double dz, float yaw) {
public static final Codec<SpawnPoint> CODEC;
}
public record PlacedSpawn(Vec3 pos, float yaw) {}
}

A preset can carry operator-placed spawn points, stored as SpawnPoint offsets relative to the snapshot’s low corner (the same corner paste writes from) plus a facing yaw. Storing offsets (not absolute coordinates) keeps spawns aligned to the blocks no matter where the snapshot is pasted: worldSpawns(origin) resolves them to world-space PlacedSpawns as origin + (dx,dy,dz). The CODEC serialises spawns as an optional field (absent/empty for blocks-only presets), so older preset files still decode.

Named, restart-durable arena presets, one set per game id.

Presets live under <gameDir>/conduit/arenas/<gameId>/<preset>.json, each file a single ArenaSnapshot encoded via its CODEC (pretty-printed gson over JsonOps). They survive restarts and you can cat/git diff them outside the game. An in-memory concurrent cache holds every loaded snapshot keyed by gameId → preset; saves are write-through (cache + disk), and the whole tree is re-read on SERVER_STARTED.

The reserved name ArenaStore.DEFAULT ("default") is the arena a game uses when a host hasn’t picked a specific preset. Games gate their saved-arena path on hasDefault(gameId) before falling back to their procedural build.

When no preset file exists in the game directory, load falls back to a classpath resource at /conduit/arenas/<gameId>/<preset>.json. Because Fabric loads every mod on one classloader, a game can ship its authored arena inside its own jar, put it under src/main/resources/conduit/arenas/<gameId>/default.json and it works out of the box on a fresh install. An operator can still override the bundled arena by dropping a file of the same name on disk; disk wins.

package me.zlex.conduit.editor;
public final class ArenaStore {
public static final String DEFAULT = "default";
public static void init(); // idempotent; call once from a ModInitializer
public static void save(String gameId, String preset, ArenaSnapshot snapshot);
public static Optional<ArenaSnapshot> load(String gameId, String preset);
public static boolean hasDefault(String gameId);
public static List<String> list(String gameId); // cache ∪ disk (∪ bundled default), sorted
public static boolean delete(String gameId, String preset); // returns whether it existed
}

Reads and writes Sponge Schematic .schem files (gzip-compressed NBT) from <gameDir>/conduit/schematics/ and bridges them to ArenaSnapshot. This is the import/export path behind /schem: operators drop a WorldEdit-authored build into that folder, /schem paste stamps it into the world, and the existing /copy + ArenaStore tooling saves it as a preset.

AspectCoverage
ReadSponge v2 (flat root: Width/Height/Length/Palette/BlockData) and v3 (nested Schematic.Blocks{Palette,Data}). The block array is a self-decoded varint stream in YZX order; palette strings resolve via BlockStateParser.
WriteSponge v3, blocks only. Round-trips through the reader and is readable by WorldEdit.
Block entities / entities / biomesIgnored (blocks only). A chest pastes empty, a spawner inert. LoadResult.ignoredEntities() is true when the file carried such data.
Unknown blocksUnresolvable palette strings fall back to air and are reported in LoadResult.unresolved().

The importer bounds its ingest by ArenaSnapshot.MAX_VOLUME (≈16.7M cells); the writer rejects any axis longer than 65535 (the unsigned-short format limit).

package me.zlex.conduit.editor;
public final class SchematicIo {
public static List<String> list(); // .schem names (no extension), sorted
public static Path fileFor(String name); // <gameDir>/conduit/schematics/<name>.schem
public static LoadResult load(MinecraftServer server, Path file) throws IOException;
public static void save(MinecraftServer server, ServerLevel level,
BlockPos from, BlockPos to, Path file) throws IOException;
public record LoadResult(ArenaSnapshot snapshot, long blockCount,
List<String> unresolved, boolean ignoredEntities) {}
}

Reads glazed-terracotta marker blocks out of a pasted arena build and turns them into gameplay anchors. Each marker is one glazed-terracotta block placed on the floor; its colour is its role. scan walks a region, records each marker’s role + world position, then removes it (→ air) so it never shows in play. floodFloor grows a single seed into the connected walkable floor it sits on, used to discover a team platform or tile surface without hardcoding coordinates. This is the shared utility every game uses to wire hand-built schematic arenas the same way.

Glazed terracotta colourRole
LimeSPAWN
RedFINISH (doubles as the team-B seed in tug-of-war)
YellowDOLL (doubles as the rope centre / caller anchor)
MagentaROOM_SEED
BlueTEAM_A_SEED
Light blueTEAM_A_SPAWN
PinkTEAM_B_SPAWN
OrangeTILE_SEED

The Role enum also defines three reserved aliases (CALLER, TEAM_B_SEED, and ROPE_CENTER) that name the contextual meaning of the red/yellow markers. scan never emits them, so MarkerSet.get(alias) is always empty; query FINISH or DOLL instead.

package me.zlex.conduit.editor;
public final class ArenaMarkers {
public enum Role {
SPAWN, FINISH, DOLL, ROOM_SEED, CALLER, TEAM_A_SEED, TEAM_B_SEED,
TEAM_A_SPAWN, TEAM_B_SPAWN, TILE_SEED, ROPE_CENTER
}
/** Scans the inclusive box, records each marker, and removes it (→ air). */
public static MarkerSet scan(ServerLevel level, BlockPos min, BlockPos max);
/** Flood-fills the walkable floor a marker sits on, bounded by maxCells. */
public static Set<BlockPos> floodFloor(ServerLevel level, BlockPos markerPos, int maxCells);
public record MarkerSet(Map<Role, List<BlockPos>> byRole) {
public List<BlockPos> get(Role r); // positions for r, or empty
public BlockPos first(Role r); // first position, or null
public int total(); // marker count across every role
}
}