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:
| Class | Job |
|---|---|
WorldEdit | The wand callbacks, the editor command tree, and the selection-outline ticker. |
WandSelection | Per-player, session-scoped pos1/pos2 selection state. |
ArenaSnapshot | Immutable palette + index capture of a region, the payload behind clipboard, presets, and .schem import. |
ArenaStore | Named, restart-durable arena presets on disk (and jar-bundled defaults). |
SchematicIo | Sponge Schematic (.schem) v2/v3 import and v3 export, bridged to ArenaSnapshot. |
ArenaMarkers | Reads 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.
WorldEdit
Section titled “WorldEdit”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.
Commands
Section titled “Commands”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.
Public API
Section titled “Public API”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);}WandSelection
Section titled “WandSelection”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}ArenaSnapshot
Section titled “ArenaSnapshot”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.
Cell order and limits
Section titled “Cell order and limits”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.
Paste: two variants
Section titled “Paste: two variants”public void paste(ServerLevel level, BlockPos origin); // fast, skips airpublic void pasteExact(ServerLevel level, BlockPos origin); // faithful, overwrites with airpaste, the fast game-build path. Skips air cells (writes non-air with flag2, 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 flag3to re-fire lighting/neighbour updates; non-air with flag2), 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) {}}Spawn points
Section titled “Spawn points”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.
ArenaStore
Section titled “ArenaStore”Named, restart-durable arena presets, one set per game id.
On disk
Section titled “On disk”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 default preset
Section titled “The default preset”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.
Jar-bundled presets
Section titled “Jar-bundled presets”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}SchematicIo
Section titled “SchematicIo”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.
| Aspect | Coverage |
|---|---|
| Read | Sponge 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. |
| Write | Sponge v3, blocks only. Round-trips through the reader and is readable by WorldEdit. |
| Block entities / entities / biomes | Ignored (blocks only). A chest pastes empty, a spawner inert. LoadResult.ignoredEntities() is true when the file carried such data. |
| Unknown blocks | Unresolvable 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) {}}ArenaMarkers
Section titled “ArenaMarkers”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.
Roles (colour → role)
Section titled “Roles (colour → role)”| Glazed terracotta colour | Role |
|---|---|
| Lime | SPAWN |
| Red | FINISH (doubles as the team-B seed in tug-of-war) |
| Yellow | DOLL (doubles as the rope centre / caller anchor) |
| Magenta | ROOM_SEED |
| Blue | TEAM_A_SEED |
| Light blue | TEAM_A_SPAWN |
| Pink | TEAM_B_SPAWN |
| Orange | TILE_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 }}Related
Section titled “Related”- Regions, the box fills behind
/set,/walls,/replace. - World settings
- Void world helper
- Instance manager