diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 516175135..053b45b2b 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -47,13 +47,11 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Data; import art.arcane.iris.spi.PlatformBlockState; -import java.util.Arrays; -import java.util.HashMap; import java.util.Map; public class IrisCarveModifier extends EngineAssignedModifier { private static final byte LIQUID_FLUID = 1; - private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(CarveScratch::new); + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(IrisCarveScratch::new); private static final int CAVE_BIOME_BLEND_RADIUS = 3; private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4; private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8; @@ -72,15 +70,15 @@ public class IrisCarveModifier extends EngineAssignedModifier output, boolean multicore, ChunkContext context) { - PrecisionStopwatch p = PrecisionStopwatch.start(); + PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start(); Mantle mantle = getEngine().getMantle().getMantle(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048); - CarveScratch scratch = SCRATCH.get(); + IrisCarveScratch scratch = SCRATCH.get(); scratch.reset(); - PackedWallBuffer walls = scratch.walls; - ColumnMask[] columnMasks = scratch.columnMasks; - ColumnMask[] boundaryMasks = scratch.boundaryMasks; + CarveWallBuffer walls = scratch.walls; + CarveColumnMask[] columnMasks = scratch.columnMasks; + CarveColumnMask[] boundaryMasks = scratch.boundaryMasks; MatterCavern[] boundaryCaverns = scratch.boundaryCaverns; int[] surfaceHeights = scratch.surfaceHeights; Map customBiomeCache = scratch.customBiomeCache; @@ -102,13 +100,13 @@ public class IrisCarveModifier extends EngineAssignedModifier mc = mantle.getChunk(x, z).use(); + MantleChunk mantleChunk = mantle.getChunk(x, z).use(); try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); - final int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); - final int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); - mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> { - if (c == null) { + int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight(); + mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> { + if (cavern == null) { return; } @@ -125,15 +123,15 @@ public class IrisCarveModifier extends EngineAssignedModifier { - int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(x); - int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(z); + int worldX = rx + chunkBlockX; + int worldZ = rz + chunkBlockZ; String customBiome = cavern.getCustomBiome(); IrisBiome biome = customBiome.isEmpty() ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState) @@ -181,7 +179,7 @@ public class IrisCarveModifier extends EngineAssignedModifier= output.getHeight()) { + continue; + } + + int belowY = surfaceY - 1; + if (!columnMasks[columnIndex].contains(belowY) && !boundaryMasks[columnIndex].contains(belowY)) { + continue; + } + + int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int localZ = columnIndex & 15; + PlatformBlockState surface = output.getRaw(localX, surfaceY, localZ); + PlatformBlockState below = output.getRaw(localX, belowY, localZ); + if (isUnsupportedSurfaceOre(surface, below)) { + output.setRaw(localX, surfaceY, localZ, AIR); + } + } } finally { getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); } } finally { - getEngine().getMetrics().getCave().put(p.getMilliseconds()); - mc.release(); + getEngine().getMetrics().getCave().put(caveStopwatch.getMilliseconds()); + mantleChunk.release(); } } @@ -219,6 +238,10 @@ public class IrisCarveModifier extends EngineAssignedModifier mc, PackedWallBuffer walls, ColumnMask[] columnMasks) { + private void addInternalWallsFromMantle(MantleChunk mc, CarveWallBuffer walls, CarveColumnMask[] columnMasks) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { - ColumnMask columnMask = columnMasks[columnIndex]; + CarveColumnMask columnMask = columnMasks[columnIndex]; if (columnMask.isEmpty()) { continue; } @@ -295,8 +318,8 @@ public class IrisCarveModifier extends EngineAssignedModifier mantle, MantleChunk mc, - PackedWallBuffer walls, - ColumnMask[] boundaryMasks, + CarveWallBuffer walls, + CarveColumnMask[] boundaryMasks, MatterCavern[] boundaryCaverns, int chunkX, int chunkZ, @@ -344,8 +367,8 @@ public class IrisCarveModifier extends EngineAssignedModifier mc, MantleChunk neighborChunk, - PackedWallBuffer walls, - ColumnMask[] boundaryMasks, + CarveWallBuffer walls, + CarveColumnMask[] boundaryMasks, MatterCavern[] boundaryCaverns, int localX, int yy, @@ -382,7 +405,7 @@ public class IrisCarveModifier extends EngineAssignedModifier output, MantleChunk mc, Mantle mantle, - ColumnMask columnMask, + CarveColumnMask columnMask, int columnIndex, int chunkX, int chunkZ, @@ -408,16 +431,14 @@ public class IrisCarveModifier extends EngineAssignedModifier= 0) { - if (y >= 0 && y <= getEngine().getHeight()) { + if (y <= getEngine().getHeight()) { if (y == buf + 1) { buf = y; zone.ceiling = buf; - } else if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache); - zone = new CaveZone(); - zone.setFloor(y); - buf = y; } else { + if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache); + } zone = new CaveZone(); zone.setFloor(y); buf = y; @@ -434,7 +455,7 @@ public class IrisCarveModifier extends EngineAssignedModifier output, - ColumnMask boundaryMask, + CarveColumnMask boundaryMask, MatterCavern cavern, int columnIndex, int chunkX, @@ -695,251 +716,6 @@ public class IrisCarveModifier extends EngineAssignedModifier= resizeAt) { - resize(); - } - return; - } - - if (existingKey == key) { - values[index] = value; - return; - } - - index = (index + 1) & mask; - } - } - - private void forEach(PackedWallConsumer consumer) { - for (int index = 0; index < keys.length; index++) { - int key = keys[index]; - if (key == EMPTY_KEY) { - continue; - } - - MatterCavern cavern = values[index]; - if (cavern == null) { - continue; - } - - consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern); - } - } - - private void clear() { - Arrays.fill(keys, EMPTY_KEY); - Arrays.fill(values, null); - size = 0; - } - - private void resize() { - int[] oldKeys = keys; - MatterCavern[] oldValues = values; - int nextCapacity = oldKeys.length << 1; - keys = new int[nextCapacity]; - Arrays.fill(keys, EMPTY_KEY); - values = new MatterCavern[nextCapacity]; - mask = nextCapacity - 1; - resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR)); - size = 0; - - for (int index = 0; index < oldKeys.length; index++) { - int key = oldKeys[index]; - if (key == EMPTY_KEY) { - continue; - } - - MatterCavern value = oldValues[index]; - if (value == null) { - continue; - } - - reinsert(key, value); - } - } - - private void reinsert(int key, MatterCavern value) { - int index = mix(key) & mask; - while (keys[index] != EMPTY_KEY) { - index = (index + 1) & mask; - } - - keys[index] = key; - values[index] = value; - size++; - } - - private int pack(int x, int y, int z) { - return (y << 8) | PowerOfTwoCoordinates.packLocal16(x & 15, z & 15); - } - - private int unpackX(int key) { - return PowerOfTwoCoordinates.unpackLocal16X(key & 255); - } - - private int unpackY(int key) { - return key >> 8; - } - - private int unpackZ(int key) { - return PowerOfTwoCoordinates.unpackLocal16Z(key); - } - - private int mix(int value) { - int mixed = value * 0x9E3779B9; - return mixed ^ (mixed >>> 16); - } - } - - private static final class CarveScratch { - private final ColumnMask[] columnMasks = new ColumnMask[256]; - private final ColumnMask[] boundaryMasks = new ColumnMask[256]; - private final MatterCavern[] boundaryCaverns = new MatterCavern[256]; - private final int[] surfaceHeights = new int[256]; - private final PackedWallBuffer walls = new PackedWallBuffer(512); - private final Map customBiomeCache = new HashMap<>(); - private int[] upperSurfaceHeights; - private boolean customCaveBiomePresent; - - private CarveScratch() { - for (int index = 0; index < columnMasks.length; index++) { - columnMasks[index] = new ColumnMask(); - boundaryMasks[index] = new ColumnMask(); - } - } - - private int[] getOrCreateUpperSurfaceHeights() { - if (upperSurfaceHeights == null) { - upperSurfaceHeights = new int[256]; - } - return upperSurfaceHeights; - } - - private void reset() { - for (int index = 0; index < columnMasks.length; index++) { - columnMasks[index].clear(); - boundaryMasks[index].clear(); - boundaryCaverns[index] = null; - } - walls.clear(); - customBiomeCache.clear(); - customCaveBiomePresent = false; - } - } - - private static final class ColumnMask { - private long[] words = new long[8]; - private int maxWord = -1; - - private void add(int y) { - if (y < 0) { - return; - } - - int wordIndex = y >> 6; - if (wordIndex >= words.length) { - words = Arrays.copyOf(words, Math.max(words.length << 1, wordIndex + 1)); - } - - words[wordIndex] |= 1L << (y & 63); - if (wordIndex > maxWord) { - maxWord = wordIndex; - } - } - - private int nextSetBit(int fromBit) { - if (maxWord < 0) { - return -1; - } - - int startBit = Math.max(0, fromBit); - int wordIndex = startBit >> 6; - if (wordIndex > maxWord) { - return -1; - } - - long word = words[wordIndex] & (-1L << (startBit & 63)); - while (true) { - if (word != 0L) { - return (wordIndex << 6) + Long.numberOfTrailingZeros(word); - } - - wordIndex++; - if (wordIndex > maxWord) { - return -1; - } - word = words[wordIndex]; - } - } - - private boolean isEmpty() { - return maxWord < 0; - } - - private boolean contains(int y) { - if (y < 0) { - return false; - } - - int wordIndex = y >> 6; - if (wordIndex > maxWord) { - return false; - } - - return (words[wordIndex] & (1L << (y & 63))) != 0L; - } - - private void clear() { - if (maxWord < 0) { - return; - } - - for (int index = 0; index <= maxWord; index++) { - words[index] = 0L; - } - maxWord = -1; - } - } - - @FunctionalInterface - private interface PackedWallConsumer { - void accept(int x, int y, int z, MatterCavern cavern); - } - @Data public static class CaveZone { private int ceiling = -1; diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java new file mode 100644 index 000000000..9b4470ca9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java @@ -0,0 +1,264 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.engine.modifier; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.math.PowerOfTwoCoordinates; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +final class IrisCarveScratch { + final CarveColumnMask[] columnMasks = new CarveColumnMask[256]; + final CarveColumnMask[] boundaryMasks = new CarveColumnMask[256]; + final MatterCavern[] boundaryCaverns = new MatterCavern[256]; + final int[] surfaceHeights = new int[256]; + final CarveWallBuffer walls = new CarveWallBuffer(512); + final Map customBiomeCache = new HashMap<>(); + int[] upperSurfaceHeights; + boolean customCaveBiomePresent; + + IrisCarveScratch() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index] = new CarveColumnMask(); + boundaryMasks[index] = new CarveColumnMask(); + } + } + + int[] getOrCreateUpperSurfaceHeights() { + if (upperSurfaceHeights == null) { + upperSurfaceHeights = new int[256]; + } + return upperSurfaceHeights; + } + + void reset() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index].clear(); + boundaryMasks[index].clear(); + boundaryCaverns[index] = null; + } + walls.clear(); + customBiomeCache.clear(); + customCaveBiomePresent = false; + } +} + +final class CarveColumnMask { + private long[] words = new long[8]; + private int maxWord = -1; + + void add(int y) { + if (y < 0) { + return; + } + + int wordIndex = y >> 6; + if (wordIndex >= words.length) { + words = Arrays.copyOf(words, Math.max(words.length << 1, wordIndex + 1)); + } + + words[wordIndex] |= 1L << (y & 63); + if (wordIndex > maxWord) { + maxWord = wordIndex; + } + } + + int nextSetBit(int fromBit) { + if (maxWord < 0) { + return -1; + } + + int startBit = Math.max(0, fromBit); + int wordIndex = startBit >> 6; + if (wordIndex > maxWord) { + return -1; + } + + long word = words[wordIndex] & (-1L << (startBit & 63)); + while (true) { + if (word != 0L) { + return (wordIndex << 6) + Long.numberOfTrailingZeros(word); + } + + wordIndex++; + if (wordIndex > maxWord) { + return -1; + } + word = words[wordIndex]; + } + } + + boolean isEmpty() { + return maxWord < 0; + } + + boolean contains(int y) { + if (y < 0) { + return false; + } + + int wordIndex = y >> 6; + if (wordIndex > maxWord) { + return false; + } + + return (words[wordIndex] & (1L << (y & 63))) != 0L; + } + + void clear() { + if (maxWord < 0) { + return; + } + + for (int index = 0; index <= maxWord; index++) { + words[index] = 0L; + } + maxWord = -1; + } +} + +final class CarveWallBuffer { + private static final int EMPTY_KEY = -1; + private static final double LOAD_FACTOR = 0.75D; + + private int[] keys; + private MatterCavern[] values; + private int mask; + private int resizeAt; + private int size; + + CarveWallBuffer(int expectedSize) { + int capacity = 1; + int minimumCapacity = Math.max(8, expectedSize); + while (capacity < minimumCapacity) { + capacity <<= 1; + } + + keys = new int[capacity]; + Arrays.fill(keys, EMPTY_KEY); + values = new MatterCavern[capacity]; + mask = capacity - 1; + resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR)); + } + + void put(int x, int y, int z, MatterCavern value) { + int key = pack(x, y, z); + int index = mix(key) & mask; + + while (true) { + int existingKey = keys[index]; + if (existingKey == EMPTY_KEY) { + keys[index] = key; + values[index] = value; + size++; + if (size >= resizeAt) { + resize(); + } + return; + } + + if (existingKey == key) { + values[index] = value; + return; + } + + index = (index + 1) & mask; + } + } + + void forEach(Consumer consumer) { + for (int index = 0; index < keys.length; index++) { + int key = keys[index]; + if (key == EMPTY_KEY) { + continue; + } + + MatterCavern cavern = values[index]; + if (cavern != null) { + consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern); + } + } + } + + void clear() { + Arrays.fill(keys, EMPTY_KEY); + Arrays.fill(values, null); + size = 0; + } + + private void resize() { + int[] oldKeys = keys; + MatterCavern[] oldValues = values; + int nextCapacity = oldKeys.length << 1; + keys = new int[nextCapacity]; + Arrays.fill(keys, EMPTY_KEY); + values = new MatterCavern[nextCapacity]; + mask = nextCapacity - 1; + resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR)); + size = 0; + + for (int index = 0; index < oldKeys.length; index++) { + int key = oldKeys[index]; + MatterCavern value = oldValues[index]; + if (key != EMPTY_KEY && value != null) { + reinsert(key, value); + } + } + } + + private void reinsert(int key, MatterCavern value) { + int index = mix(key) & mask; + while (keys[index] != EMPTY_KEY) { + index = (index + 1) & mask; + } + + keys[index] = key; + values[index] = value; + size++; + } + + private int pack(int x, int y, int z) { + return (y << 8) | PowerOfTwoCoordinates.packLocal16(x & 15, z & 15); + } + + private int unpackX(int key) { + return PowerOfTwoCoordinates.unpackLocal16X(key & 255); + } + + private int unpackY(int key) { + return key >> 8; + } + + private int unpackZ(int key) { + return PowerOfTwoCoordinates.unpackLocal16Z(key); + } + + private int mix(int value) { + int mixed = value * 0x9E3779B9; + return mixed ^ (mixed >>> 16); + } + + @FunctionalInterface + interface Consumer { + void accept(int x, int y, int z, MatterCavern cavern); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 69ad518aa..68cdef1ea 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -161,10 +161,10 @@ public class IrisBiome extends IrisRegistrant implements IRare { private IrisGeneratorStyle childStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); @RegistryListResource(IrisBiome.class) @ArrayType(min = 1, type = String.class) - @Desc("List any biome names (file names without.json) here as children. Portions of this biome can sometimes morph into their children. Iris supports cyclic relationships such as A > B > A > B. Iris will stop checking 9 biomes down the tree.") + @Desc("List any biome names (file names without.json) here as children. Portions of this biome can sometimes morph into their children. Iris supports cyclic relationships such as A > B > A > B. Iris will stop checking 4 biomes down the tree.") private KList children = new KList<>(); @RegistryListResource(IrisBiome.class) - @Desc("The carving biome. If specified the biome will be used when under a carving instead of this current biome.") + @Desc("Registers the referenced biome with this pack's reachable biome set. It is NOT applied as a substitute under carvings; cave biomes come from the region's caveBiomes list.") private String carvingBiome = ""; @MinNumber(0) @MaxNumber(256) @@ -179,10 +179,10 @@ public class IrisBiome extends IrisRegistrant implements IRare { @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") private KList layers = new KList().qadd(new IrisBiomePaletteLayer()); @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") + @Desc("Layers of materials placed on cave ceilings in this biome, indexed upward from the ceiling surface. Must not have more entries than layers, whose height generators it reuses.") private KList caveCeilingLayers = new KList().qadd(new IrisBiomePaletteLayer()); @ArrayType(type = IrisBiomePaletteLayer.class) - @Desc("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.") + @Desc("Layers of materials filling the water column of sea biomes, indexed downward from the water surface. Anything below the last layer is filled with the dimension fluid palette, not stone.") private KList seaLayers = new KList<>(); @ArrayType(min = 1, type = IrisDecorator.class) @Desc("Decorators are used for things like tall grass, bisected flowers, and even kelp or cactus (random heights)") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java index e0412051c..52dd71a38 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustomCategory.java @@ -22,21 +22,54 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("The custom biome category. Vanilla asks for this, basically what represents your biome closest?") public enum IrisBiomeCustomCategory { + @Desc("Tags the generated datapack biome as vanilla category 'beach' (shoreline biomes).") beach, + + @Desc("Tags the generated datapack biome as vanilla category 'desert' (hot, dry sand biomes).") desert, + + @Desc("Tags the generated datapack biome as vanilla category 'extreme_hills' (mountain biomes).") extreme_hills, + + @Desc("Tags the generated datapack biome as vanilla category 'forest' (tree-dense temperate biomes).") forest, + + @Desc("Tags the generated datapack biome as vanilla category 'icy' (snow and ice biomes).") icy, + + @Desc("Tags the generated datapack biome as vanilla category 'jungle'.") jungle, + + @Desc("Tags the generated datapack biome as vanilla category 'mesa' (badlands).") mesa, + + @Desc("Tags the generated datapack biome as vanilla category 'mushroom' (mushroom fields).") mushroom, + + @Desc("Tags the generated datapack biome as vanilla category 'nether'.") nether, + + @Desc("Tags the generated datapack biome as vanilla category 'none' (no classification).") none, + + @Desc("Tags the generated datapack biome as vanilla category 'ocean'.") ocean, + + @Desc("Tags the generated datapack biome as vanilla category 'plains'. This is the default when category is omitted.") plains, + + @Desc("Tags the generated datapack biome as vanilla category 'river'.") river, + + @Desc("Tags the generated datapack biome as vanilla category 'savanna'.") savanna, + + @Desc("Tags the generated datapack biome as vanilla category 'swamp'.") swamp, + + @Desc("Tags the generated datapack biome as vanilla category 'taiga'.") taiga, + + @Desc("Tags the generated datapack biome as vanilla category 'the_end'.") the_end } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java index 4087c28a6..693f98eba 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomePaletteLayer.java @@ -60,7 +60,7 @@ public class IrisBiomePaletteLayer { @Desc("The max thickness of this layer") private int maxHeight = 1; - @Desc("If set, this layer will change size depending on the slope. If in bounds, the layer will get larger (taller) the closer to the center of this slope clip it is. If outside of the slipe's bounds, this layer will not show.") + @Desc("If set, this layer only shows where the terrain slope is within these bounds; outside them the layer is skipped entirely. Thickness is not scaled by slope.") private IrisSlopeClip slopeCondition = new IrisSlopeClip(); @MinNumber(0.0001) @Desc("The terrain zoom mostly for zooming in on a wispy palette") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java index b812071aa..0368457cb 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveFieldModule.java @@ -28,7 +28,7 @@ public class IrisCaveFieldModule { @Desc("Threshold offset applied to this layer before blending.") private double threshold = 0; - @Desc("Vertical bounds where this module can contribute.") + @Desc("Vertical bounds where this module can contribute, in engine-local Y where 0 is the bottom of the dimension, not world Y.") private IrisRange verticalRange = new IrisRange(0, 384); @Desc("Invert this module before weighting.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java index c1ef5f025..39aac502c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java @@ -21,7 +21,7 @@ public class IrisCaveProfile { @Desc("Enable profile-driven cave carving.") private boolean enabled = false; - @Desc("Global vertical bounds for profile cave carving.") + @Desc("Global vertical bounds for profile cave carving, in engine-local Y where 0 is the bottom of the dimension, not world Y.") private IrisRange verticalRange = new IrisRange(0, 384); @MinNumber(0) @@ -73,7 +73,7 @@ public class IrisCaveProfile { @MinNumber(2) @MaxNumber(4) - @Desc("Horizontal adaptive predictor grid step used while classifying cave density planes.") + @Desc("Horizontal adaptive predictor tuning. The carver always samples the predictor grid at a fixed step of 8; values below 8 only widen the ambiguity margin slightly and do not tighten the grid.") private int adaptiveSampleStep = 2; @MinNumber(0) @@ -119,7 +119,7 @@ public class IrisCaveProfile { @Desc("Default cave anchor mode for cave-only object placement.") private IrisCaveAnchorMode defaultObjectAnchor = IrisCaveAnchorMode.FLOOR; - @Desc("Default placement mode for cave objects. Stilt modes tile the object base block down to the cave floor surface. FAST_MIN_STILT is recommended for cave objects to prevent floating.") + @Desc("Default placement mode for cave objects. Only applies to placements that are still on the default CENTER_HEIGHT mode; an explicit mode on the placement always wins. Stilt modes tile the object base block down to the cave floor surface. FAST_MIN_STILT is recommended for cave objects to prevent floating.") private ObjectPlaceMode defaultObjectPlaceMode = null; @MinNumber(1) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java index 0ab933dcc..08377ba22 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCoral.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.coral.CoralGenerator; @@ -94,12 +95,14 @@ public class IrisCoral implements IrisProceduralPlacement { @Required @Desc("The structural coral block (the living wood of the reef), e.g. minecraft:tube_coral_block. Ignored when blockPalette is set.") + @RegistryListBlockType private String block = "minecraft:tube_coral_block"; @Desc("A noise-driven palette for the structural coral block. When set this overrides the single block, letting the reef mix tube/brain/bubble/fire/horn coral_block tones across its body.") private IrisMaterialPalette blockPalette = null; @Desc("Optional tip block placed at branch tips and at the very top of the structure, e.g. coral fans or minecraft:sea_pickle. Ignored when tipPalette is set. Null disables tips.") + @RegistryListBlockType private String tipBlock = null; @Desc("A noise-driven palette for the tip block. When set this overrides the single tipBlock, letting tips mix fan / pickle decorations.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java index 7ee29c393..52ed68c4d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCrystal.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.crystal.CrystalGenerator; @@ -91,12 +92,14 @@ public class IrisCrystal implements IrisProceduralPlacement { @Required @Desc("The primary crystal shard block, e.g. minecraft:amethyst_block. Ignored when blockPalette is set.") + @RegistryListBlockType private String block = "minecraft:amethyst_block"; @Desc("A noise-driven palette for the crystal shards (a prismatic mix, e.g. amethyst_block, calcite, tinted_glass). When set this overrides the single block, letting the shards mix blocks by noise. Palette wins.") private IrisMaterialPalette blockPalette = null; @Desc("Optional block placed at the very tip of each shard for a different colored or sparkling point (e.g. minecraft:amethyst_cluster or glowstone). Ignored when tipPalette is set. If unset and glow is true, a light-emitting block is sprinkled among the tips instead.") + @RegistryListBlockType private String tipBlock = null; @Desc("A noise-driven palette for the shard tips. When set this overrides the single tipBlock. Palette wins.") @@ -111,9 +114,11 @@ public class IrisCrystal implements IrisProceduralPlacement { private boolean glow = false; @Desc("The light-emitting block sprinkled among the tips when glow is true and no tip block is configured.") + @RegistryListBlockType private String glowBlock = "minecraft:glowstone"; @Desc("Optional block for the budding base blob the shards grow from, e.g. minecraft:budding_amethyst or calcite. Ignored when basePalette is set. If unset, the base is built from the primary shard block.") + @RegistryListBlockType private String baseBlock = "minecraft:budding_amethyst"; @Desc("A noise-driven palette for the budding base blob. When set this overrides the single baseBlock. Palette wins.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java index 41204f865..0942302f4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java @@ -55,12 +55,12 @@ public class IrisDepositGenerator { @Required @MinNumber(0) @MaxNumber(8192) - @Desc("The minimum height this deposit can generate at") + @Desc("The minimum height this deposit can generate at, in engine-local Y where 0 is the bottom of the dimension, not world Y.") private int minHeight = 1; @Required @MinNumber(0) @MaxNumber(8192) - @Desc("The maximum height this deposit can generate at") + @Desc("The maximum height this deposit can generate at, in engine-local Y where 0 is the bottom of the dimension, not world Y. Clump centers are additionally clamped to stay below the terrain surface.") private int maxHeight = 75; @Required @MinNumber(0) @@ -84,15 +84,15 @@ public class IrisDepositGenerator { private int minPerChunk = 0; @MinNumber(0) @MaxNumber(1) - @Desc("The change of the deposit spawning in a chunk") + @Desc("The chance of the deposit spawning in a chunk") private double spawnChance = 1; @MinNumber(0) @MaxNumber(1) - @Desc("The change of the a clump spawning in a chunk") + @Desc("The chance of each individual clump spawning in a chunk") private double perClumpSpawnChance = 1; @Required @ArrayType(min = 1, type = IrisBlockData.class) - @Desc("The palette of blocks to be used in this deposit generator") + @Desc("The palette of blocks to be used in this deposit generator. Each entry is picked uniformly; the per-entry weight field is ignored here.") private KList palette = new KList<>(); @MinNumber(1) @MaxNumber(64) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index a17a6302d..75859453f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -101,7 +101,7 @@ public class IrisDimension extends IrisRegistrant { private int logicalHeight = 256; @Desc("If set to true, Iris will remove chunks to allow visualizing cross sections of chunks easily") private boolean debugChunkCrossSections = false; - @Desc("Vertically split up the biome palettes with 3 air blocks in between to visualize them") + @Desc("Vertically split up the biome palettes with barrier blocks in between to visualize them. The gap size comes from explodeBiomePaletteSize.") private boolean explodeBiomePalettes = false; @Desc("Studio Mode for testing different parts of the world") private StudioMode studioMode = StudioMode.NORMAL; @@ -137,13 +137,13 @@ public class IrisDimension extends IrisRegistrant { private IrisGeneratorStyle regionStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); @Desc("The placement style of land/sea") private IrisGeneratorStyle continentalStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") + @Desc("The noise style used to select land biomes within a region") private IrisGeneratorStyle landBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") + @Desc("The noise style used to select shore biomes within a region") private IrisGeneratorStyle shoreBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") + @Desc("The noise style used to select sea biomes within a region") private IrisGeneratorStyle seaBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); - @Desc("The placement style of biomes") + @Desc("The noise style used to select cave biomes within a region") private IrisGeneratorStyle caveBiomeStyle = NoiseStyle.CELLULAR_IRIS_DOUBLE.style(); @Desc("Instead of filling objects with air, fills them with cobweb so you can see them") private boolean debugSmartBore = false; @@ -170,7 +170,7 @@ public class IrisDimension extends IrisRegistrant { @MaxNumber(16) @Desc("Minimum surface-support buffer, in blocks, applied to every surface object placement in this dimension. A placement may ask for more but never less.") private int objectSurfaceSupportBuffer = 2; - @Desc("forceConvertTo320Height") + @Desc("Unused. This field is not read by the engine.") private Boolean forceConvertTo320Height = false; @Desc("The world environment") private IrisEnvironment environment = IrisEnvironment.NORMAL; @@ -182,9 +182,9 @@ public class IrisDimension extends IrisRegistrant { @Required @MinNumber(0) @MaxNumber(1024) - @Desc("The fluid height for this dimension") + @Desc("The fluid (sea level) height for this dimension, in world Y. Water fills every sea column up to this height, so 63 puts the water surface at world Y 63.") private int fluidHeight = 63; - @Desc("Define the min and max Y bounds of this dimension. Please keep in mind that Iris internally generates from 0 to (max - min). \n\nFor example at -64 to 320, Iris is internally generating to 0 to 384, then on outputting chunks, it shifts it down by the min height (64 blocks). The default is -64 to 320. \n\nThe fluid height is placed at (fluid height + min height). So a fluid height of 63 would actually show up in the world at 1.") + @Desc("Define the min and max Y bounds of this dimension. Please keep in mind that Iris internally generates from 0 to (max - min). \n\nFor example at -64 to 320, Iris is internally generating to 0 to 384, then on outputting chunks, it shifts it down by the min height (64 blocks). The default is -64 to 320. \n\nfluidHeight stays in world Y: the engine converts it internally by subtracting the min height, so a fluid height of 63 shows up in the world at Y 63.") private IrisRange dimensionHeight = new IrisRange(-64, 320); @Desc("Define options for this dimension") private IrisDimensionTypeOptions dimensionOptions = new IrisDimensionTypeOptions(); @@ -259,13 +259,13 @@ public class IrisDimension extends IrisRegistrant { private KList overlayNoise = new KList<>(); @MinNumber(0.0001) @MaxNumber(512) - @Desc("The rock zoom mostly for zooming in on a wispy palette") + @Desc("Unused. This field is not read by the engine; rock palette styling comes from rockPalette itself.") private double rockZoom = 5; @Desc("The palette of blocks for 'stone'") private IrisMaterialPalette rockPalette = new IrisMaterialPalette().qclear().qadd("stone"); @Desc("The dimension fluid block palette used for ocean columns and cave aquifers.") private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water"); - @Desc("Prevent cartographers to generate explorer maps (Iris worlds only)\nONLY TOUCH IF YOUR SERVER CRASHES WHILE GENERATING EXPLORER MAPS") + @Desc("Unused. This field is not read by the engine and no longer affects explorer maps.") private boolean disableExplorerMaps = false; @Desc("Collection of ores to be generated") @ArrayType(type = IrisOreGenerator.class, min = 1) @@ -282,7 +282,7 @@ public class IrisDimension extends IrisRegistrant { private KList datapackImports = new KList<>(); @MinNumber(0) @MaxNumber(318) - @Desc("The Subterrain Fluid Layer Height") + @Desc("The subterranean fluid (cave lava) layer height, in engine-local Y where 0 is the bottom of the dimension, not world Y. With the default -64..320 height, 8 means world Y -56.") private int caveLavaHeight = 8; @RegistryListFunction(ComponentFlagFunction.class) @ArrayType(type = String.class) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java index 7310db226..1622f2530 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntity.java @@ -149,7 +149,7 @@ public class IrisEntity extends IrisRegistrant { @ArrayType(min = 1, type = IrisAttributeModifier.class) private KList attributes = new KList<>(); - @Desc("Loot tables for drops") + @Desc("Loot tables for drops. Only the tables list is honored for entities; mode and multiplier are ignored, and the tables replace vanilla drops entirely.") private IrisLootReference loot = new IrisLootReference(); @Desc("If specified, this entity will be leashed by this entity. I.e. THIS ENTITY Leashed by SPECIFIED. This has no effect on EnderDragons, Withers, Players, or Bats.Non-living entities excluding leashes will not persist as leashholders.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java index 64cb9e63a..6c0df7bdf 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java @@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.RegistryListResource; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.util.project.noise.CNG; @@ -270,6 +271,7 @@ public class IrisFloatingChildBiomes implements IRare { private Integer localFluidHeight = null; @Desc("Block used for the internal water pool when localFluidHeight is positive.") + @RegistryListBlockType private String fluidBlock = "minecraft:water"; @Desc("When true, the target biome's decorators apply to the island's top surface.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java index 1f8ae80c6..6b662459a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFormation.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.formation.FormationGenerator; @@ -96,12 +97,14 @@ public class IrisFormation implements IrisProceduralPlacement { @Required @Desc("The main rock block, e.g. minecraft:stone. Ignored when blockPalette is set.") + @RegistryListBlockType private String block = "minecraft:stone"; @Desc("A noise-driven palette for the main rock body. When set this overrides the single block, letting the body mix blocks by noise.") private IrisMaterialPalette blockPalette = null; @Desc("Optional caprock block placed on the top crown of the formation (and the wide overhanging cap for HOODOO). Ignored when capPalette is set. When null and capPalette is unset, the formation uses its main rock everywhere.") + @RegistryListBlockType private String capBlock = null; @Desc("A noise-driven palette for the caprock. When set this overrides the single capBlock.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java index 1c7e3f549..d6613b5b3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFungus.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.fungi.FungusGenerator; @@ -88,6 +89,7 @@ public class IrisFungus implements IrisProceduralPlacement { @Required @Desc("The stem block, e.g. minecraft:mushroom_stem. Ignored when stemPalette is set.") + @RegistryListBlockType private String stem = "minecraft:mushroom_stem"; @Desc("A noise-driven palette for the stem. When set this overrides the single stem block, letting the stem mix blocks by noise.") @@ -95,6 +97,7 @@ public class IrisFungus implements IrisProceduralPlacement { @Required @Desc("The cap block, e.g. minecraft:red_mushroom_block. Ignored when capPalette is set.") + @RegistryListBlockType private String cap = "minecraft:red_mushroom_block"; @Desc("A noise-driven palette for the cap. When set this overrides the single cap block, letting the cap mix blocks by noise.") @@ -157,6 +160,7 @@ public class IrisFungus implements IrisProceduralPlacement { private double capOverhang = 2; @Desc("Optional block forming the gill layer on the underside of the cap (gills or a glow layer), e.g. minecraft:brown_mushroom_block or minecraft:shroomlight. Ignored when gillPalette is set or left null.") + @RegistryListBlockType private String gillBlock = null; @Desc("A noise-driven palette for the underside gill layer. When set this overrides the single gillBlock.") @@ -168,6 +172,7 @@ public class IrisFungus implements IrisProceduralPlacement { private double gillChance = 0.85; @Desc("Optional block speckled across the top of the cap by noise (white toadstool dots, warts, glowing spots), e.g. minecraft:bone_block or minecraft:white_concrete. Ignored when spotPalette is set or left null.") + @RegistryListBlockType private String spotBlock = null; @Desc("A noise-driven palette for the cap top spots. When set this overrides the single spotBlock.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java index b3572ea68..69689acc6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java @@ -58,7 +58,7 @@ public class IrisGeneratorStyle { new ConcurrentLinkedHashMap.Builder() .maximumWeightedCapacity(GENERATOR_CACHE_SIZE) .build(); - @Desc("The chance is 1 in CHANCE per interval") + @Desc("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.") private NoiseStyle style = NoiseStyle.FLAT; @Desc("If set above 0, this style will be cellularized") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java index 09c566fe0..7648e2b38 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawHeightmap.java @@ -4,12 +4,27 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("Controls whether a configured native jigsaw projects its start onto a Minecraft heightmap.") public enum IrisJigsawHeightmap { + @Desc("Keeps the registered structure's own heightmap projection exactly as the vanilla, datapack, or mod definition declares it.") SOURCE, + + @Desc("Removes heightmap projection entirely, so the start piece lands at the structure's literal start height instead of following terrain.") NONE, + + @Desc("Drops the start piece onto the terrain top counting water as solid, so ocean starts sit on the water surface.") WORLD_SURFACE_WG, + + @Desc("Identical to WORLD_SURFACE_WG in Iris worlds: the start piece rides the terrain top including water.") WORLD_SURFACE, + + @Desc("Drops the start piece onto solid terrain, ignoring water, so ocean starts sit on the seabed.") OCEAN_FLOOR_WG, + + @Desc("Identical to OCEAN_FLOOR_WG in Iris worlds: the start piece sits on solid ground beneath any water.") OCEAN_FLOOR, + + @Desc("Behaves like WORLD_SURFACE in Iris worlds; the start piece sits on the terrain top including water.") MOTION_BLOCKING, + + @Desc("Behaves like WORLD_SURFACE in Iris worlds; Iris height queries have no leaf layer to skip.") MOTION_BLOCKING_NO_LEAVES } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java index 11cf8db6f..8863ff7f7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawLiquidSettings.java @@ -4,7 +4,12 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("Controls waterlogging behavior for a configured native jigsaw.") public enum IrisJigsawLiquidSettings { + @Desc("Keeps the registered structure's own liquid setting; vanilla defaults to applying waterlogging when the definition omits it.") SOURCE, + + @Desc("Places pieces dry: existing water is displaced and waterloggable blocks stay unwaterlogged, leaving air-filled interiors underwater.") IGNORE_WATERLOGGING, + + @Desc("Re-applies pre-existing water into placed waterloggable blocks and floods from neighboring sources, so underwater pieces come out waterlogged.") APPLY_WATERLOGGING } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java index 963503896..75986aa0d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisJigsawWorkcellArchetype.java @@ -4,11 +4,22 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("The orientation-independent connector shape represented by a planar jigsaw workcell.") public enum IrisJigsawWorkcellArchetype { + @Desc("Pieces with no horizontal connectors: sealed filler cells that terminate nothing and are excluded from the default piece pool.") BLANK(0), + + @Desc("One-connector dead-end pieces; the Studio bay seeds a single north connector and new projects mark these as terminal caps.") END(1), + + @Desc("Two-connector pass-through pieces with openings on opposite sides; the Studio bay seeds north and south connectors.") STRAIGHT(5), + + @Desc("Two-connector right-angle pieces with openings on adjacent sides; the Studio bay seeds north and east connectors.") CORNER(3), + + @Desc("Three-connector junction pieces; the Studio bay seeds north, east, and west connectors.") TEE(11), + + @Desc("Four-connector pieces open on all sides; new projects use this archetype as the structure's start piece.") CROSS(15); private final int canonicalMask; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java index c2e06ac55..58cb6b433 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLoot.java @@ -71,7 +71,7 @@ public class IrisLoot { public static final int MAX_AMOUNT = 64; private final transient AtomicCache dyeColorResolved = new AtomicCache<>(); - @Desc("The target inventory slot types to fill this loot with") + @Desc("The target inventory slot types to fill this loot with. World generation only ever fills STORAGE; the other values are inert during worldgen.") private InventorySlotType slotTypes = InventorySlotType.STORAGE; @MinNumber(1) @Desc("The sub rarity of this loot. Calculated after this loot table has been picked.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java index e61fd3726..2c2cb7da6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisLootReference.java @@ -42,7 +42,7 @@ public class IrisLootReference { public static final double MAX_MULTIPLIER = 16D; private final transient AtomicCache> tt = new AtomicCache<>(); - @Desc("Add = add on top of parent tables, Replace = clear first then add these. Clear = Remove all and dont add loot from this or parent.") + @Desc("ADD = add on top of parent tables. REPLACE and CLEAR both clear parent tables first, then add these. FALLBACK = only used when nothing else defined a table.") private IrisLootMode mode = IrisLootMode.ADD; @RegistryListResource(IrisLootTable.class) @ArrayType(min = 1, type = String.class) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java index d648f6f5b..a960bd0f7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java @@ -151,7 +151,7 @@ public class IrisObjectPlacement { private boolean bore = false; @Desc("Use a generator to warp the field of coordinates. Using simplex for example would make a square placement warp like a flag") private IrisGeneratorStyle warp = new IrisGeneratorStyle(NoiseStyle.FLAT); - @Desc("If the place mode is set to CENTER_HEIGHT_RIGID and you have an X/Z translation, Turning on translate center will also translate the center height check.") + @Desc("Unused. This field is not read by the placement engine.") private boolean translateCenter = false; @Desc("The placement mode") private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java index da4f58e38..f124c8e89 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java @@ -21,6 +21,8 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.Desc; +import art.arcane.iris.engine.object.annotations.MaxNumber; +import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; import lombok.AllArgsConstructor; @@ -32,18 +34,20 @@ import lombok.experimental.Accessors; @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor -@Desc("Ore Layer") +@Desc("Scatters single ore blocks through terrain by sampling a noise field at every block in a vertical band. Defined on dimensions, regions, or biomes; the most specific level wins.") @Data public class IrisOreGenerator { - @Desc("The palette of 'ore' generated") + @Desc("The ore blocks to place. An empty palette generates nothing, so this must be set for the generator to have any effect.") private IrisMaterialPalette palette = new IrisMaterialPalette().qclear(); - @Desc("The generator style for the 'ore'") + @Desc("The noise style sampled per block to decide where ore appears. STATIC gives independent random speckle; smoother styles give veiny clusters.") private IrisGeneratorStyle chanceStyle = new IrisGeneratorStyle(NoiseStyle.STATIC); - @Desc("Will ores generate on the surface of the terrain layer") + @Desc("When true this generator also replaces surface-layer blocks (it runs before layers and fluid); when false it only replaces underground rock.") private boolean generateSurface = false; - @Desc("Threshold for rate of generation") + @MinNumber(0) + @MaxNumber(1) + @Desc("Noise cutoff: a block becomes ore when the sampled noise is at or below this value, so higher means more ore. 0 disables, 1 replaces everything in range.") private double threshold = 0.5; - @Desc("Height limit (min, max)") + @Desc("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.") private IrisRange range = new IrisRange(30, 80); private transient AtomicCache chanceCache = new AtomicCache<>(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java index c851ff4c4..ac8d26dcb 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralTree.java @@ -24,6 +24,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.tree.ProceduralTreeGenerator; @@ -93,6 +94,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement { @Required @Desc("The trunk (log) block, e.g. minecraft:oak_log. Ignored when trunkPalette is set.") + @RegistryListBlockType private String trunk = "minecraft:oak_log"; @Desc("A noise-driven palette for the trunk. When set this overrides the single trunk block, letting the trunk mix blocks by noise.") @@ -100,6 +102,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement { @Required @Desc("The leaf block, e.g. minecraft:oak_leaves. Ignored when leavesPalette is set.") + @RegistryListBlockType private String leaves = "minecraft:oak_leaves"; @Desc("A noise-driven palette for the leaves. When set this overrides the single leaf block, letting the canopy mix leaf (or other) blocks by noise. Only blocks that are actually leaves get vanilla decay distances.") @@ -188,6 +191,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement { private int azimuthWhorlCount = 5; @Desc("Optional accent leaf block scattered through the canopy (blossoms, shroomlight).") + @RegistryListBlockType private String secondaryLeaves = null; @MinNumber(0) @@ -203,6 +207,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement { private IrisMaterialPalette secondaryLeavesPalette = null; @Desc("Optional secondary trunk block used over a height band (for color-banded trunks). Ignored when secondaryTrunkPalette is set.") + @RegistryListBlockType private String secondaryTrunk = null; @Desc("A noise-driven palette for the secondary trunk band. When set this overrides the single secondaryTrunk block.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 524059c96..9d22123e7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -90,7 +90,6 @@ public class IrisRegion extends IrisRegistrant implements IRare { @ArrayType(min = 1, type = IrisBlockDrops.class) @Desc("Define custom block drops for this region") private KList blockDrops = new KList<>(); - @RegistryListResource(IrisSpawner.class) @ArrayType(min = 1, type = IrisObjectPlacement.class) @Desc("Objects define what schematics (iob files) iris will place in this region") private KList objects = new KList<>(); @@ -139,7 +138,7 @@ public class IrisRegion extends IrisRegistrant implements IRare { private KList shoreBiomes = new KList<>(); @RegistryListResource(IrisBiome.class) @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") + @Desc("A list of root-level cave biomes in this region, used for carved cave interiors. Don't specify child biomes of other biomes here. Just the root parents.") private KList caveBiomes = new KList<>(); @ArrayType(min = 1, type = IrisDepositGenerator.class) @Desc("Define regional deposit generators that add onto the global deposit generators") @@ -147,9 +146,9 @@ public class IrisRegion extends IrisRegistrant implements IRare { @ArrayType(min = 1, type = IrisDepositVariant.class) @Desc("Deposit ore remap rules scoped to this region. Each entry declares a vertical band and a source->replacement block id map. Applied after biome rules but before dimension rules; first matching region rule wins.") private KList depositVariants = new KList<>(); - @Desc("The style of rivers") + @Desc("Unused. This field is not read by the engine; rivers are not generated from it.") private IrisGeneratorStyle riverStyle = NoiseStyle.VASCULAR_THIN.style().zoomed(7.77); - @Desc("The style of lakes") + @Desc("Unused. This field is not read by the engine; lakes are not generated from it.") private IrisGeneratorStyle lakeStyle = NoiseStyle.CELLULAR_IRIS_THICK.style(); @Desc("A color for visualizing this region with a color. I.e. #F13AF5. This will show up on the map.") private String color = null; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java index a1d556bbd..0f74ddbf4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuin.java @@ -24,6 +24,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import art.arcane.iris.engine.object.ruin.RuinGenerator; @@ -89,6 +90,7 @@ public class IrisRuin implements IrisProceduralPlacement { @Required @Desc("The primary structural block, e.g. minecraft:cobblestone or minecraft:stone_bricks. Ignored when blockPalette is set. This is the bulk material of the ruin before weathering swaps some of it out.") + @RegistryListBlockType private String block = "minecraft:cobblestone"; @Desc("A noise-driven palette for the primary block. When set this overrides the single block, letting the structure mix materials by noise. Palette wins over the block string via IrisProceduralBlocks.resolve.") @@ -125,6 +127,7 @@ public class IrisRuin implements IrisProceduralPlacement { private IrisMaterialPalette weatheringPalette = null; @Desc("A single weathered block id used when weatheringPalette is not set, e.g. minecraft:mossy_cobblestone. Applied to a noise-selected fraction of the structure, biased toward the lower rows.") + @RegistryListBlockType private String weatheredBlock = "minecraft:mossy_cobblestone"; @MinNumber(0) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java index 41e398d33..959bb559e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRuinDecorator.java @@ -21,6 +21,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; @@ -40,6 +41,7 @@ public class IrisRuinDecorator { @Required @Desc("The block id to place, e.g. minecraft:moss_carpet or minecraft:vine. Ignored when palette is set.") + @RegistryListBlockType private String block = ""; @Desc("A noise-driven palette for this decorator. When set this overrides the single block, letting the accent mix blocks by noise. Resolved through IrisProceduralBlocks.resolve so the palette wins over the string.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java index ffcea85ac..1c4a0b6a8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java @@ -54,7 +54,7 @@ public class IrisSpawner extends IrisRegistrant { @Desc("The block of 24 hour time to contain this spawn in.") private IrisTimeBlock timeBlock = new IrisTimeBlock(); - @Desc("The block of 24 hour time to contain this spawn in.") + @Desc("The weather condition required for this spawner to fire.") private IrisWeather weather = IrisWeather.ANY; @Desc("The maximum rate this spawner can fire") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java index 0e723f9ac..74113d3ce 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructure.java @@ -24,6 +24,7 @@ import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.RegistryListResource; +import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; @@ -92,6 +93,7 @@ public class IrisStructure extends IrisRegistrant { @Desc("Loot tables applied to containers placed by this structure's pieces.") private KList loot = new KList<>(); + @RegistryListVanillaStructure @Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.") private String vanillaSource = ""; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java index ce246660d..eb456f9dc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java @@ -22,6 +22,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListResource; import art.arcane.iris.engine.object.annotations.RegistryListStructure; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; @@ -94,8 +95,9 @@ public class IrisStructurePlacement { @Desc("Vertical anchor policy. LEGACY preserves the underground boolean. Cave modes search existing carved-space data and implicitly behave as underground placements.") private IrisStructureAnchorMode anchor = IrisStructureAnchorMode.LEGACY; + @RegistryListResource(IrisBiome.class) @ArrayType(type = String.class, min = 1) - @Desc("Optional cave-biome allowlist for cave anchor modes. Empty accepts the cave biome resolved at the selected anchor.") + @Desc("Optional cave-biome allowlist for cave anchor modes. Empty accepts the cave biome resolved at the selected anchor. Entries match by load key, including prefix matches.") private KList caveBiomes = new KList<>(); @MinNumber(1) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java index 4e3ee5a2c..1cd27a61e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructureTerrainMode.java @@ -4,9 +4,16 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("Controls how terrain is integrated with a structure.") public enum IrisStructureTerrainMode { + @Desc("Replays the registered native structure's own authored terrain adaptation (beard surface fitting, BURY/ENCAPSULATE fill, legacy template-air clearing). Does nothing for editable Iris structures, which have no source adaptation to replay.") SOURCE, + + @Desc("Disables all terrain integration. Pieces are placed into completely unmodified terrain and may end up embedded in solid rock or floating over dips.") PRESERVE, + + @Desc("Clears one straight axis-aligned box around the assembled pieces, expanded by horizontalPadding, ceilingPadding, and floorPadding. Ignores the carve shape and erosion settings.") BORE, + + @Desc("Clears the padded envelope using the configured carve shape (BOX, ROUNDED, or noise-eroded ERODED) so the cavity hugs the assembled pieces instead of a straight box.") FORCE_CARVE, @Desc("Raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff, even when the registered structure has no terrain adaptation. Existing higher terrain and authored air remain untouched.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java index b4845bbfd..200c5e3ac 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTree.java @@ -41,7 +41,7 @@ public class IrisTree { @ArrayType(min = 1, type = String.class) private KList treeTypes = new KList<>(); - @Desc("If enabled, overrides any TreeType") + @Desc("Unused. This flag is not read by the tree matcher; use treeTypes to control which TreeTypes match.") private boolean anyTree = false; @Required @@ -49,7 +49,7 @@ public class IrisTree { @ArrayType(min = 1, type = IrisTreeSize.class) private KList sizes = new KList<>(); - @Desc("If enabled, overrides trees of any size") + @Desc("Unused. This flag is not read by the tree matcher; use sizes to control which sapling sizes match.") private boolean anySize; public boolean matches(IrisTreeSize size, TreeType type) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java index a2e36428a..fa3dc97d5 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeDecorator.java @@ -21,6 +21,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; @@ -40,6 +41,7 @@ public class IrisTreeDecorator { @Required @Desc("The block id to place, e.g. minecraft:magma_block. Ignored when palette is set.") + @RegistryListBlockType private String block = ""; @Desc("A noise-driven palette for this decorator. When set this overrides the single block.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java index 3ea5a2c2f..2e9dfd0f3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeModes.java @@ -22,7 +22,7 @@ import art.arcane.iris.engine.object.annotations.Desc; @Desc("Sapling override object picking options") public enum IrisTreeModes { - @Desc("Check biome, then region, then dimension, pick the first one that has options") + @Desc("Check biome, then region, pick the first one that has options. Dimension placements are not consulted.") FIRST, @Desc("Check biome, regions, and dimensions, and pick any option from the total list") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java index 01852673d..643e69d8d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisTreeSecondaryLeaf.java @@ -20,6 +20,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MinNumber; +import art.arcane.iris.engine.object.annotations.RegistryListBlockType; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; import lombok.AllArgsConstructor; @@ -36,6 +37,7 @@ import lombok.experimental.Accessors; public class IrisTreeSecondaryLeaf { @Required @Desc("The block id to scatter, e.g. minecraft:shroomlight") + @RegistryListBlockType private String block = ""; @MinNumber(0) diff --git a/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java index eeed3a89a..1f7d7524c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/StudioMode.java @@ -25,14 +25,31 @@ import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator; @Desc("Represents a studio mode") public enum StudioMode { + @Desc("Installs no studio generator; the dimension generates normally.") NORMAL, + + @Desc("Debug layout: every biome in the dimension on a square grid, one chunk per biome cell, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_1x1, + + @Desc("Debug layout: every biome on a square grid with 3x3-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_3x3, + + @Desc("Debug layout: every biome on a square grid with 5x5-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_5x5, + + @Desc("Debug layout: every biome on a square grid with 9x9-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_9x9, + + @Desc("Debug layout: every biome on a square grid with 18x18-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_18x18, + + @Desc("Debug layout: every biome on a square grid with 36x36-chunk cells per biome, barrier floor past the last biome. Bukkit studio worlds only.") BIOME_BUFFET_36x36, + + @Desc("Not implemented: currently generates exactly like NORMAL.") REGION_BUFFET, + + @Desc("Replaces terrain with the object studio: a flat polished-deepslate floor laying every pack object out on framed, end-rod-marked grid plinths. Bukkit studio worlds only.") OBJECT_BUFFET; public void inject(BukkitChunkGenerator c) { diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierFluidIntentTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierFluidIntentTest.java index 283830800..87627a082 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierFluidIntentTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierFluidIntentTest.java @@ -43,4 +43,20 @@ public class IrisCarveModifierFluidIntentTest { assertTrue(IrisCarveModifier.usesDefaultLava(18, 18)); assertFalse(IrisCarveModifier.usesDefaultLava(18, 19)); } + + @Test + public void unsupportedSurfaceOreIsRemovedOnlyOverCarvedAir() { + PlatformBlockState ore = mock(PlatformBlockState.class); + PlatformBlockState ordinarySurface = mock(PlatformBlockState.class); + PlatformBlockState air = mock(PlatformBlockState.class); + PlatformBlockState solid = mock(PlatformBlockState.class); + when(ore.isOre()).thenReturn(true); + when(ordinarySurface.isOre()).thenReturn(false); + when(air.isSolid()).thenReturn(false); + when(solid.isSolid()).thenReturn(true); + + assertTrue(IrisCarveModifier.isUnsupportedSurfaceOre(ore, air)); + assertFalse(IrisCarveModifier.isUnsupportedSurfaceOre(ore, solid)); + assertFalse(IrisCarveModifier.isUnsupportedSurfaceOre(ordinarySurface, air)); + } } diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java index 22d63d1d5..21d9a83f8 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java @@ -1,10 +1,7 @@ package art.arcane.iris.engine.modifier; -import org.junit.BeforeClass; import org.junit.Test; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -15,32 +12,14 @@ import java.util.Set; import static org.junit.Assert.assertEquals; public class IrisCarveModifierZoneParityTest { - private static Constructor columnMaskConstructor; - private static Method addMethod; - private static Method nextSetBitMethod; - private static Method clearMethod; - - @BeforeClass - public static void setup() throws Exception { - Class columnMaskClass = Class.forName("art.arcane.iris.engine.modifier.IrisCarveModifier$ColumnMask"); - columnMaskConstructor = columnMaskClass.getDeclaredConstructor(); - addMethod = columnMaskClass.getDeclaredMethod("add", int.class); - nextSetBitMethod = columnMaskClass.getDeclaredMethod("nextSetBit", int.class); - clearMethod = columnMaskClass.getDeclaredMethod("clear"); - columnMaskConstructor.setAccessible(true); - addMethod.setAccessible(true); - nextSetBitMethod.setAccessible(true); - clearMethod.setAccessible(true); - } - @Test - public void randomColumnZonesMatchLegacySortedResolver() throws Exception { - Object columnMask = columnMaskConstructor.newInstance(); + public void randomColumnZonesMatchLegacySortedResolver() { + CarveColumnMask columnMask = new CarveColumnMask(); Random random = new Random(913_447L); int maxHeight = 320; for (int scenario = 0; scenario < 400; scenario++) { - clearMethod.invoke(columnMask); + columnMask.clear(); int sampleSize = 1 + random.nextInt(180); Set uniqueHeights = new HashSet<>(); @@ -50,7 +29,7 @@ public class IrisCarveModifierZoneParityTest { int[] heights = toIntArray(uniqueHeights); for (int index = 0; index < heights.length; index++) { - addMethod.invoke(columnMask, heights[index]); + columnMask.add(heights[index]); } List expectedZones = legacyZones(heights, maxHeight); @@ -60,8 +39,8 @@ public class IrisCarveModifierZoneParityTest { } @Test - public void edgeColumnsMatchLegacySortedResolver() throws Exception { - Object columnMask = columnMaskConstructor.newInstance(); + public void edgeColumnsMatchLegacySortedResolver() { + CarveColumnMask columnMask = new CarveColumnMask(); int maxHeight = 320; int[][] scenarios = new int[][]{ {-10, -1, 0, 1, 2, 5, 6, 9, 10, 11, 12, 200, 201, 205}, @@ -71,10 +50,10 @@ public class IrisCarveModifierZoneParityTest { }; for (int scenario = 0; scenario < scenarios.length; scenario++) { - clearMethod.invoke(columnMask); + columnMask.clear(); int[] heights = Arrays.copyOf(scenarios[scenario], scenarios[scenario].length); for (int index = 0; index < heights.length; index++) { - addMethod.invoke(columnMask, heights[index]); + columnMask.add(heights[index]); } List expectedZones = legacyZones(heights, maxHeight); @@ -131,9 +110,9 @@ public class IrisCarveModifierZoneParityTest { return zones; } - private List bitsetZones(Object columnMask, int maxHeight) throws Exception { + private List bitsetZones(CarveColumnMask columnMask, int maxHeight) { List zones = new ArrayList<>(); - int firstHeight = nextSetBit(columnMask, 0); + int firstHeight = columnMask.nextSetBit(0); if (firstHeight < 0) { return zones; } @@ -159,7 +138,7 @@ public class IrisCarveModifierZoneParityTest { } } - y = nextSetBit(columnMask, y + 1); + y = columnMask.nextSetBit(y + 1); } if (isValidZone(floor, ceiling, maxHeight)) { @@ -169,10 +148,6 @@ public class IrisCarveModifierZoneParityTest { return zones; } - private int nextSetBit(Object columnMask, int fromBit) throws Exception { - return (Integer) nextSetBitMethod.invoke(columnMask, fromBit); - } - private boolean isValidZone(int floor, int ceiling, int maxHeight) { return floor < ceiling && floor >= 0 diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java new file mode 100644 index 000000000..e5df4bd10 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java @@ -0,0 +1,67 @@ +package art.arcane.iris.engine.modifier; + +import art.arcane.volmlib.util.matter.MatterCavern; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class IrisCarveScratchTest { + @Test + public void wallBufferResizesAndReplacesWithoutLosingEntries() { + CarveWallBuffer buffer = new CarveWallBuffer(2); + Map expected = new HashMap<>(); + for (int index = 0; index < 24; index++) { + int x = index & 15; + int y = 20 + index; + int z = (index * 3) & 15; + MatterCavern cavern = new MatterCavern(true, "cave-" + index, (byte) 0); + buffer.put(x, y, z, cavern); + expected.put(key(x, y, z), cavern); + } + + MatterCavern replacement = new MatterCavern(true, "replacement", (byte) 0); + buffer.put(5, 25, 15, replacement); + expected.put(key(5, 25, 15), replacement); + + Map actual = new HashMap<>(); + buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern)); + assertEquals(expected.keySet(), actual.keySet()); + for (Map.Entry entry : expected.entrySet()) { + assertSame(entry.getValue(), actual.get(entry.getKey())); + } + } + + @Test + public void resetClearsReusableState() { + IrisCarveScratch scratch = new IrisCarveScratch(); + MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0); + scratch.columnMasks[0].add(12); + scratch.boundaryMasks[0].add(13); + scratch.boundaryCaverns[0] = cavern; + scratch.walls.put(1, 12, 2, cavern); + scratch.customBiomeCache.put("cave", null); + scratch.customCaveBiomePresent = true; + + scratch.reset(); + + assertTrue(scratch.columnMasks[0].isEmpty()); + assertTrue(scratch.boundaryMasks[0].isEmpty()); + assertNull(scratch.boundaryCaverns[0]); + assertTrue(scratch.customBiomeCache.isEmpty()); + assertFalse(scratch.customCaveBiomePresent); + int[] wallCount = new int[1]; + scratch.walls.forEach((x, y, z, value) -> wallCount[0]++); + assertEquals(0, wallCount[0]); + } + + private static String key(int x, int y, int z) { + return x + ":" + y + ":" + z; + } +} diff --git a/docs/00 - Overview.md b/docs/00 - Overview.md index 489cc03fd..c3361c697 100644 --- a/docs/00 - Overview.md +++ b/docs/00 - Overview.md @@ -1,40 +1,48 @@ # 00 - Overview -Iris is a world generation engine for Minecraft servers and mod loaders. It builds terrain, biomes, caves, structures, objects, and entities from editable JSON packs, exposes an in-game studio authoring workflow, and runs as a Bukkit-family plugin or as a Fabric, Forge, or NeoForge server mod. Cross-platform generation is designed and tested for deterministic parity when artifacts, pack bytes, seeds, and test areas are identical; verify release candidates with GoldenHash. This branch targets Minecraft 26.2; Java 25 is required everywhere. +Iris is a world generation engine for Minecraft: it replaces the vanilla chunk generator with terrain, biomes, caves, structures, objects, and entities built from editable JSON packs. The same engine ships as a Bukkit-family plugin and as a Fabric, Forge, or NeoForge server mod, and generates identical chunks on all four when artifacts, pack bytes, seed, and area match. This page is the map of the documentation set: read it to find the page you actually need, then leave. This branch targets Minecraft 26.2 and requires Java 25 everywhere. + +## Who this documentation is for + +There are three audiences and the numbering reflects them. Pages `00`–`33` are for **server operators** installing Iris and **pack authors** writing dimensions, in roughly the order a newcomer needs them. Pages `85`–`87` are **maintainer** checklists for cutting a release. Pages `90`–`94` are for **Java developers** consuming the Iris API from their own plugin or mod. + +Reading the set front to back is a waste of time. Pick the outcome you want from the table below and follow only that row. ## Choose a learning path -Do not read the documentation as one long reference. Start with the outcome you need and follow that path in order: - -| Outcome | Read and complete | +| You want to | Read, in order | |---|---| -| Install Iris and create a world | `01 - Installation & Platforms.md` → `02 - Getting Started.md` → `31 - Operator Runbooks & Smoke Tests.md` | -| Build a pack from nothing | `05 - Concepts & Pack Layout.md` → `10 - Studio & VSCode Schemas.md` → `26 - Example - Minimal Dimension.md` | -| Design terrain and biomes | `11 - Dimensions.md` → `12 - Regions.md` → `13 - Biomes.md` → `14 - Generators & Noise.md` | -| Add caves and surface detail | `15 - Caves & Carving.md` → `16 - Surfaces, Decorators & Deposits.md` → `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md` | -| Add a structure | `18 - Structures Overview.md`, then `19 - Objects.md` + `20 - Object Placement.md`, `21 - Jigsaw Structures.md`, or `22 - Native Structures & Datapacks.md` | -| Prepare a production world | `25 - Pack Management.md` → `06 - Worlds & Lifecycle.md` → `07 - Pregeneration.md` → `31 - Operator Runbooks & Smoke Tests.md` | -| Integrate another plugin or mod | `28 - Integrations.md` → `30 - Platform Differences.md`; Java consumers start at `90 - API - Getting Started.md` | +| Get Iris running and make one world | `01 - Installation & Platforms.md` → `02 - Getting Started.md` → `31 - Operator Runbooks.md` | +| Write a pack from scratch | `05 - Concepts & Pack Layout.md` → `10 - Studio & VSCode Schemas.md` → `26 - Example - Minimal Dimension.md` | +| Shape terrain and lay out biomes | `11 - Dimensions.md` → `12 - Regions.md` → `13 - Biomes.md` → `14 - Generators & Noise.md` | +| Add caves, surface detail, and vegetation | `15 - Caves & Carving.md` → `16 - Surfaces, Decorators & Deposits.md` → `17 - Trees, Fungi, Coral, Crystals, Formations, Ruins.md` | +| Place a building or structure | `18 - Structures Overview.md` first to pick an approach, then `19 - Objects.md` + `20 - Object Placement.md` for single `.iob` objects, `21 - Jigsaw Structures.md` for multi-piece Iris structures, or `22 - Native Structures & Datapacks.md` for vanilla/datapack ones | +| Ship a pack to a production server | `25 - Pack Management.md` → `06 - Worlds & Lifecycle.md` → `07 - Pregeneration.md` → `31 - Operator Runbooks.md` | +| Make another plugin or mod work with Iris | `28 - Integrations.md` → `30 - Platform Differences.md`; if you're writing Java against Iris, start at `90 - API - Getting Started.md` | -Each tutorial gives an observable gate. Stop and resolve that gate before layering on the next system; otherwise a missing biome key can look like a cave, decorator, or structure failure later. +Each tutorial page ends with something you can observe — a world that loads, a chunk that generates, a hotload that lands. Confirm that before moving to the next page. Iris failures cascade misleadingly: a biome key you typo'd in step two shows up three systems later as a cave, decorator, or structure that "doesn't work," and you'll debug the wrong thing. ## Platforms -| Platform | Artifact | Minecraft | Notes | +One plugin jar covers the whole Bukkit family; each mod loader gets its own jar. Pick by what your server runs, not by feature set — the generator is the same code on all of them. + +| Platform | Artifact | Minecraft | What's different | |---|---|---|---| -| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 – 26.2 | Full plugin feature set | -| Folia | plugin jar | 26.1.2 – 26.2 | Region-safe scheduling; runtime world create is staged for restart (see `01 - Installation & Platforms.md`, `06 - Worlds & Lifecycle.md`) | -| Spigot / CraftBukkit | plugin jar | 26.1.2 – 26.2 | Full plugin feature set | -| Fabric | mod jar | 26.2 | Server worldgen + client HUD; Fabric Loader 0.19.3+ | -| Forge | mod jar | 26.2 | Server worldgen + client HUD; Forge 65.0.4+ | -| NeoForge | mod jar | 26.2 | Server worldgen + client HUD; NeoForge 26.2.0.12-beta+ | +| Paper / Purpur / Leaf / Canvas | plugin jar | 26.1.2 – 26.2 | Nothing; this is the reference plugin target | +| Spigot / CraftBukkit | plugin jar | 26.1.2 – 26.2 | Nothing for generation. Paper-only APIs degrade gracefully | +| Folia | plugin jar | 26.1.2 – 26.2 | Region-safe scheduling, and `/iris create` cannot build a live world at runtime — it stages the world and requires a restart. See `01 - Installation & Platforms.md` and `06 - Worlds & Lifecycle.md` | +| Fabric | mod jar | 26.2 | Server worldgen plus an optional client HUD; needs Fabric Loader 0.19.3+ and Java 25 | +| Forge | mod jar | 26.2 | Same; needs Forge 65.x (built against 26.2-65.0.4) | +| NeoForge | mod jar | 26.2 | Same; needs NeoForge 26.2.x (built against 26.2.0.12-beta) | -Plugin identity: name `Iris` (from root project name), command `iris` with aliases `ir` / `irs`, `folia-supported: true`, `load: STARTUP`, `api-version` 26.1 (loads on 26.1.2 and 26.2). Soft-depends include PlaceholderAPI, WorldEdit, item plugins, MythicMobs; Multiverse-Core is ordered after Iris (`loadbefore` / paper `load: AFTER`). +The plugin registers as `Iris` with command `/iris` (aliases `/ir`, `/irs`), `folia-supported: true`, `load: STARTUP`, and `api-version: 26.1` — the low api-version is deliberate so one jar loads on both 26.1.2 and 26.2. Its descriptor declares two permissions, `iris.all` (the whole command tree) and `iris.treefeller` (survival tree felling only), both defaulting to op. Optional soft-dependencies load before Iris; Multiverse-Core is ordered *after* Iris so Multiverse sees Iris generators once they exist. Full list in `01 - Installation & Platforms.md`. -Mod id on all three loaders: `irisworldgen`. +All three mod loaders use mod id `irisworldgen`, and register `/ir` and `/irs` as command redirects the same way the plugin does. ## Feature map +Every feature of Iris is documented on exactly one page. Find the subject, go there. + | Area | What it covers | Doc | |---|---|---| | Install and platforms | Plugin vs mod jars, data dirs, first boot, native worldgen matrix | `01 - Installation & Platforms.md` | @@ -67,7 +75,7 @@ Mod id on all three loaders: `irisworldgen`. | Integrations | WorldEdit, Multiverse, Mythic, item plugins, tree feller | `28 - Integrations.md` | | Client HUD | Client mod HUD and protocol channel | `29 - Client HUD & Protocol.md` | | Platform matrix | Bukkit vs Fabric / Forge / NeoForge differences | `30 - Platform Differences.md` | -| Operator checks | Manual verification | `31 - Operator Runbooks & Smoke Tests.md` | +| Operator checks | Manual verification | `31 - Operator Runbooks.md` | | Determinism | Goldenhash cross-platform gate | `32 - Determinism & Goldenhash.md` | | Performance | Threads, mantle, SIMD, pregen caps | `33 - Performance Tuning.md` | | Maintainer — MC version bump | Version bump procedure | `85 - Maintainer - MC Version Bump.md` | @@ -79,40 +87,44 @@ Mod id on all three loaders: `irisworldgen`. | API — tree feller | Tree feller service | `93 - API - Tree Feller.md` | | API — modded | Modded public API (`art.arcane.iris.modded.api`) | `94 - API - Modded.md` | -Docs `00`–`33` are for operators and pack authors in reading order. `85`–`87` are maintainer checklists. `90`–`94` are for plugin and mod developers. +## Content model -## Content model (brief) +Six terms carry most of the documentation. Learn them here and the rest of the set reads much faster. -| Term | Meaning | +| Term | What it is | |---|---| -| Pack | Directory of JSON and `.iob` under `packs//` with at least `dimensions/*.json` | -| Dimension | Root config for a world type (height, modes, regions, imports) | -| Region / biome / generator | Spatial and terrain authoring units | -| Object / structure | Placed content (`.iob`, Iris jigsaw, native or datapack structures) | -| Studio | Transient authoring world with live pack hotload and VSCode schemas; deleted on close and purged at startup | -| World pack snapshot | Production worlds copy the pack into `/iris/pack` and read that copy (see `05 - Concepts & Pack Layout.md`) | +| Pack | A folder of JSON and `.iob` files under `packs//`. It needs at least one `dimensions/*.json` to count as a pack at all — a folder without one is treated as absent and will be re-downloaded | +| Dimension | The root config for one world type: height range, generation modes, which regions it uses, what native content it imports. One dimension file is one world's ruleset | +| Region / biome / generator | The authoring units under a dimension. Regions divide the map, biomes fill regions, generators produce the actual heightmap noise | +| Object / structure | Placed content. An object is a single saved build (`.iob`); a structure is either an Iris jigsaw of several objects, or a vanilla/datapack/mod structure Iris allows through | +| Studio | A throwaway authoring world that reads the live pack folder and hotloads your edits into new chunks. Deleted when you close it, and any leftovers are purged at startup | +| World pack snapshot | A production world copies the pack into `/iris/pack` at creation and reads only that copy forever after. This is the single most common source of "my edits did nothing" — see `05 - Concepts & Pack Layout.md` | ## Project layout +Relevant if you're building Iris or filing a bug against a specific subsystem. + | Path | Role | |---|---| -| `core/` | Pure-JVM engine, pack loader, pregen, studio services, localization catalogs | -| `core/agent/` | Agent helper module used by the core build | -| `spi/` | Platform SPI and pure-JVM contracts (`IrisPlatform`, protocol types) | -| `adapters/bukkit/plugin/` | Bukkit plugin main, commands, public Bukkit API, Paper plugin descriptor | +| `core/` | The engine: pack loader, generation pipeline, pregen, studio services, localization catalogs. Pure JVM, no platform types | +| `core/agent/` | Java instrumentation agent (premain/agent-class jar) consumed by the core build | +| `spi/` | The platform contract (`IrisPlatform`, protocol types) that lets `core/` stay platform-free | +| `adapters/bukkit/plugin/` | Bukkit plugin main class, the Director command tree, the public Bukkit API, and both plugin descriptors | | `adapters/bukkit/nms/v26_2_R1/` | NMS bindings for the current Minecraft line | -| `adapters/minecraft-common/` | Shared adapter code used by Bukkit and mod loaders | -| `adapters/modded-common/` | Shared Fabric / Forge / NeoForge worldgen, commands, services | -| `adapters/client-common/` | Client HUD and world-type screens | -| `adapters/fabric/`, `adapters/forge/`, `adapters/neoforge/` | Standalone loader builds (own `settings.gradle`) | -| `probe/` | Offline tooling and stub platform | -| `buildSrc/` | Shared Gradle helpers (artifact verification, API generation) | -| `dist/` | Built consumer jars after `buildAllToOut` | -| `docs/` | Authoritative product and API documentation | +| `adapters/minecraft-common/` | Source shared by the Bukkit and mod-loader adapters | +| `adapters/modded-common/` | Source shared by Fabric, Forge, and NeoForge: worldgen hooks, Brigadier commands, services | +| `adapters/client-common/` | Client-dist source: HUD, keybinds, world-type screens | +| `adapters/fabric/`, `adapters/forge/`, `adapters/neoforge/` | The three loader builds. Each is a standalone Gradle build with its own `settings.gradle` | +| `probe/` | Offline tooling and a stub platform for running the engine without a server | +| `buildSrc/` | Gradle helpers: artifact verification, NMS bindings, API generation | +| `dist/` | Where `buildAllToOut` drops the finished consumer jars | +| `docs/` | This documentation set, which is the authority over any hosted copy | + +`minecraft-common`, `modded-common`, and `client-common` are source trees only — they have no `build.gradle` and are not Gradle projects. The loader builds pull them in as extra source directories. ## Developer build check -Set `JAVA_HOME` to JDK 25, then run the repository gate from the Iris root: +Set `JAVA_HOME` to a JDK 25, then from the Iris root: ```text java -version @@ -120,29 +132,31 @@ java -version ./gradlew buildAllToOut ``` -The check passes when `build` completes with no failed tasks and `buildAllToOut` publishes one current jar per supported platform under `dist/`. `build` already runs the test suite; use `./gradlew test` when you need to rerun tests without assembling every artifact. +The check passes when `build` finishes with no failed tasks and `buildAllToOut` leaves one current jar per platform in `dist/`. `build` already runs the tests, so use `./gradlew test` only when you want to rerun tests without reassembling every artifact. -`buildAllToOut` writes every platform jar into `dist/`: +At version `4.0.0-26.2` the four jars are named like this — the CraftBukkit one carries the supported Minecraft *range*, the loader jars carry `+`: -``` -Iris v [CraftBukkit] .jar -Iris v [Fabric] +.jar -Iris v [Forge] +.jar -Iris v [NeoForge] +.jar +```text +Iris v4.0.0-26.2 [CraftBukkit] 26.1.2-26.2.jar +Iris v4.0.0-26.2 [Fabric] 26.2+0.19.3.jar +Iris v4.0.0-26.2 [Forge] 26.2+65.0.4.jar +Iris v4.0.0-26.2 [NeoForge] 26.2+26.2.0.12-beta.jar ``` -Per-platform: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `buildNeoforge`. SPI jar: `./gradlew :spi:jar` → `spi/build/libs/`. +Build one platform at a time with `./gradlew buildBukkit`, `buildFabric`, `buildForge`, or `buildNeoforge`. The SPI jar comes from `./gradlew :spi:jar` and lands in `spi/build/libs/`. -Modded adapters are driven with their own project root when developing: +To iterate on a loader adapter, drive it from its own project root — these are separate Gradle builds, so a root-level invocation won't reach them: -``` +```text ./gradlew -p adapters/fabric runServer ./gradlew -p adapters/forge runServer ./gradlew -p adapters/neoforge runServer ``` -`-PincludeModdedAdapters=true` can surface those builds in the root composite for IDE import only; it is off by default because each adapter includes the root build back for `core`/`spi` substitution. +`-PincludeModdedAdapters=true` surfaces those builds in the root composite for IDE import. It's off by default because each adapter includes the root build back for `core`/`spi` substitution, which closes a composite cycle. -Current version property: `irisVersion=4.0.0-26.2` in `gradle.properties`. +A passing Bukkit jar proves nothing about the loaders. Loom, ForgeGradle, and ModDevGradle each fail in their own ways, so if a loader build breaks while the root build is green, rerun that adapter from its own root and fix its first error rather than re-running the root build. -If a mod-loader build fails while the root Bukkit/core build passes, rerun that adapter from its own project root and fix the first loader-specific error. Do not treat a Bukkit jar or core test pass as proof that Fabric, Forge, or NeoForge compiled. +The current version lives in `gradle.properties` as `irisVersion=4.0.0-26.2`. + +Next: install Iris with `01 - Installation & Platforms.md`. diff --git a/docs/01 - Installation & Platforms.md b/docs/01 - Installation & Platforms.md index de81f70a2..baabbc661 100644 --- a/docs/01 - Installation & Platforms.md +++ b/docs/01 - Installation & Platforms.md @@ -1,46 +1,49 @@ # 01 - Installation & Platforms -Iris installs as either a Bukkit-family plugin jar or a self-contained Fabric, Forge, or NeoForge mod jar. Java 25 is required on every platform. On first boot the managed `overworld` and `underworld` beta packs are downloaded when missing; packs live under each platform’s data directory. +Iris ships as one Bukkit-family plugin jar and three self-contained mod jars (Fabric, Forge, NeoForge). This page gets the right artifact onto your server, tells you how to prove the install actually worked, and documents where Iris puts its files on each platform. Java 25 is required everywhere. On first boot Iris downloads the managed `overworld` and `underworld` beta packs if they aren't already present. -## Installation outcome +Read this before `02 - Getting Started.md`. If Iris is already installed and you just want a world, skip ahead. -Complete one platform path below. A successful install has all three results: +## What a good install looks like -1. Iris reaches its enabled/ready state without an exception. -2. The platform data directory contains `settings.json` and loadable `packs/overworld/` and `packs/underworld/` directories. -3. `/iris` prints help from the server console. On a modded client, the Iris keybind category is an additional client-side check, not a substitute for the server check. +Whichever path you take, you're done when all three of these are true: -Keep the previous jar/mod and the entire Iris data directory until the new build passes these checks. Replacing the binary does not update pack snapshots already stored inside worlds. +1. Iris reached its enabled/ready state with no exception in the startup log. +2. The data directory has a `settings.json` and loadable `packs/overworld/` and `packs/underworld/` directories. +3. `/iris` prints help from the server console. + +On a modded client the Iris keybind category showing up is a nice extra signal, but it proves the client mod loaded — not that the server can generate chunks. Always check server-side. + +Keep the old jar and the entire Iris data directory until the new build passes these checks. Swapping the binary does **not** update pack snapshots already copied into existing worlds; that's a separate operation covered in `06 - Worlds & Lifecycle.md`. ## Requirements | Requirement | Value | |---|---| -| Java | 25 (`--release 25` / `java >= 25` on mod loaders) | -| Minecraft (plugin) | 26.1.2 – 26.2 (`api-version` 26.1) | -| Minecraft (mod) | 26.2 | +| Java | 25. The mod jars declare `java >= 25` and refuse to load on anything older | +| Minecraft (plugin) | 26.1.2 – 26.2. One jar covers both; `api-version` is pinned to 26.1 so it loads on the older line too | +| Minecraft (mod) | 26.2 only | | Fabric Loader | 0.19.3+ | -| Forge | 65.0.4+ | -| NeoForge | 26.2.0.12-beta+ | -| Network | Outbound HTTPS on first boot for the GitHub IrisDimensions Overworld and Underworld beta release assets | +| Forge | 65.x (built and tested against 26.2-65.0.4) | +| NeoForge | 26.2.x (built and tested against 26.2.0.12-beta) | +| Network | Outbound HTTPS to `github.com` on first boot, to fetch the IrisDimensions Overworld and Underworld beta release assets | Before replacing an existing installation: -1. Run `java -version` and confirm the server is using Java 25, not merely that Java 25 is installed elsewhere. -2. Match the jar label to the target platform and Minecraft version. +1. Run `java -version` **on the server** and confirm it reports 25. Having Java 25 installed somewhere on the box is not the same as the server process using it. +2. Match the jar label to the platform and Minecraft version you're running. 3. Stop the server cleanly. -4. Back up the existing Iris jar/mod, Iris data directory, and every Iris world you intend to retain. +4. Back up the Iris jar/mod, the Iris data directory, and every Iris world you intend to keep. -Do not copy multiple Iris platform jars into the same `plugins/` or `mods/` directory. +Never put two Iris platform jars in the same `plugins/` or `mods/` folder. That fails in confusing ways rather than picking a winner. ## Plugin install (Paper / Purpur / Leaf / Canvas / Folia / Spigot) -1. Place the CraftBukkit-labelled plugin jar into `plugins/`. -2. Start the server. Iris loads at `STARTUP` (`plugin.yml` / `paper-plugin.yml`). -3. On first boot Iris provisions `overworld` and `underworld` into `plugins/Iris/packs/` when missing from their IrisDimensions `beta` release ZIPs. -4. Settings are written at `plugins/Iris/settings.json` if absent (`IrisSettings.read()`). +1. Drop the CraftBukkit-labelled plugin jar into `plugins/`. +2. Start the server. Iris loads at `STARTUP`, before worlds are created, because it has to register generators first. +3. First boot writes `plugins/Iris/settings.json` with defaults if it's absent, and provisions `overworld` and `underworld` into `plugins/Iris/packs/` from their IrisDimensions `beta` release ZIPs when missing. -Validate the plugin install from the server console: +Then verify from the server console: ```text /iris version @@ -48,28 +51,53 @@ Validate the plugin install from the server console: /iris pack validate pack=underworld ``` -The first command must report the running Iris, platform, and Minecraft versions. The second must resolve the downloaded pack and finish without blocking validation errors. Then complete the disposable-world workflow in `02 - Getting Started.md`; a command response alone does not prove that the generator can create chunks. +`/iris version` prints exactly one line — `Iris v by Volmit Software`. That's the whole output; it does not report platform or Minecraft version, so use it only as a "the command tree is alive" check. Each `pack validate` must resolve the downloaded pack and finish with no blocking errors. -Iris denies player login until managed external datapacks and installed dimension packs complete startup validation. Unchanged validated datapacks and packs reuse their persisted content/context results; a failed external datapack state keeps login and all Iris world creation locked, while an install or repair that changes registry inputs requires the clean restart Iris reports. A dimension pack with blocking errors remains unavailable to world and Studio creation without preventing healthy validated packs from being used. +Pass an explicit pack name. `/iris pack validate` with no argument fails with a missing-argument error rather than validating everything, because the parameter is required. To validate every installed pack, pass the key with an empty value: `/iris pack validate pack=`. -Command root: `/iris` (aliases `/ir`, `/irs`). Explicit permission in the descriptor: `iris.treefeller` (default op). Command access uses the Director permission model rooted at `iris.all` (see `04 - Commands & Permissions.md`). +A command responding is not proof the generator can produce chunks. Finish with the disposable-world walkthrough in `02 - Getting Started.md`. -Soft dependencies (optional, not bundled): PlaceholderAPI, CraftEngine, Nexo, ItemsAdder, SCore, ExecutableItems, MythicLib, MMOItems, eco, EcoItems, MythicMobs, MythicCrucible, KGenerators, WorldEdit. Multiverse-Core is ordered after Iris so Multiverse sees Iris generators after Iris is up. +### Startup validation gates login -Before creating a real world, run the Bukkit fresh-install smoke in `31 - Operator Runbooks & Smoke Tests.md`. If the first-boot pack download fails, fix network access and restart; do not create an empty directory named `overworld` as a workaround because an incomplete pack is not a usable dimension. +Iris blocks player login until external datapack validation and dimension-pack validation both complete. The kick message names the reason and tells you to check the console. -### Folia note +The two gates behave differently. A failed or restart-pending **external datapack** state keeps login locked and blocks all Iris world creation until you fix it and restart — Iris tells you when a restart is what's required. Unchanged, already-validated datapacks and packs reuse their persisted results, so this costs nothing on a normal boot. A **dimension pack** with blocking errors does *not* lock the server: that one pack is refused for world and Studio creation, an error listing the reasons is printed at startup, and every healthy pack stays usable. -`folia-supported: true`. Engine work uses region-safe scheduling. Runtime `/iris create` does **not** hot-create a live world on Folia: Iris stages world files, pack snapshot, and `bukkit.yml` registration, then requires a server restart before the world generates and loads. After restart, use `/iris load` or rely on the registered world entry as appropriate. See `06 - Worlds & Lifecycle.md`. +### Permissions + +The descriptor declares two permissions, both defaulting to op: + +| Permission | Grants | +|---|---| +| `iris.all` | The entire `/iris` command tree — worlds, studio, pregen, packs, developer tools | +| `iris.treefeller` | Survival tree felling with an axe. Nothing else | + +There is exactly one permission check, at the command root, against `iris.all`. Subcommands do not derive their own permission nodes — there is no `iris.all.pregen`. If a non-op needs any Iris command, they need `iris.all`, and that is all of it. See `04 - Commands & Permissions.md`. + +Command root is `/iris`, aliases `/ir` and `/irs`. + +### Soft dependencies + +None of these are bundled or required. When present they load before Iris so Iris can see them: PlaceholderAPI, CraftEngine, Nexo, ItemsAdder, SCore, ExecutableItems, MythicLib, MMOItems, eco, EcoItems, MythicMobs, MythicCrucible, KGenerators, WorldEdit. Multiverse-Core is deliberately ordered *after* Iris so that Multiverse sees Iris generators once they're registered. Integration details in `28 - Integrations.md`. + +### Folia + +`folia-supported: true`, and engine work uses region-safe scheduling. The one behavioral difference that matters at install time: `/iris create` cannot build a live world at runtime on Folia. Instead it stages the world files, installs the pack snapshot, registers the world in `bukkit.yml`, and prints a message telling you to restart. After the restart the world generates and loads on its own from that `bukkit.yml` entry — you do not need to run `/iris load`. See `06 - Worlds & Lifecycle.md`. + +### If the first-boot download fails + +Fix network access and restart. Do not create an empty folder named `overworld` to silence the error: a folder without a `dimensions/*.json` inside it is treated as absent (and re-downloaded), and a partial pack is not a usable dimension. To install by hand later, use `/iris download overworld` and `/iris download underworld`. + +Before you create a world you care about, run the Bukkit fresh-install runbook in `31 - Operator Runbooks.md`. ## Mod install (Fabric / Forge / NeoForge) -1. Place the matching mod jar into `mods/`. -2. Start the dedicated server (or a client for singleplayer; see below). -3. The jar is self-contained: core, SPI, and required Fabric API modules are bundled where applicable. Mod id: `irisworldgen`. -4. On first boot, if `config/irisworldgen/modded.json` has `autoDownloadDefaultPack` true (default), Iris installs the managed `overworld` and `underworld` beta releases when missing, followed by a configured non-managed `defaultPack` when applicable, before the forced worldgen datapack is written. +1. Drop the matching mod jar into `mods/`. +2. Start the dedicated server, or a client if you want singleplayer. +3. The jar is self-contained — engine, SPI, and the required Fabric API modules are bundled. Mod id is `irisworldgen` on all three loaders. +4. On first boot, if `autoDownloadDefaultPack` is true in `config/irisworldgen/modded.json` (it is by default), a daemon thread installs the managed `overworld` and `underworld` beta packs when missing, then a configured non-managed `defaultPack` if you've set one. Only after that does it write the forced worldgen datapack. -Validate the server-side mod install: +Verify server-side: ```text /iris version @@ -77,121 +105,143 @@ Validate the server-side mod install: /iris pack validate underworld ``` -The install passes when Iris reports the expected loader/version, both managed pack directories contain their primary dimension JSON, and validation has no blocking errors. Restart once before creating a world if a pack or its generated dimension-type datapack was installed during this boot. +Modded `/iris version` prints more than the Bukkit one — mod version, platform, Minecraft version, and the count of loaded Iris dimensions. The install passes when that line looks right, both managed pack directories contain their primary dimension JSON, and validation reports no blocking errors. -Packs installed later register custom dimension types (height ranges) and custom biomes through the forced datapack at server start. **Restart once after adding a pack** so worlds get full heights and biomes. Worlds created before that restart run with fallback heights. +### Restart once after installing a pack -### Singleplayer (modded clients) +This is the modded-specific gotcha. Packs register their custom dimension types (which set the world height range) and their custom biomes through the forced datapack, and that datapack is read when the server builds its registries at start. A pack installed during *this* boot may land after registries are already built. -Installed Iris packs appear as selectable World Types on the Create New World screen (`IRIS:` style presets from the forced datapack). The integrated server runs the same engine as dedicated servers. +So: **restart once after any pack is installed, before creating a world with it.** Worlds created before that restart run with fallback heights and will not have the pack's real height range or custom biomes. If a pack or its generated dimension-type datapack was installed during the current boot, restart before continuing. + +### Singleplayer on a modded client + +Installed Iris packs show up as selectable World Types on the Create New World screen, named `IRIS:` (or `IRIS: / ` when a pack exposes more than one dimension). The integrated server runs the same engine as a dedicated one. ### Client HUD -Installing the mod jar on a client adds a pregeneration HUD (progress bar, chunks done/total, percent, chunks/s, ETA; yellow while paused). Key `H` (rebindable, category “Iris”) toggles it. The HUD talks to modded Iris servers and Bukkit/Paper Iris over channel `irisworldgen:main` (custom payloads on modded, plugin messaging on Bukkit). Vanilla clients are unaffected and get the server-side boss bar instead; on non-Iris servers the client mod is inert. +Installing the mod jar on a client adds a pregeneration HUD showing a progress bar, chunks done and total, percent, chunks per second, and ETA, turning yellow while paused. `H` toggles it. The keybind category is "Iris" and also holds `M` (Iris Vision Map) and `J` (Iris What overlay); all three are rebindable. Details in `29 - Client HUD & Protocol.md`. + +The HUD talks to both modded Iris servers and Bukkit/Paper Iris over channel `irisworldgen:main` — custom payloads on modded, plugin messaging on Bukkit. Vanilla clients are unaffected and get the server-side boss bar instead. On a non-Iris server the client mod is inert. ## Data directories -### Plugin (`plugins/Iris/`) +### Plugin -| Path | Role | +| Path | What lives there | |---|---| -| `plugins/Iris/settings.json` | Engine settings (`IrisSettings`); created with defaults on first read | -| `plugins/Iris/packs//` | Installed packs (workspace name `packs`) | -| `plugins/Iris/bootstrap/` | Default-pack provision marker and related bootstrap state | -| `plugins/Iris/datapacks/` | Datapack download cache / staging (Bukkit datapack tooling) | -| `plugins/Iris/languages/overrides/.json` | Optional server message overrides | -| `/iris/pack/` | Per-world pack **snapshot** used by production engines | +| `plugins/Iris/settings.json` | Engine settings. Written with defaults if absent, and rewritten on every read, so keys added by a new Iris version appear automatically with their defaults and your edits survive | +| `plugins/Iris/packs//` | Installed packs. This is the live tree the Studio reads and edits | +| `plugins/Iris/bootstrap/` | First-boot provisioning marker (`provisioned.properties`) recording what was installed and against which compiler identity | +| `plugins/Iris/datapacks/` | External datapack imports pulled from Modrinth by `/iris datapack`, plus a `staging/` subfolder used mid-download | +| `plugins/Iris/languages/overrides/.json` | Optional server message overrides. See `08 - Localization.md` | +| `/datapacks/iris/` | The aggregate worldgen datapack Iris compiles from your installed packs. Iris owns this; do not hand-edit it | +| `/dimensions///` | Storage for a managed Iris world. Namespace is `iris` for worlds Iris creates | +| `/iris/pack/` | The per-world pack **snapshot**. A production engine reads only this copy, never `plugins/Iris/packs/` | -World dimension roots for managed Iris worlds are under the server’s world container (Iris managed dimension storage); see `06 - Worlds & Lifecycle.md`. +`` is the server's level directory — the folder named by `level-name` in `server.properties`. -### Mod (`config/` relative to the game instance) +That last row is worth internalizing early: editing `plugins/Iris/packs/overworld/` has no effect on a world that already exists, because that world froze a copy of the pack at creation time. See `05 - Concepts & Pack Layout.md`. -| Path | Role | +### Mod + +Paths are relative to the game instance's `config/` directory. + +| Path | What lives there | |---|---| -| `config/irisworldgen/packs//` | Installed packs; valid when `dimensions/.json` exists | -| `config/irisworldgen/generated/datapack/iris/` | Generated forced datapack (owned by Iris; do not edit) | -| `config/irisworldgen/modded.json` | Mod-side config: `defaultPack`, `autoDownloadDefaultPack`, primary world routing, main-world override | -| `config/iris/` | Engine data directory: `settings.json` and per-world engine state via `dataFile` | +| `config/irisworldgen/packs//` | Installed packs. A pack counts as installed when `dimensions/.json` exists | +| `config/irisworldgen/generated/datapack/iris/` | The generated forced datapack (datapack id `iris_worldgen`), plus a hash sidecar used to detect staleness. Iris owns this; do not edit it | +| `config/irisworldgen/modded.json` | Mod-side config: default pack, auto-download, primary-world routing, main-world override | +| `config/iris/` | Engine data directory — `settings.json` and per-world engine state | -Pack resolution for engines, commands, and the forced datapack uses `config/irisworldgen/packs`. The engine data folder is `config/iris` — different roots. +Two different roots, and mixing them up is a common mistake. Packs, the generated datapack, and mod config live under `config/irisworldgen/`. The shared engine's own data lives under `config/iris/`. -### Default `modded.json` keys +### `modded.json` -| Key | Default | Effect | +Written with these defaults on first read. If the file is unparseable Iris logs an error and falls back to defaults without rewriting it, so a syntax error is silent apart from the log line — check the log if a setting seems ignored. + +| Key | Default | What it does | |---|---|---| -| `defaultPack` | `overworld` | Pack auto-download and default create pack name | -| `autoDownloadDefaultPack` | `true` | Async prefetch of both managed beta packs and any configured non-managed default when missing | -| `primaryWorld` | `""` | Primary-world router target dimension id | -| `routePlayersToPrimaryWorld` | `true` | Route players from vanilla overworld when primary is set | -| `mainWorldPack` | `""` | Main-world generator override pack ref | -| `mainWorldSeed` | `0` | Seed for main-world override | -| `mainWorldAutoRestart` | `false` | Auto-restart related to main-world override | +| `defaultPack` | `overworld` | The pack `/iris create` uses when you don't name one, and the extra pack auto-download will fetch beyond the two managed betas | +| `autoDownloadDefaultPack` | `true` | Whether boot installs missing packs at all. Set false for air-gapped servers where you place pack folders by hand | +| `primaryWorld` | `""` | Dimension id players get routed into. Empty means no routing. Set by `/iris world replace-overworld` rather than by hand | +| `routePlayersToPrimaryWorld` | `true` | Whether the routing above actually happens. Set false to keep a primary world configured but stop moving players into it | +| `mainWorldPack` | `""` | Pack whose generator replaces the vanilla main world. Empty means the vanilla overworld is untouched | +| `mainWorldSeed` | `0` | Seed for that main-world override | +| `mainWorldAutoRestart` | `false` | When true, `/iris world mainworld` halts the server immediately so the override takes effect. Leave false unless you have a supervisor that restarts the process | -## Platform defaults (settings) +Only `/iris world replace-overworld`, `/iris world disable|delete` on the primary, and `/iris world mainworld` rewrite this file at runtime. -`IrisSettings` is shared across platforms. Generator default relevant to install and first world: +## Settings that affect install and first world -| Key path | Default | Effect | +`IrisSettings` is shared across every platform; only the file location differs. The three keys most likely to matter before your first world: + +| Key path | Default | When you'd change it | |---|---|---| -| `generator.defaultWorldType` | `overworld` | Bukkit `/iris create` resolves `type=default` to this pack/dimension key | -| `general.language` | `en_US` | Server locale selection | -| `studio.openVSCode` | `true` | Whether studio may launch VSCode | -| `studio.autoStartDefaultStudio` | `false` | Do not auto-open studio on boot | +| `generator.defaultWorldType` | `overworld` | Bukkit `/iris create` resolves `type=default` through this. Point it at your own pack so plain `/iris create ` produces your world instead of the stock overworld | +| `general.language` | `en_US` | Server-side message locale. See `08 - Localization.md` | +| `studio.openVSCode` | `true` | Set false on a headless box so `/iris studio vscode` writes the workspace file without trying to launch an editor | Full key list: `03 - Configuration.md`. -## First boot pack download +## First-boot pack download -| Platform | Behavior | +| Platform | What happens | |---|---| -| Plugin | `DefaultPackBootstrapProvisioner` independently manages the Overworld and Underworld beta assets under `packs/overworld` and `packs/underworld`, then compiles the aggregate datapack once | -| Mod | If `autoDownloadDefaultPack` is enabled, async install of both managed beta packs plus any distinct configured default into `config/irisworldgen/packs` | +| Plugin | `DefaultPackBootstrapProvisioner` installs the Overworld and Underworld beta release assets into `packs/overworld` and `packs/underworld` independently, then compiles the aggregate datapack once | +| Mod | If `autoDownloadDefaultPack` is on, a daemon thread installs both managed beta packs plus any distinct configured `defaultPack` into `config/irisworldgen/packs`, then regenerates the forced datapack | -Manual install: `/iris download ` (alias `dl`). `overworld` and `underworld` use their beta-release assets and ignore the branch argument; other packs use `IrisDimensions//` (default branch `stable` — see `25 - Pack Management.md`). +Manual install is `/iris download ` (alias `dl`). The two managed packs are special-cased: `overworld` and `underworld` always come from their pinned beta release assets and **ignore the `branch` argument entirely**. Any other pack resolves as `IrisDimensions//`, with `branch` defaulting to `stable`. -## Installation recovery +The managed-pack match is case-sensitive, so `/iris download Overworld` misses the special case and tries `IrisDimensions/Overworld/stable` instead. Use lowercase. -| Symptom | Check | Recovery | +One more branch inconsistency to be aware of on modded: when `/iris create` auto-downloads a pack that isn't installed, and when boot fetches a non-managed `defaultPack`, both use the `master` branch — not `stable`. Only the explicit `/iris download` command defaults to `stable`. Pin the branch explicitly if it matters. See `25 - Pack Management.md`. + +## When the install goes wrong + +| Symptom | Likely cause | Fix | |---|---|---| -| Iris does not appear in `/iris version` | Wrong directory, wrong platform jar, duplicate jar, Java mismatch, or an enable exception | Stop the server, keep only the matching artifact, confirm Java 25, and fix the first Iris exception in the startup log | -| `settings.json` exists but a managed pack is absent | Managed beta download failed or is still incomplete | Restore outbound HTTPS or install the complete release pack, then restart; do not create an empty pack folder | -| Pack validates but modded height/biomes use fallbacks | Forced datapack was generated after registries loaded | Restart once with the pack already installed, then create a new disposable world | -| Bukkit command is denied for a non-op | `iris.all` is missing | Grant `iris.all`; `iris.treefeller` controls only survival tree felling | -| Client HUD is absent but server commands work | Client mod missing, disabled keybind, or server capability not negotiated | Install the matching client mod, reconnect, and verify the Iris keybind category; server generation does not require the client HUD | -| Existing world ignores a newly installed pack | Production world is using its stored snapshot | Follow the explicit snapshot update or new-world workflow in `06 - Worlds & Lifecycle.md` and `25 - Pack Management.md` | +| `/iris version` does nothing | Wrong directory, wrong platform jar, a duplicate Iris jar, Java below 25, or an exception during enable | Stop the server, leave exactly one matching artifact in place, confirm Java 25, then fix the **first** Iris exception in the startup log — later ones are usually fallout | +| `settings.json` exists but a managed pack is missing | The beta download failed or is still running | Restore outbound HTTPS or drop in the complete release pack, then restart. Do not create an empty pack folder to paper over it | +| Players are kicked at login with an Iris message | Startup validation hasn't passed | Read the reason in the kick text and the console. External datapack failures lock login; fix the datapack state and restart | +| Pack validates, but modded heights and biomes are wrong | The forced datapack was generated after registries had already loaded | Restart once with the pack already on disk, then create a fresh disposable world to confirm | +| A non-op can't run any Iris command | `iris.all` isn't granted | Grant `iris.all`. `iris.treefeller` only covers survival tree felling and grants no commands | +| Client HUD missing but server commands work | Client mod absent, keybind unbound, or capability not negotiated | Install the matching client mod, reconnect, check the Iris keybind category. Server-side generation never depends on the client HUD | +| An existing world ignores a newly installed pack | The world is reading its frozen snapshot | Use the explicit snapshot update or create a new world — `06 - Worlds & Lifecycle.md` and `25 - Pack Management.md` | ## Native worldgen over Iris terrain -Iris replaces the chunk generator. Vanilla and mod worldgen only runs where Iris runs it. Identical on every platform: +Iris replaces the chunk generator outright, so vanilla and mod worldgen only runs where Iris explicitly runs it. This behaves the same on every platform. -| Vanilla / mod worldgen | Over Iris terrain | Control | +| Vanilla / mod worldgen | Runs over Iris terrain? | Control | |---|---|---| -| Structures (vanilla, datapack, mod) | Yes, on by default | `importedStructures.disabled` denies families; `disabledExact` denies one complete key | -| Placed features: ores, trees, plants, springs, geodes | Yes, **off by default** | `importedFeatures.enabled` per dimension, with per-step and per-key filters | -| Carvers (caves, canyons, mod carvers) | Never | No `NoiseGeneratorSettings` for a carver to sample; use pack `caves` / `carvings` | +| Structures (vanilla, datapack, mod) | Yes, on by default | Deny families with `importedStructures.disabled`, or one complete key with `importedStructures.disabledExact` | +| Placed features: ores, trees, plants, springs, geodes | Yes, but **off by default** | `importedFeatures.enabled` per dimension, with per-step (`steps` / `disabledSteps`) and per-key (`disabled`) filters | +| Carvers (caves, canyons, mod carvers) | Never | There's no `NoiseGeneratorSettings` for a carver to sample against. Use the pack's `caves` and `carvings` instead | | Surface builders and surface rules | Never | Iris builds surfaces from pack palettes | -| Mod biomes | Only as `derivative`, `vanillaDerivative`, `biomeScatter`, or `biomeSkyScatter` target | Iris chooses biomes from the pack | -| Mob spawning, including mod mobs | Yes | Biome spawn tables merged with the vanilla derivative’s | +| Mod biomes | Only as a `derivative`, `vanillaDerivative`, `biomeScatter`, or `biomeSkyScatter` target | Iris always picks the biome from the pack | +| Mob spawning, including mod mobs | Yes | Biome spawn tables are merged with the vanilla derivative's | -With `importedFeatures` off (default), chunk output is the pure Iris result. Full control reference: `94 - API - Modded.md` (also applies conceptually on Bukkit for imported native stages). +With `importedFeatures` off — the default — chunk output is pure Iris. The full control reference is `94 - API - Modded.md`, which also applies conceptually on Bukkit for the imported native stages. -Independently of that flag, Iris custom biomes inherit biome tags of their vanilla derivative on every platform, so tag-driven content (`#minecraft:is_overworld`, mod spawn rules, etc.) applies to Iris custom biomes. +Separately from that flag, Iris custom biomes inherit the biome tags of their vanilla derivative on every platform. Tag-driven content such as `#minecraft:is_overworld` and mod spawn rules therefore applies to Iris custom biomes without any extra configuration. ## Build artifacts -From repo root with JDK 25: +If you're building rather than downloading, from the repo root with JDK 25: -``` +```text ./gradlew buildAllToOut ``` -Output under `dist/`: +Four jars land in `dist/`. The CraftBukkit jar's version token is the supported Minecraft *range*; the loader jars use `+`: | Pattern | Platform | |---|---| -| `Iris v… [CraftBukkit] ….jar` | Plugin | -| `Iris v… [Fabric] ….jar` | Fabric | -| `Iris v… [Forge] ….jar` | Forge | -| `Iris v… [NeoForge] ….jar` | NeoForge | +| `Iris v [CraftBukkit] 26.1.2-26.2.jar` | Plugin (whole Bukkit family, including Folia) | +| `Iris v [Fabric] 26.2+.jar` | Fabric | +| `Iris v [Forge] 26.2+.jar` | Forge | +| `Iris v [NeoForge] 26.2+.jar` | NeoForge | -Next: create a world and open studio in `02 - Getting Started.md`. Settings detail in `03 - Configuration.md`. +Per-platform build tasks and the developer build gate are in `00 - Overview.md`. + +Next: create a world and open the Studio in `02 - Getting Started.md`. Every settings key is in `03 - Configuration.md`. diff --git a/docs/02 - Getting Started.md b/docs/02 - Getting Started.md index 71f636f93..30e069605 100644 --- a/docs/02 - Getting Started.md +++ b/docs/02 - Getting Started.md @@ -1,211 +1,226 @@ # 02 - Getting Started -This page walks through creating an Iris world, teleporting into it, running a short pregeneration, and opening a studio pack workspace. Command argument style differs by platform: Bukkit uses Director keyed optional parameters; modded uses Brigadier positional arguments and flag literals. +This page takes you from a working Iris install to a world you can stand in, a small pregenerated area, and an open Studio session for editing packs. It's written for whoever is running the server, and assumes nothing about pack authoring. Command syntax differs between the plugin and the mods, so each step gives both forms. Full command trees and permissions: `04 - Commands & Permissions.md`. World lifecycle detail: `06 - Worlds & Lifecycle.md`. Studio detail: `10 - Studio & VSCode Schemas.md`. -## Outcome +## What you'll end up with -At the end you will have one disposable Iris world created from the `overworld` pack, you will have entered it, generated a small known area, and opened a separate Studio authoring session. Use the fixed seed `1337` until the workflow is proven; changing seeds while diagnosing a pack makes comparisons ambiguous. +One disposable Iris world built from the `overworld` pack, entered and generating chunks, with roughly a 45×45-chunk area pregenerated, plus a separate Studio session pointed at the live pack. Use seed `1337` throughout. Changing seeds while you're still diagnosing something makes every comparison meaningless. -Treat each numbered section as a gate. Confirm the world is loaded before teleporting, confirm ordinary chunks generate before starting pregen, and confirm the Studio world is separate from the production snapshot before editing files. +Work through the sections in order and confirm each one before moving on. Confirm the world loaded before teleporting; confirm ordinary chunks generate around you before starting a pregen; confirm the Studio world is genuinely separate from your production world before editing files. Skipping a check doesn't save time here, because Iris failures surface late and in the wrong place. ## Prerequisites -- Iris installed per `01 - Installation & Platforms.md` -- Java 25 server or mod instance running -- Operator / gamemaster access (`iris` commands; modded mutating commands require permission level 2 / gamemasters) -- Managed Overworld and Underworld packs present (auto-downloaded on first boot) or the required project pack installed under the platform packs directory +- Iris installed and verified per `01 - Installation & Platforms.md` +- A Java 25 server or mod instance running +- Operator access on Bukkit (the `iris.all` permission), or permission level 2 / gamemaster on modded for anything that mutates state +- The managed `overworld` and `underworld` packs present, or your own pack installed under the platform's packs directory -## Argument style +## The one syntax rule that trips everyone up + +On the plugin, Iris uses the Director command framework, and it has a hard rule: **only required parameters accept a bare positional value. Every optional parameter must be given as `key=value`.** A leftover positional token isn't ignored — it's an error, and the command fails. + +```text +/iris create myworld type=overworld seed=1337 correct +/iris create myworld overworld 1337 fails: unexpected argument +``` + +Parameters marked contextual (like `world` on pregen, which normally comes from where you're standing) also never take a positional, so name them with `key=` when you need to override them. + +Modded is Brigadier and works the way you'd expect: everything is positional, in order, and pregen options are literal flag words. | Platform | Required args | Optional args | Example | |---|---|---|---| -| Plugin (Bukkit) | Positional in declaration order | Must be `key=value` | `/iris create myworld type=overworld seed=1337` | +| Plugin (Bukkit) | Positional, in declaration order | Must be `key=value` | `/iris create myworld type=overworld seed=1337` | | Mod (Fabric / Forge / NeoForge) | Positional | Further positional tokens or literal flags | `/iris create myworld overworld 1337` | -On Bukkit, a bare extra token that is not a known key is a hard error. On modded, pregen flags are combinable literals (`gui`, `sync`, `nocache`) after the radius / dimension / center. +Director also matches command and parameter names fuzzily, so shortenings and near-misses often resolve. That's convenient but don't rely on it in scripts — write the real names. ## 1. Create a world ### Plugin -``` -/iris create [type=…] [seed=…] [main=true|false] +```text +/iris create [type=…] [seed=…] [main=true|false] [overwrite=true|false] ``` -| Parameter | Aliases | Default | Meaning | +| Parameter | Aliases | Default | What it does | |---|---|---|---| -| `name` | `world-name` | (required) | World name | -| `type` | `dimension`, `pack` | `default` → `generator.defaultWorldType` (`overworld`) | Pack/dimension load key | +| `name` | `world-name` | required | The world name. The only parameter that takes a positional value | +| `type` | `dimension`, `pack` | `default` | Which pack/dimension to generate. The literal `default` is resolved at runtime through `generator.defaultWorldType` (stock value `overworld`), so it follows your config rather than being hardcoded | | `seed` | — | `1337` | World seed | -| `main` | `main-world` | `false` | If true, register a shutdown hook to promote this world as `level-name` in `server.properties` | +| `main` | `main-world` | `false` | Promote this world to the server's main world. See below — it does not take effect until a restart | +| `overwrite` | `force` | `false` | Replace an existing world in place instead of creating a new one. This is a staged, restart-to-publish operation, not something to reach for casually | -Aliases for the create command itself: `c`. +The command itself has alias `c`. -**Reserved names (plugin):** `iris` and `benchmark` are rejected (case-insensitive). Iris suggests using another name (for example `irisworld`). +`main=true` on a non-Folia server registers a JVM shutdown hook. On shutdown it copies `data/`, `datapacks/`, and `players/` from the current level root along with the Iris dimension folder into a new level directory, then rewrites `server.properties` with the new `level-name` and `level-seed`. Nothing changes while the server is up. On Folia the promotion is applied inline during staging instead, and rolls back the `bukkit.yml` entry and the staged folder if it fails. -**Already exists:** if the managed dimension root already exists, create aborts. +**Names Iris refuses.** `iris` and `benchmark` are rejected outright (case-insensitive) and Iris suggests something like `irisworld`. Before those checks, the name also has to be a safe single path segment matching `[a-z0-9_-]`, so anything containing `/`, `\`, or `..` is rejected, as is any name that would collide with a vanilla world slot — your server's `level-name`, `_nether`, or `_the_end`. Those produce a different message about only Iris-managed worlds being changeable. -**Folia:** runtime create is disabled. Iris stages world files, installs the pack snapshot, registers `bukkit.yml`, and tells you to **restart** the server. After restart the world can load. See `01 - Installation & Platforms.md`. +**Already exists.** Without `overwrite=true`, create aborts if the managed dimension folder is already there. That folder lives at `/dimensions/iris/`, not next to your server jar. -**Non-Folia:** create builds the world immediately via `IrisToolbelt.createWorld()` (production, not studio). +**Folia.** Runtime creation is disabled. Iris stages the world files, installs the pack snapshot, registers the world in `bukkit.yml`, and tells you to restart. After the restart the world generates and loads on its own from that registration — you don't need `/iris load`. -``` +**Everything else.** Create builds the world immediately through `IrisToolbelt.createWorld()`, as a production world (not a studio world). + +```text /iris create myworld type=overworld seed=1337 ``` -Run `/iris worlds` after the command. On non-Folia servers, `myworld` must appear as a loaded Iris world. On Folia, success is the staging-and-restart message; restart before continuing. +Now run `/iris worlds` (alias `accesslist`). It prints two lists — Iris worlds and plain Bukkit worlds. On a non-Folia server `myworld` must appear under Iris worlds. On Folia, success is the staging-and-restart message; restart before continuing. ### Mod -``` +```text /iris create [pack] [seed] ``` -| Parameter | Default | Meaning | +| Parameter | Default | What it does | |---|---|---| -| `name` | (required) | Dimension id fragment; normalized under namespace `irisworldgen` when not fully qualified | -| `pack` | `overworld` | Pack key (optional `pack:dimension` form when the pack’s dimension key differs) | +| `name` | required | Dimension id. A bare name is normalized into the `irisworldgen` namespace, so `myworld` becomes `irisworldgen:myworld` | +| `pack` | `overworld` | Pack key. Use the `pack:dimension` form when the pack's dimension key differs from its name | | `seed` | `1337` | Long seed | -Aliases: `c`. Equivalent world management lives under `/iris world create|enable` with the same enable path. +Alias `c`. You can't pass `seed` without also passing `pack`. -If the pack is not installed, create starts an async download of `IrisDimensions/` then injects the dimension. On success the dimension is live and re-injected on later startups. +The `pack:dimension` form has to be **quoted** — `"overworld:overworld"` — because Brigadier's unquoted string type doesn't accept a colon. Iris's own help text says the same thing. -``` +If the pack isn't installed, create prints a message, downloads `IrisDimensions/` on a background thread (from the `master` branch), then injects the dimension. Once that succeeds the dimension is live and gets re-injected on later startups. + +```text /iris create myworld overworld 1337 ``` -There is no separate “load” step on modded after a successful create. +There's no separate load step on modded. The same world management also lives under `/iris world create|enable`, where `create` is simply an alias of `enable`. That form requires the pack argument and has no `overworld` default. -Run `/iris world status` and confirm the new dimension uses pack `overworld`. Then run `/iris info irisworldgen:myworld` as a gamemaster and verify seed `1337` before teleporting. +Confirm with `/iris world status`, which lists each loaded Iris level with its pack and dimension key. Then run `/iris info` to check the seed. `/iris info` takes an optional greedy string that acts as a **substring filter** across dimension id, generator identity, and pack key — not a dimension selector — so `/iris info myworld` narrows the listing. The seed is only printed to gamemasters; at lower permission levels the line simply omits it. ## 2. Load a world (plugin only) -``` +```text /iris load ``` -Aliases: `import`. Requires an existing managed dimension directory on disk. Origin: player (Director `PLAYER`). Loads through `BukkitWorldReconciler` and registers the world with the server. +The real command node is `loadWorld`, with alias `import`; `/iris load` reaches it through fuzzy matching. It requires the managed dimension directory to already exist on disk, then loads through `BukkitWorldReconciler` and registers the world with the server. -Modded worlds created with `/iris create` or `/iris world enable` are already injected; use teleport instead of load. +This one is **player-origin only** — the console cannot run it. On a headless server, load worlds by having them registered in `bukkit.yml` (which create already does) and restarting, or run it as a player. + +Modded worlds created with `/iris create` or `/iris world enable` are already injected. Teleport instead. ## 3. Teleport ### Plugin -``` +```text /iris teleport [player=…] ``` -Aliases: `tp`. Teleports the target (or the executing player) to the world spawn asynchronously when possible. +Alias `tp`. The world is positional; the player is optional and therefore keyed — `/iris tp myworld player=Notch`. Left out, it targets whoever ran the command, so console needs to name a player explicitly or it reports that the player doesn't exist. The teleport itself is performed asynchronously where the platform allows it. -``` +```text /iris tp myworld ``` -Success is a completed teleport followed by normal chunk generation around spawn. If the teleport target is missing, return to the create/load gate instead of retrying pregen. +You've passed this gate when the teleport completes and chunks generate normally around spawn. If the teleport target doesn't exist, go back to the create/load step — don't push on to pregen. ### Mod -``` +```text /iris teleport [player] /iris tp [player] ``` -Dimension is a loaded level argument (tab-completes Iris dimensions). Console must name a player. Teleport target is a fixed spawn-like position in the Iris dimension (engine-managed placement). +Dimension is a loaded-level argument and tab-completes Iris dimensions; a non-Iris dimension is rejected. Console must name a player. You land at x=8.5, z=8.5 in that dimension, at the `MOTION_BLOCKING` height for that column, and Iris force-loads the chunk with a ticket if it isn't loaded yet. -``` +```text /iris tp irisworldgen:myworld ``` -Success is entry into `irisworldgen:myworld` with `/iris info irisworldgen:myworld` still reporting the expected pack and seed. +Success is entry into `irisworldgen:myworld` with `/iris info myworld` still reporting the pack and seed you expect. ## 4. Pregenerate -Radius is in **blocks**. One pregeneration job runs server-wide. +Radius is in **blocks**, measured from the center outward, and one pregeneration job runs server-wide at a time. + +The block radius is converted to an inclusive chunk box, so a 352-block radius at `0,0` covers chunks -22 through 22 on both axes: **45 × 45 = 2,025 chunks**, or 720 × 720 blocks of actual generated area. The command's own feedback describes the request as 704 × 704 blocks (radius × 2); the extra chunk on each edge is the inclusive rounding. Pick your radius knowing that chunk count, not the block number, is what determines how long this takes. ### Plugin -``` +```text /iris pregen start [world=…] [center=x,z|me] [gui=true|false] [serial=true|false] ``` -| Parameter | Default | Notes | -|---|---|---| -| `radius` | (required) | Blocks; must be > 0 | -| `world` | contextual (sender’s world) | Target world | -| `center` | `0,0` | Or `me` for player position; aliases `middle` | -| `gui` | `true` | Open pregen GUI when available | -| `serial` | `false` | One chunk at a time; requires Paper-compatible server | +| Parameter | Aliases | Default | What it does | +|---|---|---|---| +| `radius` | `size` | required | Radius in blocks; must be greater than 0. The only positional parameter | +| `world` | — | your current world | Target world. Contextual, so it must be keyed when you override it — typically when running from console | +| `center` | `middle` | `0,0` | Center point. `me` uses the running player's position | +| `gui` | — | `true` | Open the pregen progress window. Set false on a headless server | +| `serial` | — | `false` | Generate one chunk at a time. Much slower, but the safe option when parallel generation is destabilizing the server. Requires a Paper-compatible server | -Control: - -``` -/iris pregen stop -/iris pregen pause -/iris pregen status -``` - -Example: - -``` +```text /iris pregen start 352 world=myworld center=0,0 gui=false ``` -Immediately run `/iris pregen status`. A 352-block radius centered at `0,0` should report a 2,025-chunk job and advance without a growing failed count. +Immediately run `/iris pregen status`. It should report a 2,025-chunk job that advances without a growing failure count. + +Control it with `/iris pregen stop` (alias `x`), `/iris pregen pause`, and `/iris pregen status`. **`resume` is an alias of `pause`, and `pause` is a toggle** — there's no distinct resume command, so running either one on a paused job resumes it and on a running job pauses it. ### Mod -``` +```text /iris pregen start [dimension] [at ] [gui] [sync] [nocache] ``` -| Piece | Meaning | +| Piece | What it does | |---|---| -| `radius` | 1–100000 blocks | -| `dimension` | Optional level; defaults to current dimension | -| `at x z` | Optional center (default 0, 0) | -| `gui` | Request progress map window on the server display when GUI is launchable | +| `radius` | Blocks, 1–100000 | +| `dimension` | Optional target level; defaults to the dimension you're in | +| `at x z` | Optional center; defaults to 0, 0 | +| `gui` | Ask for the progress map window on the server display. Silently ignored when no GUI can be launched | | `sync` | Synchronous chunk writes | -| `nocache` | Disable resumable checkpoint cache (default is cached / resumable) | +| `nocache` | Disable the resumable checkpoint cache. Caching is on by default, which is what lets a stopped job pick up where it left off | -Flags are optional and combinable in any order after the radius/dimension/center prefix. +The three flags are combinable in any order and each may appear once, but **`at ` must come before any flag.** `/iris pregen start 100 gui at 0 0` is a syntax error; `/iris pregen start 100 at 0 0 gui` is fine. -``` +```text /iris pregen start 352 irisworldgen:myworld at 0 0 sync ``` -Immediately run `/iris pregen status` and confirm the target dimension, total, and generated count. Use `/iris pregen stop` before retrying with different flags. +Run `/iris pregen status` right away and confirm the target dimension, total, and generated count. Stop before retrying with different flags. As on Bukkit, `pause` and `resume` are the same toggle. Progress shows in the client mod HUD when present, otherwise a boss bar or the console. -Control: `/iris pregen stop`, `pause` / `resume`, `status`. Progress: client mod HUD when present, otherwise boss bar / console. +## 5. Open a Studio -## 5. Studio (first authoring steps) - -Studio worlds are transient: closed on command, purged at startup. They read the **live** pack and hotload JSON/object edits into newly generated chunks. Production worlds do not (see pitfalls below). +Studio worlds are transient. They're discarded when you close them and any leftovers are purged at startup. Crucially, a Studio world reads the **live** pack directory and hotloads JSON and object edits into newly generated chunks. Production worlds never do this — they read the frozen snapshot copied into the world at creation. That difference is the reason Studio exists, and the reason pack edits appear to do nothing on a production world. ### Plugin -``` -/iris studio create [name=studio] [template=…] -/iris studio open [seed=1337] -/iris studio vscode [dimension=default] +```text +/iris studio create [name=…] [template=…] +/iris studio open [seed=…] +/iris studio vscode [dimension=…] /iris studio close ``` | Command | Aliases | Notes | |---|---|---| -| `create` | `+` | Omitting template scaffolds a **starter** pack (minimal dimension/region/biome/generator). Providing a template copies an existing packs entry (or downloads it) | -| `open` | `o` | Temporary studio world for the pack | -| `vscode` | `vsc` | Write / open a `.code-workspace` with live registry schemas | -| `close` | `x` | Discard studio world | +| `create` | `+` | Both parameters are optional, so **neither takes a positional value** — use `name=mypack`. With no template it scaffolds a minimal starter pack (`dimensions/`, `regions/`, `biomes/`, `generators/` with one of each). With a template it copies an existing packs entry, downloading it if needed | +| `open` | `o` | Opens a temporary studio world for a pack. `dimension` is required and positional; `seed` is optional (alias `s`) and defaults to `1337` | +| `vscode` | `vsc` | Writes and opens a `.code-workspace` with live registry schemas. `dimension` is optional, so keyed only, and defaults to `default` | +| `close` | `x` | Discards the studio world | -Default create name is `studio`; if that folder already exists, Iris picks the next free name. +The studio group itself has aliases `std` and `s`. Default create name is `studio`; if a project by that name already exists, Iris picks the next free name rather than failing. + +```text +/iris studio open overworld seed=1337 +/iris studio vscode dimension=overworld +``` ### Mod -``` +```text /iris studio create [name] [template] /iris studio open [seed] /iris studio vscode [pack] @@ -213,79 +228,80 @@ Default create name is `studio`; if that folder already exists, Iris picks the n /iris studio close ``` -| Command | Notes | -|---|---| -| `create` / `+` | Defaults: name `studio`, template **`example`** (differs from Bukkit starter-pack path when template omitted) | -| `open` / `o` | Pack required; seed default `1337` | -| `vscode` / `vsc` | Generate workspace | -| `update` | Regenerate schemas only | -| `close` / `x` | Discard studio | +| Command | Aliases | Notes | +|---|---|---| +| `create` | `+` | Name defaults to `studio`, template defaults to **`example`**. This differs from the plugin, where omitting the template scaffolds a starter pack instead. The template pack is auto-downloaded (from `master`) if missing | +| `open` | `o` | Pack is required; seed defaults to `1337` | +| `vscode` | `vsc` | Writes the workspace and opens it | +| `update` | — | Regenerates the workspace schemas without opening anything | +| `close` | `x` | Discards the studio world | -Some Bukkit studio tools (importvanilla feature capture, loot GUI, profile, etc.) refuse or redirect on modded with an explicit message; capture vanilla features on Bukkit and copy the pack folder if needed. - -Plugin example: - -```text -/iris studio open overworld seed=1337 -/iris studio vscode dimension=overworld -``` - -Modded example: +Group aliases are `std` and `s`. ```text /iris studio open overworld 1337 /iris studio vscode overworld ``` -The Studio gate passes when the transient Studio world opens, the workspace points at the live `packs/overworld/` tree, and a saved valid JSON change produces a hotload result. Close it with `/iris studio close`; production `myworld` must remain separate. +A number of Bukkit studio and content tools deliberately refuse on modded and print an explanatory message rather than half-working: `importvanilla` (`importv`, `iv`), `loot`, `profile`, `spawn`/`summon`, `objects`/`find-objects`, the object `we`, `studio`, and `convert` subcommands, structure `import`/`import-all`/`reimport`, and datapack `ingest`/`pull`/`remove`. Do that work on a Bukkit server and copy the pack folder across. -## Suggested first-session flow +The Studio gate passes when the transient world opens, the workspace points at the live `packs/overworld/` tree, and saving a valid JSON change produces a hotload result in-game. Close it with `/iris studio close` and confirm your production `myworld` is still there and unaffected. -1. Confirm pack: ensure `overworld` (or your pack) exists under the platform packs directory. -2. Create world (plugin or mod forms above). -3. On Folia plugin: restart after staging, then load if needed. -4. Teleport into the world. -5. Optional: `/iris pregen start 352 …` for a small square (~704×704 blocks). -6. Optional: `/iris studio open ` to edit live; use VSCode schemas for autocomplete of blocks/items/entities (mod content included on mod loaders). +## The whole first session -The session passes when the production world loads again after a clean restart and generates new chunks from its copied pack snapshot. Remove a disposable world only through the lifecycle command after evacuating players; see `06 - Worlds & Lifecycle.md`. +1. Confirm the pack: `overworld` (or yours) exists under the platform packs directory. +2. Create the world using the form for your platform. +3. On Folia only: restart after the staging message. The world comes back on its own. +4. Teleport in and fly around a little to confirm chunks generate. +5. Optional: `/iris pregen start 352 …` for a 45×45-chunk area. +6. Optional: `/iris studio open ` and use the VSCode schemas for block, item, and entity autocomplete — mod content is included in those schemas on mod loaders. + +The session is genuinely finished when you restart the server cleanly, the production world loads again, and it generates new chunks from its copied pack snapshot. Remove a disposable world through the lifecycle command after evacuating players, never by deleting folders — see `06 - Worlds & Lifecycle.md`. ## Common pitfalls -| Pitfall | What happens | What to do | +| Pitfall | What actually happens | What to do | |---|---|---| -| World name `iris` or `benchmark` (plugin) | Create rejected | Use another name | -| Editing `packs/` after production create | **No effect** on existing worlds | Production engines read `/iris/pack` snapshot. Push with `/iris developer update-world world= pack= confirm=true` (Bukkit, all keyed) and restart; or only new chunks after update. Studio reads live packs | -| Expecting pack edits in old chunks | Only new chunks use new config | Fly to unexplored terrain, pregen fresh radius, or use studio | -| Folia: create then teleport immediately | World not live yet | Restart after staging message, then load/teleport | -| Mod: new pack heights/biomes missing | Forced datapack not yet applied | Restart after installing pack | -| `/iris load` on modded | No equivalent subcommand | Use create/enable + teleport | -| Bukkit optional args without `key=` | Parse error | Use `seed=1337`, not a bare second number for optional params | -| Mod pregen while another job runs | Start fails | `/iris pregen stop` then start again | -| Studio closed mid-edit | World discarded | Edits on disk in `packs/` remain; reopen studio | -| Managed pack download blocked | Startup or create/open fails with a missing pack | Allow HTTPS or install with `/iris download overworld` and `/iris download underworld`; an offline install must contain each complete pack tree | -| `type=default` vs pack key | Resolves via `generator.defaultWorldType` | Prefer explicit `type=overworld` or your pack key | +| Bukkit optional args passed positionally | Hard parse error, command does nothing | Write `seed=1337`, not a bare second token | +| `/iris studio create mypack` | Fails — both params are optional so neither is positional | `/iris studio create name=mypack` | +| `/iris pack validate` with no argument | Missing-argument error, not "validate everything" | Name the pack, or pass an explicitly empty `pack=` to do all of them | +| World named `iris` or `benchmark` | Create rejected | Pick another name, e.g. `irisworld` | +| Editing `packs/` after creating a production world | **No effect** on that world, ever | Production engines read `/iris/pack`. Push changes with `/iris developer update-world world= pack= confirm=true` and restart, or accept that only new chunks change. Studio reads the live pack | +| Expecting pack edits to change existing chunks | Only newly generated chunks use the new config | Fly to unexplored terrain, pregen a fresh radius, or use a Studio world | +| Folia: create then teleport immediately | The world isn't live yet | Restart after the staging message, then teleport | +| Modded: new pack's heights or biomes missing | The forced datapack wasn't applied before registries loaded | Restart once with the pack already installed | +| `/iris load` from console | Player-origin only; console can't run it | Rely on the `bukkit.yml` registration plus a restart, or run it as a player | +| `/iris load` on modded | No such subcommand | Use create or `world enable`, then teleport | +| Modded `pack:dimension` unquoted | Brigadier rejects the colon | Quote it: `"overworld:overworld"` | +| Modded pregen flags before `at x z` | Syntax error | Put `at ` before any flag | +| Starting a pregen while one is running | Start fails | `/iris pregen stop` first | +| `/iris pregen resume` expected to only resume | It's an alias of `pause`, which toggles | Check `/iris pregen status` instead of assuming | +| Studio closed mid-edit | The studio world is discarded | Your edits are on disk in `packs/` and survive. Reopen the studio | +| Managed pack download blocked | Startup, create, or studio open fails with a missing pack | Allow HTTPS or run `/iris download overworld` and `/iris download underworld`. An offline install must contain each complete pack tree | +| Relying on `type=default` | Resolves through `generator.defaultWorldType`, which someone may have changed | Name the pack explicitly: `type=overworld` | ## Quick reference **Plugin** -``` +```text /iris create myworld type=overworld seed=1337 /iris tp myworld /iris pregen start 352 world=myworld center=0,0 gui=false +/iris pregen status /iris studio open overworld seed=1337 /iris studio close ``` **Mod** -``` +```text /iris create myworld overworld 1337 /iris tp irisworldgen:myworld /iris pregen start 352 irisworldgen:myworld at 0 0 +/iris pregen status /iris studio open overworld 1337 /iris studio close ``` -Next: pack structure in `05 - Concepts & Pack Layout.md`, configuration in `03 - Configuration.md`. +Next: how packs are structured in `05 - Concepts & Pack Layout.md`, and every settings key in `03 - Configuration.md`. diff --git a/docs/03 - Configuration.md b/docs/03 - Configuration.md index 4f222997e..b0d9b8c73 100644 --- a/docs/03 - Configuration.md +++ b/docs/03 - Configuration.md @@ -1,19 +1,45 @@ # 03 - Configuration -Iris stores shared runtime settings in `settings.json` under the platform data folder. On first boot Iris writes a full defaults file if missing; every successful load rewrites the file so new keys appear with defaults. See `01 - Installation & Platforms.md` for data paths and `33 - Performance Tuning.md` for tuning guidance. +Iris keeps its shared runtime settings in `settings.json` under the platform data folder. On first boot Iris writes a full defaults file if one is missing, and every successful load rewrites the file so new keys appear with defaults. Bukkit adds `compat.json`; mod loaders add `modded.json`. See `01 - Installation & Platforms.md` for data paths and `33 - Performance Tuning.md` for how to measure a tuning change. -## Tutorial: change one setting safely +## What you actually need to change + +The shipped defaults are correct for almost every server. Most operators only ever touch a handful of keys: + +| You want to | Change | +|---|---| +| Run the server in another language | `general.language` | +| See why generation is behaving oddly | `general.debug`, or `/iris debug` | +| Stop Iris opening desktop windows on the host | `gui.useServerLaunchedGuis`, `studio.openVSCode` | +| Survive pregen on a memory-constrained box | `pregen.maxResidentTectonicPlates`, the `performance.*CacheSize` keys | +| Turn on the survival tree feller | `treeFeller.enabled` | +| Catch broken pack keys instead of silently ignoring them | `general.strictContentKeys` | + +Everything else is either already right, only meaningful while diagnosing a specific problem, or inert on your platform. Each table below marks which is which. + +## File locations + +| Platform | Shared settings | Packs root | Platform-only config | +|----------|-----------------|------------|----------------------| +| Bukkit / Paper / Folia | `plugins/Iris/settings.json` | `plugins/Iris/packs/` | `plugins/Iris/compat.json` | +| Fabric / Forge / NeoForge | `/iris/settings.json` | `/irisworldgen/packs/` | `/irisworldgen/modded.json` | + +`` is the loader config directory (game `config/` on Fabric, Forge, and NeoForge). Both surfaces use the same `IrisSettings` schema for `settings.json`. + +The modded split is real and easy to get wrong: the engine data folder is `/iris`, but installed packs, the generated datapack, and `modded.json` live under `/irisworldgen`. Iris also creates an empty `/iris/packs` directory; that is not the pack root and putting a pack there will not load it. + +## Changing a setting safely 1. Start Iris once so it writes the current schema and defaults. 2. Copy `settings.json` outside the server directory as a rollback file. -3. Change one key only. Keep its JSON type unchanged; quoted numbers and strings such as `"false"` are not booleans. -4. Save the file and use `/iris reload`, or wait for the platform hotload interval described below. -5. Confirm the console reports the settings reload without a parse exception. -6. Exercise the affected feature. For performance or thread-pool settings, restart before judging the result because some values are read when services are constructed. +3. Change one key. Keep its JSON type: quoted values such as `"false"` are strings, not booleans. +4. Save the file, then run `/iris reload` or wait for the hotload poll (about 3 seconds on both platforms). +5. Confirm the console logs `Hotloaded settings.json` or the reload success message, with no parse error. +6. Exercise the affected feature. If nothing changed, check the "Takes effect" column below — several keys are captured when a service, pool, or cache is constructed and need a restart. -If parsing fails, restore the saved file and restart. Do not delete `settings.json` unless resetting every setting to defaults is intentional. +If parsing fails, restore the saved file and restart. Do not delete `settings.json` unless resetting every setting to defaults is what you want. -For example, to change only the server locale, edit the existing `general` object in place: +To change only the server locale, edit the existing `general` object in place: ```json { @@ -23,201 +49,213 @@ For example, to change only the server locale, edit the existing `general` objec } ``` -This fragment shows the field location; do not replace a populated settings file with the fragment. After `/iris reload`, run `/iris help` and confirm the selected locale is active. Iris rewrites the complete settings file after a successful load, including defaults for fields that were absent. +That fragment shows the field location; do not replace a populated settings file with it. After `/iris reload`, run `/iris help` and confirm the selected locale is active. Iris rewrites the complete settings file after a successful load, including defaults for fields that were absent. ### Validation and rollback | Result | Meaning | Action | |---|---|---| -| Reload succeeds and the affected feature changes | File parsed and the setting reached a live reload path | Keep the backup until the next clean restart | -| Reload succeeds but behavior is unchanged | Setting is read only when a service or engine is constructed | Restart, then retest the same workload | -| Parse exception or requested locale rejected | JSON shape, type, or locale is invalid | Restore the backup, reload, and make one smaller edit | -| File is rewritten with defaults | Missing fields were normalized by `IrisSettings` | Reapply only intentional overrides; do not restore an obsolete full file over new defaults | -| Modded and Bukkit paths differ | The wrong data root was edited | Use the path table below and confirm the changed file timestamp before reloading | - -## File locations - -| Platform | Shared settings | Packs root | Modded-only config | -|----------|-----------------|------------|--------------------| -| Bukkit / Paper / Folia | `plugins/Iris/settings.json` | `plugins/Iris/packs/` | — | -| Fabric / Forge / NeoForge | `/iris/settings.json` | `/irisworldgen/packs/` | `/irisworldgen/modded.json` | - -`` is the loader config directory (game `config/` for Fabric/Forge/NeoForge). Both surfaces use the same `IrisSettings` schema for `settings.json`. +| Reload succeeds and the feature changes | The file parsed and the setting is read live | Keep the backup until the next clean restart | +| Reload succeeds but behavior is unchanged | The value was captured when a service, pool, or cache was built | Restart, then retest the same workload | +| Parse error in console, file unchanged | Gson threw before the rewrite, so your broken file is still on disk and Iris is running built-in defaults | Fix the JSON, reload; restore the backup if you cannot | +| File is rewritten with defaults | Missing or unknown fields were normalized by `IrisSettings` | Reapply only intentional overrides; do not restore an obsolete full file over new defaults | +| Modded and Bukkit paths differ | The wrong data root was edited | Use the path table above and confirm the file timestamp changed before reloading | ## Load, save, hotload | Action | Behavior | |--------|----------| | First boot | Create `settings.json` with current defaults if the file is absent | -| Load | Parse with Gson into `IrisSettings`; on parse failure log and keep empty defaults for that boot | -| After load | Rewrite `settings.json` (pretty JSON) so new keys and migrated values persist | -| `/iris reload` | Invalidate cached settings, re-read `settings.json`, reload locale | -| Hotload (Bukkit) | `SettingsHotloadWatch` via VolmLib `ConfigHotloadEngine`; on content change invalidates, reloads, reloads language, logs `Hotloaded settings.json` | -| Hotload (modded) | `ModdedSettingsHotloadService` polls every 3s; on `lastModified` change invalidates, reloads, reloads language, logs `Hotloaded settings.json` | -| Locale-only tick | Hotload paths also call `IrisLanguage.update()` when the file is unchanged | -| `forceSave()` | Used by `/iris debug` and similar toggles that mutate settings in memory | +| Load | Parse with Gson into `IrisSettings`. On failure, log `Configuration Error in settings.json!` and run on built-in defaults for that boot — the bad file is left untouched, because the rewrite never runs | +| After a successful load | Rewrite `settings.json` as pretty JSON so new keys and migrated values persist. Comments and hand formatting are lost | +| `/iris reload` | Invalidate the cached settings, re-read the file, reload the locale. On modded it also schedules a forced datapack regeneration. It does not restart services, reload packs, or rebuild engines | +| Hotload (Bukkit) | `SettingsHotloadWatch` polls every 60 ticks (about 3 s) through the VolmLib `ConfigHotloadEngine`. A `lastModified` or size change triggers a read; the reload only runs if the normalized file content actually differs. Logs `Hotloaded settings.json` | +| Hotload (modded) | `ModdedSettingsHotloadService` polls `lastModified` every 3 s. Because a load rewrites the file, a touch with no edit still produces one reload (it does not loop). Logs `Hotloaded settings.json` | +| Locale refresh | Bukkit calls `IrisLanguage.update()` on every poll; modded calls it only when the file is unchanged, and calls a full `IrisLanguage.reload()` when it did change | +| `forceSave()` | Only `/iris debug` writes settings back from memory | -Legacy migration: if raw JSON still has `world.anbientEntitySpawningSystem`, it is copied to `world.ambientEntitySpawningSystem` and logged once. +Legacy migration: if the raw JSON still contains `world.anbientEntitySpawningSystem`, the value is copied to `world.ambientEntitySpawningSystem` and logged once. ## Root object -Top-level Gson fields on `IrisSettings` (all nested objects are created with defaults when missing): +Top-level Gson fields on `IrisSettings`. Every nested object is created with defaults when missing. -| Field | Nested class | Purpose | -|-------|--------------|---------| -| `general` | `IrisSettingsGeneral` | Language, debug, colors, datapack ingest, strict keys, splash | -| `world` | `IrisSettingsWorld` | Entity systems, async tick, WorldEdit CUI, pregen cache | -| `gui` | `IrisSettingsGUI` | Server-launched GUIs and pregen GUI options | -| `autoConfiguration` | `IrisSettingsAutoconfiguration` | Spigot/Paper timeout autoconfig, custom-biome restart | -| `generator` | `IrisSettingsGenerator` | Default world type, leaf decay | -| `concurrency` | `IrisSettingsConcurrency` | Runtime thread helpers only (no persisted fields) | -| `studio` | `IrisSettingsStudio` | Studio open/VSCode/weather/spawn defaults | -| `performance` | `IrisSettingsPerformance` | Mantle, caches, SIMD, nested engine SVC | -| `pregen` | `IrisSettingsPregen` | Pregen scheduler, mantle residency, timeouts | -| `sentry` | `IrisSettingsSentry` | Error reporter options | -| `treeFeller` | `IrisSettingsTreeFeller` | Survival tree feller enable and axe durability | +| Field | Nested class | Covers | +|-------|--------------|--------| +| `general` | `IrisSettingsGeneral` | Locale, debug output, console colors, datapack ingest, strict keys, splash | +| `world` | `IrisSettingsWorld` | Entity systems, async world tick, WorldEdit CUI, pregen cache | +| `gui` | `IrisSettingsGUI` | Server-launched desktop GUIs | +| `autoConfiguration` | `IrisSettingsAutoconfiguration` | Spigot/Paper server-file fixups, custom-biome restart | +| `generator` | `IrisSettingsGenerator` | Default pack for world creation, leaf decay | +| `concurrency` | `IrisSettingsConcurrency` | Nothing configurable — see below | +| `studio` | `IrisSettingsStudio` | Studio world behavior | +| `performance` | `IrisSettingsPerformance` | Mantle residency, loader caches, SIMD, engine service pool | +| `pregen` | `IrisSettingsPregen` | Pregen scheduling, mantle backpressure, timeouts | +| `sentry` | `IrisSettingsSentry` | Error reporter | +| `treeFeller` | `IrisSettingsTreeFeller` | Survival tree feller | -Static helper `IrisSettings.getThreadCount(int c)`: for `c` in `{-1,-2,-4}` returns `max(availableProcessors / -c, 1)`; otherwise `max(c, 2)` floored to at least 1. +Static helper `IrisSettings.getThreadCount(int c)`: for `c` in `{-1, -2, -4}` it returns `max(availableProcessors / -c, 1)`; otherwise `max(c, 2)`, floored at 1. -## `general` +## `general` — locale, diagnostics, and console output -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `language` | string | `"en_US"` | Active locale key; reloaded by `/iris reload` and hotload | -| `commandSounds` | boolean | `true` | Tab-complete amethyst chime on Bukkit when true | -| `debug` | boolean | `false` | Toggled by `/iris debug`; saved immediately | -| `dumpMantleOnError` | boolean | `false` | Dump mantle plates when tectonic errors occur | -| `disableNMS` | boolean | `false` | Disable NMS bindings when true | -| `pluginMetrics` | boolean | `true` | Plugin metrics reporting | -| `splashLogoStartup` | boolean | `true` | Console splash on enable | -| `useConsoleCustomColors` | boolean | `true` | Custom colors for console senders | -| `useCustomColorsIngame` | boolean | `true` | Custom colors for player senders | -| `adjustVanillaHeight` | boolean | `false` | Adjust vanilla height handling | -| `autoIngestDatapacks` | boolean | `true` | Validate and ingest configured external datapacks during the startup admission gate; unchanged committed content reuses its persisted result without another remote/full validation, and managed structures remain scoped to declaring Iris dimensions | -| `autoImportDatapackStructures` | boolean | `false` | Opt-in bulk write of every registered datapack structure as editable Iris resources; prefer `/iris structure import ` | -| `strictContentKeys` | boolean | `false` | Unresolved pack content keys and bad block-state properties become blocking pack errors; system property `-Diris.strictContent` overrides when set | -| `spinh` | int | `-20` | Splash / spin color H | -| `spins` | int | `7` | Splash / spin color S | -| `spinb` | int | `8` | Splash / spin color B | +This group decides what Iris says and how loudly. `language`, `debug`, and `strictContentKeys` are the ones worth touching; the colour and spin keys are cosmetic; the datapack keys change startup work on Bukkit only. -## `world` +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `language` | `"en_US"` | Live | Selects the locale catalog for all Iris messages. Reloaded by `/iris reload` and by both hotload watchers | +| `commandSounds` | `true` | Live | **Bukkit only.** Plays the amethyst chime on `/iris` tab completion and success/failure sounds after a command. Turn off if the noise annoys staff | +| `debug` | `false` | Live | Enables verbose engine tracing on the console and writes per-chunk crash dumps under `debug/chunk-errors/`. Toggle with `/iris debug` rather than editing by hand; leave off in production because it is loud | +| `dumpMantleOnError` | `false` | Live | When a tectonic plate read reports an error, dump the decoded region to `dump/.bin` instead of logging a timing line. Turn on only when investigating mantle corruption | +| `disableNMS` | `false` | **Restart** | **Bukkit only.** Forces the no-op NMS binding. Iris logs a warning and world creation stops working entirely, so this is a diagnostic escape hatch, not a compatibility switch. Read in a class initializer, so a reload will not change it | +| `pluginMetrics` | `true` | **Restart** | **Bukkit only.** Registers the bStats reporter at enable | +| `splashLogoStartup` | `true` | **Restart** | Prints the ASCII logo and version block at startup. Set false for quieter console logs | +| `useConsoleCustomColors` | `true` | Live | Gradient/hex colouring for console output. Set false if your log viewer mangles it — you still get legacy colour codes. Iris also forces both colour keys off in memory if Adventure fails to bind | +| `useCustomColorsIngame` | `true` | Live | Same, for messages sent to players | +| `adjustVanillaHeight` | `false` | **Restart** | **Bukkit only.** Overwrites the vanilla `overworld`/`the_nether`/`the_end` dimension-type JSON with Iris height when compiling the datapack. It is part of the datapack fingerprint, so flipping it forces a datapack rebuild | +| `autoIngestDatapacks` | `true` | **Restart** | **Bukkit only.** Downloads and installs configured `datapackImports` during the startup admission gate. Unchanged committed content reuses its persisted result instead of revalidating; managed structures stay scoped to the declaring Iris dimensions | +| `autoImportDatapackStructures` | `false` | Live (next ingest) | **Bukkit only.** Converts every registered datapack structure into editable Iris pools, pieces, and objects — thousands of files in your pack folder. Native generation never needs those copies, so leave it off and run `/iris structure import ` when you actually want them | +| `strictContentKeys` | `false` | Live | Promotes unresolved pack content keys and bad block-state properties from warnings to blocking pack errors. Worth turning on while developing a pack. `-Diris.strictContent` overrides it in both directions, and the bare property with no value counts as true | +| `spinh` | `-20` | Live | Hue factor of the animated "aura" gradient on Iris text | +| `spins` | `7` | Live | Saturation factor of the same gradient | +| `spinb` | `8` | Live | Brightness factor of the same gradient | -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `postLoadBlockUpdates` | boolean | `true` | Post-load block updates | -| `forcePersistEntities` | boolean | `true` | Force entity persistence | -| `ambientEntitySpawningSystem` | boolean | `true` | Ambient entity spawning (legacy key `anbientEntitySpawningSystem` migrated) | -| `asyncTickIntervalMS` | long | `700` | World manager async tick interval ms | -| `targetSpawnEntitiesPerChunk` | double | `0.95` | Target entity density per chunk | -| `markerEntitySpawningSystem` | boolean | `true` | Marker-driven entity spawning | -| `effectSystem` | boolean | `true` | Engine effects | -| `worldEditWandCUI` | boolean | `true` | WorldEdit wand CUI integration (Bukkit) | -| `globalPregenCache` | boolean | `false` | Global pregen cache | +## `world` — entity systems and the async world tick -If both `markerEntitySpawningSystem` and `ambientEntitySpawningSystem` are false, the world manager skips related entity work. +Iris runs its own spawning and effects pass on a background loop, separate from vanilla mob spawning. These keys decide whether that loop does anything and how often. Turning the spawn systems off makes Iris worlds feel emptier but removes an entire class of tick cost; the defaults are the intended experience. -## `gui` +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `postLoadBlockUpdates` | `true` | Live | Runs a block-update pass over freshly generated chunks near players so placed objects settle (physics and waterlogging fixups). Turning it off is faster but leaves floating or unwatered blocks from some objects | +| `forcePersistEntities` | `true` | Live | Marks every Iris-spawned entity persistent so vanilla mob-cap and distance rules do not despawn it. Turn off if pack-spawned mobs are accumulating | +| `ambientEntitySpawningSystem` | `true` | Live | Enables the biome/region ambient spawn lists on the async tick (legacy key `anbientEntitySpawningSystem` is migrated automatically) | +| `asyncTickIntervalMS` | `700` | Live (next tick) | Milliseconds between world-manager passes that handle spawning, effects, and cleanup. Raise it to cut background cost on a busy server; lower it only if pack spawns feel too sparse | +| `targetSpawnEntitiesPerChunk` | `0.95` | Live | Entity saturation ceiling. Once entities per loaded chunk exceed this, Iris stops spawning (the Bukkit path also backs off for 5 seconds). Lower it on servers already near their entity budget | +| `markerEntitySpawningSystem` | `true` | Live | Enables spawning driven by mantle marker blocks, which is how packs place specific mobs at specific generated features | +| `effectSystem` | `true` | Live | Applies per-biome and per-region `IrisEffect`s (potion effects, particles, sounds) to players | +| `worldEditWandCUI` | `true` | Live | **Bukkit only.** Lets a WorldEdit selection act as an Iris wand and draws the particle outline for it | +| `globalPregenCache` | `false` | Live, one event late | **Bukkit only.** Maintains a persistent per-world bitmap of already-generated chunks so pregen can skip finished work across restarts. The enable/disable flip is observed on the following world-init or chunk-load event, not the current one | -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `useServerLaunchedGuis` | boolean | `true` | Allow server-side GUI hosts (noise map, vision, pregen UI) | -| `maximumPregenGuiFPS` | boolean | `false` | Cap pregen GUI at max FPS when true | -| `colorMode` | boolean | `true` | Colored GUI mode | +With both `markerEntitySpawningSystem` and `ambientEntitySpawningSystem` false, the world manager skips all related entity work. -## `autoConfiguration` +## `gui` — desktop windows launched by the server -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `configureSpigotTimeoutTime` | boolean | `true` | Raise Spigot timeout on Bukkit family when supported | -| `configurePaperWatchdogDelay` | boolean | `true` | Adjust Paper watchdog delay when supported | -| `autoRestartOnCustomBiomeInstall` | boolean | `true` | Auto-restart path after custom biome datapack install when required | +Iris can open AWT windows on the machine running the server: the noise explorer, the vision map, and the pregen viewer. That is useful on a local dev box and wrong on a headless host, which is the only reason to touch this group. -Bukkit-oriented; no-op or unused on mod loaders. +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `useServerLaunchedGuis` | `true` | Live | Allows server-side GUI hosts to open windows. Set false on any remote or headless server; the commands then report that GUIs are unavailable instead of trying | +| `maximumPregenGuiFPS` | `false` | Live | Repaints the pregen map window as fast as possible instead of roughly four times a second. Only affects the local window, never generation throughput | +| `colorMode` | `true` | Per window open | Colour rendering in the noise explorer instead of grayscale. It is captured when the window opens, so close and reopen the explorer to apply a change | -## `generator` +## `autoConfiguration` — Bukkit server-file fixups -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `defaultWorldType` | string | `"overworld"` | Default pack/dimension type key for world create when not overridden by command defaults | -| `preventLeafDecay` | boolean | `true` | Prevent leaf decay on Iris-managed leaves when true | +Iris edits a couple of server config files at boot so long chunk generation does not look like a hang to the server's own watchdogs. Leave these on unless you manage those files yourself. They are all Bukkit-only and all read once during enable. -## `concurrency` +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `configureSpigotTimeoutTime` | `true` | **Restart** | Raises `timeout-time` in `spigot.yml` so a long generation stall does not kill the server | +| `configurePaperWatchdogDelay` | `true` | **Restart** | Raises Paper's watchdog early-warning and timeout for the same reason | +| `autoRestartOnCustomBiomeInstall` | `true` | **Restart** | When a datapack install registers new custom biomes and reports that a restart is required, Iris restarts the server itself instead of waiting for an admin | -This section has **no public fields** serialized to JSON. Gson writes an empty object `{}`. Methods used at runtime: +These keys are no-ops on mod loaders. -| Method | Result | -|--------|--------| -| `getParallelism()` | `max(2, availableProcessors)` | -| `getIoParallelism()` | `max(2, availableProcessors / 2)` | -| `getWorldGenThreads()` | `max(2, availableProcessors)` | +## `generator` — defaults for world creation -## `studio` +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `defaultWorldType` | `"overworld"` | Live | **Bukkit only.** The pack key used whenever a world, studio, or command omits one — including a bare `Iris` generator string in `bukkit.yml` and `/iris create name type=default`. Mod loaders use `defaultPack` in `modded.json` instead | +| `preventLeafDecay` | `true` | Effectively **restart** | Marks generated leaves persistent so they do not decay. The flag is baked into resolved block data that is then cached, so already-resolved leaf blocks keep the old behavior after a reload. Unrelated to the per-dimension `preventLeafDecay` field in pack JSON | -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `openVSCode` | boolean | `true` | Open VS Code / workspace on studio open paths | -| `disableTimeAndWeather` | boolean | `true` | Freeze time/weather in studio worlds | -| `entitySpawning` | boolean | `true` | Allow entity spawning in studio | -| `autoStartDefaultStudio` | boolean | `false` | Auto-open default studio on enable | +## `concurrency` — nothing to configure -## `performance` +This object has no serialized fields. Gson writes `{}`, and anything you type inside it is silently discarded the next time Iris saves the file. The values are derived from CPU count at runtime: -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `engineSVC` | object | see below | Nested engine service thread pool | -| `trimMantleInStudio` | boolean | `false` | Trim mantle while in studio | -| `mantleKeepAlive` | int | `30` | Mantle keep-alive window | -| `noiseCacheSize` | int | `1024` | Noise cache capacity | -| `resourceLoaderCacheSize` | int | `1024` | Resource loader cache | -| `objectLoaderCacheSize` | int | `4096` | Object loader cache | -| `mantleCleanupDelay` | int | `200` | Cleanup delay ticks; world manager uses `max(mantleCleanupDelay * 50, 0)` ms | -| `simdKernels` | boolean | `true` | SIMD-accelerated noise kernels when available | +| Method | Result | Used by | +|--------|--------|---------| +| `getParallelism()` | `max(2, availableProcessors)` | Default `MultiBurst` pools, hybrid pregen thread count, locator searches | +| `getIoParallelism()` | `max(2, availableProcessors / 2)` | The shared IO burst pool | +| `getWorldGenThreads()` | `max(2, availableProcessors)` | Async pregen concurrency cap and the Moonrise worker-pool adjustment | + +## `performance` — caches, mantle residency, and the engine service pool + +This is the memory-versus-rework group. Larger loader caches trade heap for fewer pack reloads; mantle keys decide how long generated region data stays resident before being written out. Most keys here are captured when a pool or cache is built, so plan on a restart. Use `33 - Performance Tuning.md` for the measurement procedure — changing these blind usually makes things worse. + +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `trimMantleInStudio` | `false` | Live | Lets the maintenance pass trim mantle in studio worlds. With the default `false`, studio engines skip the whole maintenance pass, which keeps edits responsive at the cost of growing memory during long authoring sessions | +| `mantleKeepAlive` | `30` | Live | Seconds a mantle plate stays resident before it is eligible for trimming. Scaled down automatically as reclaim pressure rises. Lower it when heap is tight, raise it if the same regions are reloaded repeatedly | +| `noiseCacheSize` | `1024` | Mixed | Capacity of the noise sample caches. The terrain query API picks it up live; the engine's own caches need an engine hotload or restart. Pregen temporarily raises it to at least 4096 in memory and does not lower it again or persist the change | +| `resourceLoaderCacheSize` | `1024` | **Restart / pack reload** | How many loaded pack resources stay cached per loader. Captured when a pack's `IrisData` is opened | +| `objectLoaderCacheSize` | `4096` | **Restart / pack reload** | Same, for `.iob` objects, matter objects, and images. Raise it for object-heavy packs when heap allows; lower it first when profiling shows retained pack data | +| `mantleCleanupDelay` | `200` | Live | Delay in **ticks** before a loaded chunk's mantle cleanup runs — the default is 10 seconds. Read from the raw field with no clamping, so a negative value is floored at 0 ms and a huge value really does postpone cleanup | +| `simdKernels` | `true` | **Restart** | Uses Vector API noise kernels when `jdk.incubator.vector` is on the module path, otherwise scalar fallbacks. Chosen once during class initialization, so toggling it and reloading does nothing, and it is silently inert without the JVM module flag | ### `performance.engineSVC` -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `useVirtualThreads` | boolean | `true` | Prefer virtual threads when available | -| `forceMulticoreWrite` | boolean | `false` | Force multicore write path | -| `priority` | int | `Thread.NORM_PRIORITY` (5) | Clamped to `[MIN_PRIORITY, MAX_PRIORITY]` | -| `parallelism` | int | `-1` | `>0`: min of configured and `processors * 2`; `≤0`: `ceil(sqrt(processors))` at least 1 | +The engine maintenance service is a small scheduled pool that trims and unloads mantle plates. Its three sizing keys are read once at enable, so a restart is required for any change to matter. -## `pregen` +| Key | Default | Takes effect | What it does | +|-----|---------|--------------|--------------| +| `useVirtualThreads` | `true` | **Restart** | Builds the maintenance thread factory from virtual threads instead of platform threads | +| `forceMulticoreWrite` | `false` | Live | Makes every maintenance pass unload all eligible tectonic plates instead of only unloading under heap pressure. Trades steadier memory for more write work; useful during long pregens on a small heap | +| `priority` | `5` (`Thread.NORM_PRIORITY`) | **Restart** | Thread priority, clamped to `[MIN_PRIORITY, MAX_PRIORITY]`. It is applied only when `useVirtualThreads` is false, so with the shipped defaults this key does nothing | +| `parallelism` | `-1` | **Restart** | Maintenance pool size. `>0` is capped at `processors * 2`; `<=0` uses `ceil(sqrt(processors))`, at least 1 | -| Key | Type | Default | Effective clamp / resolve | -|-----|------|---------|---------------------------| -| `runtimeSchedulerMode` | enum | `AUTO` | `AUTO`, `PAPER_LIKE`, `FOLIA`. Regionized (Folia) always resolves to `FOLIA`. On non-regionized, configured `FOLIA` is forced to `PAPER_LIKE`. `AUTO` probes server name/version/class for Folia vs Paper-like family | -| `paperLikeBackendMode` | enum | `AUTO` | `AUTO`, `TICKET`, `SERVICE`. Non-`AUTO` uses the configured value; `AUTO` resolves to `TICKET` | -| `chunkLoadTimeoutSeconds` | int | `15` | Clamped `[5, 120]` | -| `timeoutWarnIntervalMs` | int | `500` | Minimum 250 | -| `saveIntervalMs` | int | `30000` | Clamped `[5000, 900000]` | -| `maxResidentTectonicPlates` | int | `96` | Minimum 16 via getter; effective residency also scales by world height and ~60% heap budget (~48 MB reference plate at height 384) with floor 16 | -| `mantleBackpressureWaitMs` | int | `25` | Clamped `[5, 1000]` | -| `mantleBackpressureTimeoutMs` | int | `60000` | Clamped `[5000, 600000]` | -| `moddedPregenInFlight` | int | `0` | `>0`: clamped to max 512; `≤0`: `max(16, min(48, cpu * 2))` for modded pregen concurrency | +## `pregen` — scheduling, timeouts, and mantle backpressure -`runtimeSchedulerMode` and Paper-like backend modes apply to Bukkit-family pregen routing. `moddedPregenInFlight` is the modded in-flight chunk budget. +These keys bound how aggressively pregeneration pushes the server. They are read when a pregen job is constructed, so a change applies to the *next* job, not a running one. The two that matter in practice are `maxResidentTectonicPlates` (the memory ceiling) and, on mod loaders, `moddedPregenInFlight` (the concurrency ceiling). The rest exist for diagnosing a specific failure mode. -## `sentry` +| Key | Default | Applies to | What it does and how it resolves | +|-----|---------|------------|----------------------------------| +| `runtimeSchedulerMode` | `AUTO` | Bukkit | `AUTO`, `PAPER_LIKE`, `FOLIA`. A regionized (Folia) runtime resolves to `FOLIA` before the setting is consulted, and off Folia a configured `FOLIA` is downgraded to `PAPER_LIKE`. Since `AUTO` also lands on `PAPER_LIKE` for every recognized and unrecognized fork, this key changes nothing in practice — the one exception is a non-regionized server that still identifies itself as Folia by name or version, where `AUTO` picks `FOLIA` and an explicit `PAPER_LIKE` does not | +| `paperLikeBackendMode` | `AUTO` | Bukkit, non-Folia | `AUTO`, `TICKET`, `SERVICE`. `SERVICE` uses the service executor (`paper-service`); `TICKET` and `AUTO` both use the ticket executor (`paper-ticket`). Ignored entirely on Folia. Try `SERVICE` only if ticket-based chunk loading is producing timeouts | +| `chunkLoadTimeoutSeconds` | `15` | Both | Clamped to `[5, 120]`. How long pregen waits for one chunk load before it counts as timed out. **On mod loaders the effective value is floored at 120**, so any value below that is ignored there | +| `timeoutWarnIntervalMs` | `500` | Bukkit | Minimum 250. Rate-limits the "timed out async pregen chunk load" and failed-release warnings so a bad run does not flood the log. Not read on mod loaders | +| `saveIntervalMs` | `30000` | Both | Clamped to `[5000, 900000]`. How often a running pregen flushes progress. Lower it if you expect to lose the process and want a closer resume point; the cost is more IO | +| `maxResidentTectonicPlates` | `96` | Both | Minimum 16. The mantle memory ceiling, and the first knob to lower on an out-of-memory pregen. The effective cap is also scaled by world height and by roughly 60% of the heap budget against a ~48 MB reference plate at height 384, with a floor of 16 — so on a small heap you may already be running below the configured number | +| `mantleBackpressureWaitMs` | `25` | Both | Clamped to `[5, 1000]`. Sleep granularity while pregen waits for resident plates to drop below the cap | +| `mantleBackpressureTimeoutMs` | `60000` | Both | Clamped to `[5000, 600000]`. How long that wait may last before Iris logs a backpressure warning and lowers its adaptive in-flight limit. Seeing this warning repeatedly means `maxResidentTectonicPlates` is too high for your heap, not too low | +| `moddedPregenInFlight` | `0` | Modded | Concurrent chunk budget for modded pregen. `>0` is capped at 512; `<=0` derives `max(16, min(48, cpu * 2))`. Lower it when modded pregen causes chunk-load timeouts or memory growth. Inert on Bukkit | -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `includeServerId` | boolean | `true` | Include server id in reports | -| `disableAutoReporting` | boolean | `false` | Disable automatic Sentry reporting when true | -| `debug` | boolean | `false` | Sentry debug logging | +## `sentry` — error reporting -## `treeFeller` +Read once during boot on both platforms, so every change here needs a restart. -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `enabled` | boolean | `false` | Master switch for survival tree feller | -| `durabilityPreservationChance` | int | `0` | Percent chance to preserve axe durability; clamped `[0, 100]` | +| Key | Default | What it does | +|-----|---------|--------------| +| `includeServerId` | `true` | **Bukkit only.** Attaches the server id to reports so recurring reports from one server can be grouped. Not read on mod loaders | +| `disableAutoReporting` | `false` | Skips Sentry initialization entirely. Set true if you do not want automatic error reports leaving the machine | +| `debug` | `false` | Turns on Sentry's own debug logging. Useful only when reports are not arriving | -Requires permission `iris.treefeller` on Bukkit (and the platform tree-feller permission node on mod loaders). See `04 - Commands & Permissions.md` and `28 - Integrations.md`. +## `treeFeller` — survival tree felling + +Off by default because it changes survival gameplay. Both keys are read live, so `/iris reload` is enough. + +| Key | Default | What it does | +|-----|---------|--------------| +| `enabled` | `false` | Master switch. With it on, a permitted player breaking one log fells the whole Iris-managed tree. Disabling it mid-run cancels an in-flight fell on mod loaders only; the Bukkit runner does not re-read the setting once a fell has started | +| `durabilityPreservationChance` | `0` | Percent chance per block that the axe takes no durability, clamped to `[0, 100]`. An integration may override this per call | + +Requires permission `iris.treefeller` on Bukkit, or the platform tree-feller node on mod loaders. See `04 - Commands & Permissions.md` and `28 - Integrations.md`. + +## `studio` — authoring world behavior + +Only two of the four keys in this section do anything today. + +| Key | Default | What it does | +|-----|---------|--------------| +| `openVSCode` | `true` | Whether `/iris studio vscode` launches an editor after writing the workspace file. Set false on a headless box | +| `entitySpawning` | `true` | Whether mobs spawn inside studio worlds. Has no effect on normal worlds | +| `disableTimeAndWeather` | `true` | Nothing. Present in the settings model but not read by any code path today | +| `autoStartDefaultStudio` | `false` | Nothing. Present in the settings model but not read by any code path today | + +Studio workflow details: see `10 - Studio & VSCode Schemas.md`. ## Bukkit-only: `compat.json` -On Bukkit, Iris loads `plugins/Iris/compat.json` at startup and writes the built-in table to `compat.default.json`. Built-in compatibility mappings remain active; entries from `compat.json` are appended so operators can add fallback blocks and items for content that is unavailable on the running server. +On Bukkit, Iris loads `plugins/Iris/compat.json` at startup and writes the complete built-in table to `compat.default.json` next to it for reference. This is how you keep a pack working on a server that lacks some block or item it references: Iris substitutes the replacement instead of failing. + +Built-in mappings always stay active; entries read from `compat.json` are appended to them. Both files are read once at boot — `/iris reload` does not re-read them, and mod loaders do not use them at all. ```json { @@ -232,32 +270,34 @@ On Bukkit, Iris loads `plugins/Iris/compat.json` at startup and writes the built | Field | Applies to | Behavior | |-------|------------|----------| -| `when` | block and item filters | Unsupported source key to match | -| `supplement` | block and item filters | Replacement key; block replacement can continue through further mappings | -| `exact` | block filters only | When true, match complete block data instead of material only | +| `when` | block and item filters | The unsupported source key to match | +| `supplement` | block and item filters | The replacement key. If the replacement is also unsupported, Iris re-runs the lookup on it, up to 16 hops, and falls back to `STONE` with an error | +| `exact` | block filters only | When true, match the full key including namespace and block-state properties (`minecraft:some_log[axis=x]`). When false, match the bare material name. Item filters have no `exact` field | -Invalid JSON logs the failure and leaves the built-in mappings active. These files are runtime compatibility configuration, not pack resources, and are not used by the modded adapters. +A block substitution logs `Compat: Using '' in place of '' since this server doesnt support ''` as a warning; item substitutions log the same at debug level. Invalid JSON logs the failure and leaves the built-in mappings active. + +One quirk to know: when `compat.json` is absent, Iris seeds it with a copy of the entire built-in table. On the next boot those entries are appended to the built-ins again, so the runtime list holds every default twice. It is harmless because the first match wins, but if you are editing the file, delete the entries you did not add. ## Modded-only: `modded.json` -Path: `/irisworldgen/modded.json`. Written with defaults on first load if missing. Not used by the Bukkit plugin. +Path: `/irisworldgen/modded.json`, written with defaults on first load if missing. Not used by the Bukkit plugin. Unlike `settings.json` this file is parsed by hand rather than Gson, is cached once, and has no hotload — a restart is required except for the keys that Iris rewrites itself. Malformed JSON logs `Iris modded config at … is invalid; using defaults` and runs on defaults **without** rewriting your file. -| Key | Type | Default | Notes | -|-----|------|---------|-------| -| `defaultPack` | string | `"overworld"` | Default create pack; a distinct non-managed value is also prefetched when enabled | -| `autoDownloadDefaultPack` | boolean | `true` | Download missing managed Overworld/Underworld beta packs and any distinct configured default | -| `primaryWorld` | string | `""` | Primary Iris dimension id for player routing | -| `routePlayersToPrimaryWorld` | boolean | `true` | Route players to primary when set | -| `mainWorldPack` | string | `""` | Pack (or `pack:dimensionKey`) for main-world preset | -| `mainWorldSeed` | long | `0` | Seed for main-world preset | -| `mainWorldAutoRestart` | boolean | `false` | Auto-restart after main-world inject when true | +| Key | Default | What it does | +|-----|---------|--------------| +| `defaultPack` | `"overworld"` | Pack used by `/iris create` when none is given. A distinct non-managed value is also prefetched when auto-download is on | +| `autoDownloadDefaultPack` | `true` | Downloads the managed Overworld and Underworld beta packs when missing, plus any distinct configured default. Set false on an air-gapped server and install packs by hand | +| `primaryWorld` | `""` | Iris dimension id used for player routing | +| `routePlayersToPrimaryWorld` | `true` | Sends players to the primary world when one is set | +| `mainWorldPack` | `""` | Pack (or `pack:dimensionKey`) for the main-world preset | +| `mainWorldSeed` | `0` | Seed for the main-world preset | +| `mainWorldAutoRestart` | `false` | Restarts the server automatically after a main-world inject instead of telling you to | -Updated by `/iris world mainworld`, `/iris world replace-overworld`, primary-world clear paths, and related world commands. See `06 - Worlds & Lifecycle.md` and `30 - Platform Differences.md`. +`/iris world mainworld`, `/iris world replace-overworld`, and the primary-world clear paths write this file directly. See `06 - Worlds & Lifecycle.md` and `30 - Platform Differences.md`. ## What is not in these files - Pack JSON (dimensions, biomes, objects) lives under `packs//` — see `05 - Concepts & Pack Layout.md`. -- Per-world studio/workspace files are generated under pack roots — see `10 - Studio & VSCode Schemas.md`. +- Per-world studio and workspace files are generated under pack roots — see `10 - Studio & VSCode Schemas.md`. - Locale files and overrides — see `08 - Localization.md`. ## Related diff --git a/docs/04 - Commands & Permissions.md b/docs/04 - Commands & Permissions.md index 98fec2f6a..da25c3410 100644 --- a/docs/04 - Commands & Permissions.md +++ b/docs/04 - Commands & Permissions.md @@ -1,95 +1,140 @@ # 04 - Commands & Permissions -Iris exposes one root command, `/iris` (aliases `/ir`, `/irs`), with Bukkit using VolmLib Director for named parameters and optional `key=value` arguments. Fabric, Forge, and NeoForge register a Brigadier tree with the same root aliases. This is the complete command reference; platform gaps are marked **Bukkit-only** or **modded-only**. See `30 - Platform Differences.md` for a matrix and `03 - Configuration.md` for `/iris reload` targets. +Iris exposes one root command, `/iris` (aliases `/ir`, `/irs`), on every platform. Bukkit uses VolmLib Director, where optional arguments are always `key=value`; Fabric, Forge, and NeoForge register a Brigadier tree with positional arguments and literal flags. This page is the complete command reference; platform gaps are marked **Bukkit-only** or **modded-only**. See `30 - Platform Differences.md` for the platform matrix and `03 - Configuration.md` for what `/iris reload` re-reads. -## Common command recipes +## Everyday commands -Use these as entry points; follow the linked guide before running destructive or long-running forms. +Four workflows cover most operator use. The Bukkit and modded forms are separate command trees, not translations of each other, so do not port `key=value` tokens to a mod loader. -| Goal | Bukkit-family | Fabric / Forge / NeoForge | Success check | Detailed guide | -|---|---|---|---|---| -| Create and enter a disposable world | `/iris create tutorial type=overworld seed=1337`, then `/iris tp tutorial` | `/iris create tutorial overworld 1337`, then `/iris tp irisworldgen:tutorial` | World/dimension appears in `/iris worlds` or `/iris world status`; ordinary chunks generate | `02 - Getting Started.md` | -| Replace the vanilla Nether slot | `/iris create world_nether type=underworld seed=1337 overwrite=true`, then restart | Not available | `minecraft:the_nether` loads through Iris and the retained old Nether is removed only after verification | `06 - Worlds & Lifecycle.md` | -| Validate a pack before world creation | `/iris pack validate pack=overworld` | `/iris pack validate overworld` | No blocking validation errors | `25 - Pack Management.md` | -| Open the live authoring pack | `/iris studio open overworld seed=1337` | `/iris studio open overworld 1337` | Transient Studio world opens and a valid save hotloads | `10 - Studio & VSCode Schemas.md` | -| Create an in-game jigsaw project | `/iris jigsaw create overworld village/demo` | Not available; author on Bukkit and copy the saved pack | Owned planar, Iris-native graph is created atomically with six 15×15×15 workcells, one variant per archetype, and seed `1337`; edits then autosave | `21 - Jigsaw Structures.md` | -| Inspect an Iris jigsaw graph | `/iris structure info overworld ` | `/iris structure info ` while in its Iris dimension | Resolved graph reports pieces and bounds | `21 - Jigsaw Structures.md` | -| Pregenerate a small test area | `/iris pregen start 352 world= center=0,0 gui=false` | `/iris pregen start 352 at 0 0` | `/iris pregen status` advances with no accumulating failures | `07 - Pregeneration.md` | -| Remove a disposable Iris world | Evacuate players, unload, then `/iris remove ` | `/iris world delete ` | Target is absent from world status and its managed data is removed | `06 - Worlds & Lifecycle.md` | +### Create a world and enter it -Do not translate Bukkit `key=value` examples token-for-token to a mod loader. Use the Brigadier forms in the matching command sections. +``` +/iris create tutorial type=overworld seed=1337 # Bukkit +/iris create tutorial overworld 1337 # modded +``` -If a command fails before doing work, check in this order: platform syntax, permission level, sender requirements (player versus console), exact pack/world key, then lifecycle busy state. A parse error is not evidence that the underlying feature failed. +`type` (aliases `dimension`, `pack`) takes a pack key or `pack:dimensionKey`. Left at its default `default`, it resolves to `generator.defaultWorldType`. Bukkit refuses the names `iris` and `benchmark`, and refuses any name whose dimension folder already exists. + +When it works, Bukkit prints `Successfully created your world!` and the world is immediately teleportable with `/iris tp tutorial`. On Folia the world is staged instead and the message tells you to restart before it exists. On mod loaders the dimension appears in `/iris world list`, and you enter it with `/iris tp irisworldgen:tutorial`. + +If the pack is missing you get `Could not find or download dimension …` plus a `/iris download ` hint — the world is not created. + +### Pregenerate an area + +Radius is in **blocks** and measured from the center, so `352` covers a 704x704 block square. + +``` +/iris pregen start 352 world=tutorial center=0,0 gui=false # Bukkit +/iris pregen start 352 irisworldgen:tutorial at 0 0 # modded +``` + +On Bukkit, `world` resolves from the sender's current world when omitted and is hidden from the in-game usage line, but `world=` still works — use it from console. `center=me` uses your own position. On modded, `at` must come after the dimension, never before it, and the flags `gui`, `sync`, `nocache` are literals in any order. The resumable checkpoint cache is on unless you pass `nocache`. + +Confirm with `/iris pregen status`. A running job prints the target world, generated and total chunks, percent, chunks/s, ETA, elapsed time, the generation method, and a failure count. No output line for failures means nothing has failed. `/iris pregen pause` toggles, and `/iris pregen stop` finishes in-flight work before cancelling. Only one job runs server-wide. Detail: `07 - Pregeneration.md`. + +### Open a studio for pack authoring + +``` +/iris studio open overworld seed=1337 # Bukkit +/iris studio open overworld 1337 # modded +``` + +A transient studio world opens and you are teleported into it; saving any pack file hotloads the change into that world. `/iris studio close` (alias `x`) discards the world. `/iris studio vscode` regenerates the `.code-workspace` and JSON schemas, and opens it in the desktop editor unless `studio.openVSCode` is false. Detail: `10 - Studio & VSCode Schemas.md`. + +### Check a pack before you rely on it + +``` +/iris pack validate pack=overworld # Bukkit — pack is required, see the note below +/iris pack validate # modded — empty means every pack +``` + +A clean pack reports no blocking errors; the all-packs form finishes with a broken-pack count out of the total scanned. `/iris pack status` reprints the last recorded result without revalidating. Warnings do not block world creation, blocking errors do. Detail: `25 - Pack Management.md`. + +**Bukkit quirk:** `/iris pack validate` and `/iris pack status` describe `pack` as optional ("leave empty for all"), but the parameter is declared with a blank default, which Director treats as *no* default. Both commands therefore reject a bare invocation with a missing-argument error, and the all-packs branch in the code is unreachable from Bukkit chat. Pass a pack name explicitly. + +### Other common goals + +| Goal | Bukkit-family | Fabric / Forge / NeoForge | Detailed guide | +|---|---|---|---| +| Replace the vanilla Nether slot | `/iris create world_nether type=underworld seed=1337 overwrite=true`, then restart | Not available | `06 - Worlds & Lifecycle.md` | +| Create an in-game jigsaw project | `/iris jigsaw create overworld village/demo` | Not available; author on Bukkit and copy the saved pack | `21 - Jigsaw Structures.md` | +| Inspect an Iris jigsaw graph | `/iris structure info overworld ` | `/iris structure info ` while in its Iris dimension | `21 - Jigsaw Structures.md` | +| Remove a disposable Iris world | Evacuate players, `/iris unloadWorld `, then `/iris remove ` | `/iris world delete ` | `06 - Worlds & Lifecycle.md` | + +If a command fails before doing work, check in this order: platform syntax, permission, sender type (player versus console), exact pack/world key, then lifecycle busy state. A parse error is not evidence that the underlying feature failed. ## Syntax ### Bukkit (Director) -- Root: `/iris` / `/ir` / `/irs`. -- Subcommands and nested groups use method names (or `@Director(name=...)`) and aliases. -- Required parameters appear as positionals; optional parameters with defaults accept `name=value` (or short aliases from `@Param`). -- Help uses Director mini-menu: required shown as ``, optional with default as `[name=default]`. -- Example: `/iris create myworld type=overworld seed=42 main=false overwrite=false` -- Example: `/iris pregen start 5000 world=world center=me gui=true serial=false` -- Contextual params (world, dimension, location) often resolve from the sender’s current world or look target when omitted. +- Root: `/iris` / `/ir` / `/irs`. Subcommand and group names come from the method or class name unless `@Director(name=…)` overrides it, so several commands read differently than you would guess — see the reference tables. +- **Required parameters are positional; optional parameters are never positional.** `/iris pregen start 500 true` is a parse error; write `/iris pregen start 500 gui=true`. +- A parameter declared with a blank default counts as required even when the description says otherwise. +- Names and aliases match case-insensitively. +- Help uses the Director mini-menu: required renders as ``, optional as `[name=…]`. +- **Contextual** parameters (world, dimension, pack, location, generator, template on many nodes) resolve from the sender's current world or look target. They are hidden from the usage line and from tab completion, but they still accept `name=value`, which is how you drive them from console. +- Tab completion is not permission-gated; only execution is. ### Modded (Brigadier) -- Same root and most names; arguments are ordered literals/arguments, not free-form `key=value`. -- `/iris` and `/iris help [section]` print the help browser (player paginated UI; console text list). -- Flags are literals where used (e.g. pregen `gui`, `sync`, `nocache`; download `force`). +- Same root and aliases; `ir` and `irs` are redirect nodes onto `iris`. +- Arguments are ordered literals and typed arguments, never free-form `key=value`. +- `/iris` and `/iris help [section]` open the help browser: a paginated clickable UI for players (17 entries per page, trailing page number accepted in the section string) and a flat text list for console. Bare group nodes route into the same help. +- Flags are literals where used (pregen `gui`, `sync`, `nocache`; download `force`). ## Permissions ### Bukkit -| Permission | Declared in `plugin.yml` / `paper-plugin.yml` | Default | Gate | -|------------|-----------------------------------------------|---------|------| -| `iris.all` | Declared in `plugin.yml` / `paper-plugin.yml` (`default: op`) | Operators receive it by default; otherwise grant explicitly | `CommandSVC` rejects every `/iris` execution without `iris.all` | -| `iris.treefeller` | Yes | `op` | Survival tree feller only (`TreeFellerSVC`); requires `treeFeller.enabled` in settings | +| Permission | Declared in | Default | Gate | +|------------|-------------|---------|------| +| `iris.all` | `plugin.yml` and `paper-plugin.yml` | `op` | `CommandSVC.executeRoot` rejects every `/iris` execution without it | +| `iris.treefeller` | `plugin.yml` and `paper-plugin.yml` | `op` | Survival tree feller only (`TreeFellerSVC`); also requires `treeFeller.enabled` in settings | -`iris.all` is code-gated as `ROOT_PERMISSION` in `CommandSVC` and declared on the plugin with `default: op`. Without it, the sender gets a permission-denied message and no subcommand runs. - -Custom-biome restart warnings also notify online players who are op **or** hold `iris.all`. +`iris.all` is code-gated as `ROOT_PERMISSION` in `CommandSVC`. There are no per-subcommand permission nodes: a sender either has the whole tree or none of it. Custom-biome restart warnings also notify online players who are op **or** hold `iris.all`. ### Modded | Gate | Brigadier level | Applies to | |------|-----------------|------------| -| Gamemaster | `Commands.LEVEL_GAMEMASTERS` | Mutating commands: create/world, studio, object tools, pregen, download, debug, reload, evacuate, seed, structure place, edit, developer, etc. | -| Read-only | `Commands.LEVEL_ALL` | `version`, `info`/`worlds` (seed field omitted unless gamemaster), `height`, `metrics`, `what` (relaxed at root), `help` | +| Gamemaster | `Commands.LEVEL_GAMEMASTERS` (2) | Everything that mutates: create/world, studio, object tools, pregen, download, debug, reload, evacuate, teleport, seed, edit, find/goto, structure, datapack, pack, developer, regen, goldenhash, accesslist | +| Read-only | `Commands.LEVEL_ALL` (0) | `help`, `version`, `info` (seed field omitted unless gamemaster), `worlds`, `height`, `metrics`, and the entire `what` subtree | -Tree feller on mod loaders uses platform permission APIs (`irisworldgen:treefeller` on Fabric; PermissionAPI nodes on Forge/NeoForge), not Bukkit permission strings. +The root `iris` literal itself carries no requirement, so any player can run it and reach the help browser; the help marks itself as restricted when the source fails the gamemaster check. + +Tree feller on mod loaders uses the platform permission API with node `irisworldgen:treefeller` (Fabric `Identifier`; Forge and NeoForge `PermissionNode`), defaulting to gamemaster level on all three. It is not a Bukkit permission string. --- ## Root: `/iris` +Bukkit names below are the names Director actually registers. Where that differs from what you would expect, the method name is what you type. + | Command | Aliases | Platforms | Params (Bukkit-style) | Description | |---------|---------|-----------|------------------------|-------------| -| (empty) / help | | Both | `[section]` (modded) | Open help; modded supports section path | +| (empty) / help | | Both | `[section]` (modded) | Open help; modded supports a section path and page number | | `version` | | Both | — | Print Iris/platform/Minecraft version and engine count | -| `info` | | **Modded** (see `worlds`) | `[dimension]` | List Iris dimensions and pack details; seed only for gamemasters | -| `create` | `c` | Both | **Bukkit:** ` [type=default] [seed=1337] [main=false] [overwrite=false]` (`type` aliases `dimension`,`pack`; `overwrite` alias `force`). **Modded:** ` [pack=overworld] [seed=1337]` | Create an Iris world/dimension; Bukkit `overwrite=true` stages an exact slot replacement for restart | -| `teleport` | `tp` | Both | **Bukkit:** ` [player=]`. **Modded:** ` [player]` | Teleport self or named player into Iris world/dimension | -| `evacuate` | | Both | **Bukkit:** `` (player origin). **Modded:** `[dimension]` | Move players out of Iris world to fallback/primary | -| `height` | | Both | — | Print world height (player on Bukkit) | -| `worlds` | `accesslist` | Both | — | **Bukkit:** access list of worlds. **Modded:** same as `info` (read-only); `accesslist` requires gamemaster | -| `remove` | `rm` | **Bukkit** | ` [delete=true]` | Remove managed Iris world; disk deletion defaults to true | -| `load` | `import` | **Bukkit** | `` | Load managed Iris world | -| `unload` | | **Bukkit** | `` | Unload Iris world | +| `info` | | **Modded** | `[dimension]` (substring filter) | List Iris dimensions and pack details; seed only for gamemasters | +| `create` | `c` | Both | **Bukkit:** ` [type=default] [seed=1337] [main=false] [overwrite=false]` (`name` alias `world-name`; `type` aliases `dimension`,`pack`; `main` alias `main-world`; `overwrite` alias `force`). **Modded:** ` [pack=overworld] [seed=1337]` | Create an Iris world/dimension; Bukkit `overwrite=true` stages an exact slot replacement for restart | +| `teleport` | `tp` | Both | **Bukkit:** ` [player]` (defaults to the sender). **Modded:** ` [player]` | Teleport self or a named player into an Iris world/dimension | +| `evacuate` | | Both | **Bukkit:** ``, player origin. **Modded:** `[dimension]` | Move players out of an Iris world to fallback/primary | +| `height` | | Both | — | Print world height; player origin on Bukkit | +| `worlds` | `accesslist` (Bukkit) | Both | — | **Bukkit:** access list of worlds. **Modded:** two separate nodes — `worlds` is read-only and takes no argument, `accesslist` needs gamemaster; both print the `info` listing | +| `remove` | `rm` | **Bukkit** | ` [delete=true]` | Remove a managed Iris world; disk deletion defaults to true. `world` is a name, so worlds that exist only on disk are accepted | +| `loadWorld` | `import` | **Bukkit** | ``, player origin | Load a managed Iris world | +| `unloadWorld` | | **Bukkit** | ``, player origin | Unload an Iris world | | `debug` | | Both | — | Toggle `general.debug` and save settings | -| `download` | `dl` | Both | ` [branch=stable] [overwrite=false]` (`overwrite` alias `force`) | Download a pack; `overworld` and `underworld` resolve to managed beta release ZIPs | -| `metrics` | `measure` | Both | — | Generation metrics (player / current Iris level) | +| `download` | `dl` | Both | ` [branch=stable] [overwrite=false]` (`pack` alias `project`; `overwrite` alias `force`) | Download a pack; `overworld` and `underworld` resolve to managed beta release ZIPs | +| `metrics` | `measure` | Both | — | Generation metrics; player origin on Bukkit | | `reload` | | Both | — | Reload `settings.json` and locale; modded also schedules forced datapack regeneration | -| `seed` | | **Modded** | — | Print world/engine seeds (gamemaster) | -| `regen` | `rg` | **Modded** root; Bukkit under `developer` | `[radius]` | Delete/regenerate nearby chunks | -| `goldenhash` | `gold` | **Modded** root; Bukkit under `developer` | `[radius] [threads] [capture\|verify]` | Deterministic buffer hashes | -| `wand` | | **Modded** root (+ object) | — | Give object wand | -| `dust` | `d` | **Modded** root (+ object) | — | Give reveal dust | +| `seed` | | **Modded** | — | Print world and engine seeds | +| `regen` | `rg` | **Modded** root; Bukkit under `Developer` | `[radius]` — modded default `0`, range `0..64` | Delete and regenerate nearby chunks | +| `goldenhash` | `gold` | **Modded** root; Bukkit under `Developer` | `[radius=8] [threads=8] [capture\|verify]`, radius `0..256`, threads `1..64` | Deterministic buffer hashes | +| `wand` | | **Modded** root (+ `object`) | — | Give object wand | +| `dust` | `d` | **Modded** root (+ `object`) | — | Give reveal dust | | `find` | `goto` | Both | see Find | Locate biome/region/object/structure/POI | | `what` | | Both | see What | Inspect context | -| `edit` | | Both | see Edit | Open JSON in desktop editor | +| `edit` | | Both | see Edit | Open pack JSON in the desktop editor | | `pregen` | `pregenerate` | Both | see Pregen | Pregeneration control | | `object` | `o` | Both | see Object | Object tools | | `studio` | `std`, `s` | Both | see Studio | Studio / pack authoring | @@ -97,12 +142,12 @@ Tree feller on mod loaders uses platform permission APIs (`irisworldgen:treefell | `pack` | `pk` | Both | see Pack | Validate/cleanup/restore/status | | `structure` | `struct`, `str` | Both | see Structure | Structure index/import/place | | `datapack` | `datapacks`, `dp` | Both | see Datapack | Datapack helpers | -| `developer` | `dev` | Both | see Developer | Diagnostics | +| `Developer` | `dev` | Both | see Developer | Diagnostics; the group name is registered with a capital `D`, but matching is case-insensitive | | `world` | `w` | **Modded** | see World | Runtime dimension enable/disable | --- -On Paper-family servers, `overwrite=true` is deliberately restart-only; Spigot rejects it because it has no pre-registry plugin bootstrap. The exact target dimension folder must already exist; use ordinary `/iris create` for a new world. The name may resolve to a safe `iris:*` world or exactly the configured main, `_nether`, or `_the_end` alias; arbitrary `minecraft:*` and foreign namespaces are rejected. Iris stages and validates a fresh pack snapshot, compare-and-swaps only that world's `bukkit.yml` generator and seed, and retains the existing dimension folder as a rollback backup until the restarted world proves its Iris identity, pack, dimension, environment, and seed. Multiple distinct slots may be staged before one restart. `main=true` is valid with overwrite only when the name is the configured main-world name. Exact vanilla slots preserve the authoritative seed shared by the existing level, regardless of the supplied `seed`; this keeps Overworld/Nether/End coordinate generation aligned. Use ordinary new-main promotion when a new level seed is required. +On Paper-family servers, `overwrite=true` is deliberately restart-only; Spigot rejects it because it has no pre-registry plugin bootstrap. The exact target dimension folder must already exist; use ordinary `/iris create` for a new world. The name may resolve to a safe `iris:*` world or exactly the configured main, `_nether`, or `_the_end` alias; arbitrary `minecraft:*` and foreign namespaces are rejected. Iris stages and validates a fresh pack snapshot, compare-and-swaps only that world's `bukkit.yml` generator and seed, and retains the existing dimension folder as a rollback backup until the restarted world proves its Iris identity, pack, dimension, environment, and seed. Multiple distinct slots may be staged before one restart. `main=true` is valid with overwrite only when the name is the configured main-world name. Exact vanilla slots preserve the authoritative seed shared by the existing level, regardless of the supplied `seed`; this keeps Overworld/Nether/End coordinate generation aligned, and Iris tells you which seed it used instead. Use ordinary new-main promotion when a new level seed is required. --- @@ -112,37 +157,37 @@ On Paper-family servers, `overwrite=true` is deliberately restart-only; Spigot r | Command | Params | Description | |---------|--------|-------------| -| `biome` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find Iris biome; teleport default true on Bukkit | -| `region` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find Iris region | -| `object` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find object placement (Bukkit may teleport to object studio first) | -| `structure` | **Bukkit:** `` (sync). **Modded:** `` | Find vanilla/datapack/Iris structure | -| `poi` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find supported point of interest | -| `unregistered` | — | Print structures excluded from goto completion and rejection reasons to console | +| `biome` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find an Iris biome; teleport defaults to true on Bukkit | +| `region` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find an Iris region | +| `object` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find an object placement (Bukkit may teleport to the object studio first) | +| `structure` | **Bukkit:** ``, runs sync. **Modded:** `` | Find a vanilla/datapack/Iris structure | +| `poi` | **Bukkit:** ` [teleport=true]`. **Modded:** `` | Find a supported point of interest | +| `unregistered` | — | Print structures excluded from goto completion, and the rejection reasons, to console | --- ## What: `/iris what` -**Bukkit origin:** player only. No bare `here` command on Bukkit. +**Bukkit origin:** player only, and there is no bare or `here` form. On modded the whole subtree is `LEVEL_ALL`, so any player can use it. | Command | Platforms | Params | Description | |---------|-----------|--------|-------------| -| (empty) / `here` | **Modded** | — | Full inspect at player position | +| (empty) / `here` | **Modded** | — | Full inspect at the player position | | `biome` | Both | — | Current Iris biome | | `region` | Both | — | Current Iris region | | `block` | Both | — | Target block | | `hand` | Both | — | Held item | -| `markers` | Both | `` | Reveal nearby markers (e.g. `cave_floor`, `cave_ceiling`, `object`) | +| `markers` | Both | `` | Reveal nearby markers (for example `cave_floor`, `cave_ceiling`, `object`) | --- ## Edit: `/iris edit` -**Bukkit origin:** player. Opens pack JSON in the desktop editor. +**Bukkit origin:** player. Opens pack JSON in the desktop editor. The same tree is also mounted at `/iris studio edit …` on Bukkit. | Command | Aliases | Params | Description | |---------|---------|--------|-------------| -| `biome` | `b` | **Bukkit:** ``. **Modded:** `[key]` | Open biome JSON (modded: omit key for current) | +| `biome` | `b` | **Bukkit:** ``. **Modded:** `[key]` | Open biome JSON (modded: omit the key for the current biome) | | `region` | `r` | **Bukkit:** ``. **Modded:** `[key]` | Open region JSON | | `dimension` | `d` | **Bukkit:** ``. **Modded:** — | Open dimension JSON (modded: current pack) | @@ -152,12 +197,12 @@ On Paper-family servers, `overwrite=true` is deliberately restart-only; Spigot r | Command | Aliases | Params | Description | |---------|---------|--------|-------------| -| `start` | | **Bukkit:** ` [world=] [center=0,0] [gui=true] [serial=false]` (`radius` alias `size`, `center` alias `middle`, use `me` for player). **Modded:** ` [dimension] [at ] [gui] [sync] [nocache]` | Start pregen; radius in **blocks**; resumable checkpoint cache on by default on modded unless `nocache` | -| `stop` | `x` | — | Stop active pregen | +| `start` | | **Bukkit:** ` [world] [center=0,0] [gui=true] [serial=false]` (`radius` alias `size`, `center` alias `middle`, `me` for the player position; `world` is contextual with no default). **Modded:** ` [dimension] [at ] [gui] [sync] [nocache]`, radius `1..100000` | Start pregen; radius in **blocks**; the resumable checkpoint cache is on by default on modded unless `nocache` | +| `stop` | `x` | — | Stop the active pregen after in-flight work closes | | `pause` | `resume` | — | Toggle pause/resume | -| `status` | — | — | Progress, CPS, ETA, method, failures | +| `status` | | — | Progress, chunks/s, ETA, elapsed, method, failures | -**Bukkit:** `serial=true` requires a Paper-compatible server (strict serial chunk generation). **Modded:** `sync` is the serial-like flag; `gui` opens boss-bar/GUI path when available. +**Bukkit:** `serial=true` requires a Paper-compatible server (strict serial chunk generation) and is rejected elsewhere. **Modded:** `sync` is the serial-like flag; `gui` opens the boss-bar/GUI path when available. See `07 - Pregeneration.md`. @@ -165,28 +210,28 @@ See `07 - Pregeneration.md`. ## Object: `/iris object` (`o`) -**Bukkit:** group origin player. Root `wand`/`dust` also exist on modded. +**Bukkit:** group origin is player. Root `wand`/`dust` also exist on modded. | Command | Aliases | Platforms | Params | Description | |---------|---------|-----------|--------|-------------| -| `wand` | | Both | — | Give Iris object wand | +| `wand` | | Both | — | Give the Iris object wand | | `dust` | `d` | Both | — | Give reveal dust | -| `save` | | Both | **Bukkit:** `[dimension=] [overwrite=false] [legacy=true]`. **Modded:** `[overwrite] ` | Save wand selection as `.iob` | -| `paste` | | Both | **Bukkit:** ` [edit=false] [rotate=0] [scale=1]`. **Modded:** `[at x y z] [rotate degrees] ` | Paste object | -| `expand` | | **Modded** | `[amount]` (default `1`) | Expand selection along look | -| `contract` | `-` | Both | `[amount=1]` | Contract selection along look | -| `shift` | | Both | `[amount=1]` | Shift selection along look | -| `position1` | `p1` | Both | **Bukkit:** `[here=true]` (look vs feet). **Modded:** `[look]` | Set selection point 1 | -| `position2` | `p2` | Both | same | Set selection point 2 | +| `save` | | Both | **Bukkit:** ` [overwrite=false] [legacy=true]` (`overwrite` alias `force`; a contextual `dimension` is resolved from your world, or passed as `dimension=`). **Modded:** `[overwrite] ` | Save the wand selection as `.iob` | +| `paste` | | Both | **Bukkit:** ` [edit=false] [rotate=0] [scale=1]`. **Modded:** `[at x y z] [rotate degrees] ` | Paste an object | +| `expand` | | **Modded** | `[amount=1]`, range `1..256` | Expand the selection along your look direction | +| `contract` | `-` | Both | `[amount=1]` | Contract the selection along your look direction | +| `shift` | | Both | `[amount=1]` | Shift the selection along your look direction | +| `position1` | `p1` | Both | **Bukkit:** `[here=true]` uses the block under your feet; `here=false` uses your look target. **Modded:** feet by default, `look` switches to the look target | Set one selection corner; requires the Iris wand in hand | +| `position2` | `p2` | Both | same | Set the other selection corner | | `x+y` | `xpy` (modded) | Both | — | Autoselect up and out | | `x&y` | `xay` (modded) | Both | — | Autoselect up, down, and out | -| `analyze` | | Both | `` | Composition stats | -| `shrink` | | Both | `` | Shrink object to minimum bounds | -| `plausibilize` | | Both | **Bukkit:** ` [dryrun=false] [reach=12]`. **Modded:** greedy args `key [dryrun=true] [reach=N]` | Grow branches so leaves survive vanilla decay | +| `analyze` | | Both | `` | Composition stats | +| `shrink` | | Both | `` | Shrink the object to its minimum bounds | +| `plausibilize` | | Both | **Bukkit:** ` [dryrun=false] [reach=12]`; `target` accepts a key or a `prefix/`. **Modded:** greedy `` parsed as `key [dryrun] [reach]`, same defaults | Grow branches so leaves survive vanilla decay | | `undo` | `u` | Both | `[amount=1]` | Undo pastes | -| `we` | | **Bukkit**; modded stub | — | Wand + import WorldEdit selection | -| `studio` | | **Bukkit**; modded stub | `[dimension=null] [seed=1337]` | Object studio grid world | -| `convert` | | **Bukkit**; modded stub | — | Convert `convert/` folder `.schem` → `.iob` | +| `we` | | **Bukkit**; modded stub | — | Wand plus import of the WorldEdit selection | +| `studio` | | **Bukkit**; modded stub | `[dimension] [seed=1337]` | Object studio grid world | +| `convert` | | **Bukkit**; modded stub | — | Convert `convert/` folder `.schem` to `.iob` | --- @@ -194,23 +239,23 @@ See `07 - Pregeneration.md`. | Command | Aliases | Platforms | Params | Description | |---------|---------|-----------|--------|-------------| -| `open` | `o` | Both | **Bukkit:** ` [seed=1337]`. **Modded:** ` [seed]` | Open temporary studio dimension; the owning player may replace an active Jigsaw Studio, and Iris waits for its autosave and active operation barriers before closing it | -| `close` | `x` | Both | — | Close studio and discard world; Bukkit requires `/iris jigsaw close` for an active Jigsaw Studio | -| `tpstudio` | `stp` | Both | — | Teleport into open studio | -| `status` | | **Modded** (Bukkit uses other paths) | — | Show open studio and pack | -| `create` | `+` | Both | **Bukkit:** `[name=studio] [template=]`. **Modded:** `[name] [template=example]` | Create pack project | -| `package` | `pkg` (Bukkit method `pkg`, alias `package`) | Both | **Bukkit:** `[dimension=default] [obfuscate=false] [minify=true]`. **Modded:** `[pack]` | Zip/package pack | +| `open` | `o` | Both | **Bukkit:** ` [seed=1337]` (`dimension` alias `dim`, `seed` alias `s`). **Modded:** ` [seed]` | Open a temporary studio dimension; the owning player may replace an active Jigsaw Studio, and Iris waits for its autosave and active-operation barriers before closing it | +| `close` | `x` | Both | — | Close the studio and discard the world; Bukkit requires `/iris jigsaw close` for an active Jigsaw Studio | +| `tpstudio` | `stp` | Both | — | Teleport into the open studio | +| `status` | | **Modded** | — | Show the open studio and pack | +| `create` | `+` | Both | **Bukkit:** `[name=studio] [template]`. **Modded:** `[name] [template=example]` | Create a pack project | +| `pkg` | `package` | Both | **Bukkit:** `[dimension=default] [obfuscate=false] [minify=true]`. **Modded:** `[pack]` | Zip and package a pack. The Bukkit command name is `pkg`; `package` is the alias | | `version` | | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Pack version | -| `regions` | | Both | **Bukkit:** `[radius=500]` (player). **Modded:** `[radius]` default 500 | Nearby region distribution | -| `noise` | `nmap` | Both | **Bukkit:** `[generator=] [seed=12345]`. **Modded:** `[generator] [seed]` | Noise explorer GUI | -| `map` | `render` | Both | **Bukkit:** `[world=]`. **Modded:** — | Vision map GUI | -| `vscode` | `vsc` | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Generate/open code workspace | -| `update` | | Both | same as vscode pack | Regenerate workspace only | -| `importvanilla` | `importv`, `iv` | **Bukkit** functional; **modded message** | ` [variants=3] [structures=true]` | Import vanilla trees/objects/structures into pack | -| `scoreboard` | `board`, `sidebar`, `sb` | **Bukkit** | — | Toggle studio debug scoreboard | -| `loot` | | **Bukkit**; modded stub | `[fast=false] [add=true]` | Simulate chest loot GUI | +| `regions` | | Both | **Bukkit:** `[radius=500]`, player origin. **Modded:** `[radius]`, default 500 | Nearby region distribution | +| `noise` | `nmap` | Both | **Bukkit:** `[generator] [seed=12345]`. **Modded:** `[generator] [seed]` | Noise explorer GUI | +| `map` | `render` | Both | **Bukkit:** contextual `world`, required. **Modded:** — | Vision map GUI | +| `vscode` | `vsc` | Both | **Bukkit:** `[dimension=default]`. **Modded:** `[pack]` | Generate and open the code workspace | +| `update` | | Both | same pack argument as `vscode` | Regenerate the workspace only | +| `importvanilla` | `importv`, `iv` | **Bukkit**; modded stub | ` [variants=3] [structures=true]` | Import vanilla trees/objects/structures into a pack | +| `scoreboard` | `board`, `sidebar`, `sb` | **Bukkit** | — | Toggle the studio debug scoreboard | +| `loot` | | **Bukkit**; modded stub | `[fast=false] [add=true]` | Simulate chest loot in a GUI | | `profile` | | **Bukkit**; modded stub | `[dimension=default]` | Pack performance profile | -| `spawn` | `summon` | **Bukkit**; modded stub | ` [location=]` | Spawn Iris entity | +| `spawn` | `summon` | **Bukkit**; modded stub | ` ` (`location` is contextual) | Spawn an Iris entity | | `objects` | `find-objects` | **Bukkit**; modded stub | — | IGenData chunk report for nearby chunks | See `10 - Studio & VSCode Schemas.md`. @@ -223,28 +268,28 @@ See `10 - Studio & VSCode Schemas.md`. | Command | Params | Description | |---|---|---| -| `create` | ` [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic graph creation followed by open; named `structure=` and `name=` alias `key=`; `mode` completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`; planar X/Z `3..128`, spatial X/Z `1..128`, Y `1..192`, volume `<=2,097,152` | +| `create` | ` [mode=planar] [compatibility=iris] [width=15] [height=15] [depth=15] [seed=1337]` | Add-only atomic graph creation followed by open; `key` aliases `structure` and `name`; `mode` completes `planar`/`spatial`, compatibility completes `iris`/`vanilla`; planar X/Z `3..128`, spatial X/Z `1..128`, Y `1..192`, volume `<=2,097,152` | | `convert` | ` [target=auto] [seed=1337]` | Add-only conversion of one live registered vanilla/datapack jigsaw into an owned Iris graph, then open it; aliases `import`, `import-vanilla` | | `adopt inspect` | ` [target=auto] [strategy=auto]` | Asynchronously inspect an existing Iris closure and issue a hash-pinned `IN_PLACE`, `CLONE_REQUIRED`, or `BLOCKED` plan; strategy completes `auto`, `in-place`, `clone` | -| `adopt apply` | `` | Revalidate and atomically apply that player's unexpired plan, then open the target at seed `1337`; active/opening Jigsaw Studio is rejected | +| `adopt apply` | `` | Revalidate and atomically apply that player's unexpired plan, then open the target at seed `1337`; an active or opening Jigsaw Studio is rejected | | `open` | ` [seed=1337]` | Open an existing graph in compact workcells; aliases `edit`, `reopen`; existing Iris structure keys tab-complete; owner, autosave, and operation barriers protect replacement | | `close` | `[discard=false]` | Close Studio; refuse active autosave/load/graph work or a pending dirty capture unless deliberately discarded | | `status` | — | Show project/workcell state and the current automatic seed-`1337` evaluation, theme, piece count, and diagnostic | | `menu` | — | Open the six-row controls also opened by the generated chest or three sneaks within 1.5 seconds | | `select` | — | Select the workcell containing the player | -| `goto` | `` | Select and teleport above a stable workcell ID; alias `teleport` | +| `goto` | `` | Select and teleport above a stable workcell ID; alias `teleport` | | `particles` | `` | Toggle player-local workcell-bound, connector, and temporary assembly-preview particle trails | | `save` | `[bay=selected]` | Flush the selected dirty workcell's automatic capture now; normal block and container updates already autosave | | `connector channel` | `` | Look at a saved marker in the active owned workcell within 8 blocks and set/clear its Iris-only channel at the inverse-mapped source position | | `bounds` | ` ` | Set the selected workcell capacity without resizing any variant object; every existing variant must fit, and the compact Studio layout regenerates in place; aliases `cell`, `resize` | -| `workcell capacity` | ` ` | Explicit nested form of `bounds`; planar capacities are per canonical archetype and spatial capacity is the shared envelope for its one-row variant cells | +| `workcell capacity` | ` ` | Explicit nested form of `bounds` (group alias `cell`); planar capacities are per canonical archetype and spatial capacity is the shared envelope for its one-row variant cells | | `workcell label` | `` | Set the selected planar or spatial workcell's author label; quote spaces; solver identity remains canonical | | `workcell label-reset` | — | Reset the selected workcell to its canonical solver label; alias `reset-label` | | `pool create` | ` [fallbackPoolKey=none]` | Atomically create an empty owned pool, optionally using an existing owned direct fallback | | `piece create` | ` [weight=1]` | Create and load an owned variant; planar derives connectors from the contextual canonical workcell | | `piece add` | ` [weight=1]` | Re-add and load an existing piece/object already owned by this project | | `piece remove` | `` | Remove the active variant from a pool without deleting its owned resources | -| `piece rotatable` | `` | Persist cardinal rotation for the active variant; portable sessions reject `false` | +| `piece rotatable` | `` | Persist cardinal rotation for the active variant; portable sessions reject `false` | | `piece expand` | — | Resize the active planar or spatial owned variant exactly to workcell capacity; planar canonical sockets move to the new faces | | `variant weight` | ` ` | Set the active variant's positive weight in an owned pool | | `variant resize` | ` ` | Resize only the active owned variant within its workcell capacity; safe shrink rejects cropped content and the active cell reloads in place | @@ -252,8 +297,8 @@ See `10 - Studio & VSCode Schemas.md`. | `variant label-reset` | — | Reset the active variant to its resource-key fallback; alias `reset-label` | | `variant duplicate` | — | Copy the active variant's object, metadata, and exact pool memberships into one new variant in this workcell | | `variant duplicate-family` | `[themeKey=next]` | Atomically clone every enabled workcell's active owned variant into one coherent Iris family and load the whole family; alias `family` | -| `rules limits` | ` ` | Set depth `1..30` and radius `1..32`; `VANILLA_PORTABLE` is restricted to `<=20` and `<=8` | -| `rules fallback` | ` ` | Set or clear one owned pool's direct fallback after compiling the complete graph | +| `rules limits` | ` ` | Set depth `1..30` and radius `1..32` (group alias `rule`); `VANILLA_PORTABLE` is restricted to `<=20` and `<=8` | +| `rules fallback` | ` ` | Set or clear one owned pool's direct fallback after compiling the complete graph; pass `none` to clear. Unlike `pool create`, this argument is required | | `preview goto` | — | Teleport above the permanent seed-`1337` block preview; alias `teleport` | | `preview assemble` | `[seed=1337]` | Compute a deterministic read-only assembly at the player, report its complete piece count, and show in-range bounds as purple particle boxes for 10 seconds within the shared particle budget; places no blocks | | `export` | `[namespace=iris] [output=jigsaw-export] [format=zip] [replace=false]` | Start a background strict Minecraft 26.2 vanilla datapack export as one direct artifact under the Studio packs `exports/` folder | @@ -269,10 +314,10 @@ Bukkit has one global Studio project/world and the Jigsaw session belongs to one | Command | Aliases | Params | Description | |---------|---------|--------|-------------| -| `validate` | `v` | **Bukkit:** `[pack=]`. **Modded:** `[pack]`; empty = all | Validate pack(s) and publish results | -| `cleanup` | `c` | **Bukkit:** ` [mode=preview]`. **Modded:** ` [apply]` | Preview/quarantine unused resources | -| `restore` | `r` | same pattern | Preview/restore latest quarantine | -| `status` | `s` | **Bukkit:** `[pack=]`. **Modded:** `[pack]` | Startup-published validation status, including persisted unchanged results | +| `validate` | `v` | **Bukkit:** `` (required despite the "leave empty for all" description). **Modded:** `[pack]`; empty means all | Validate pack(s) and publish results | +| `cleanup` | `c` | **Bukkit:** ` [mode=preview]`. **Modded:** ` [apply]` | Preview or quarantine unused resources | +| `restore` | `r` | same pattern as `cleanup` | Preview or restore the latest quarantine | +| `status` | `s` | **Bukkit:** `` (required, same blank-default trap). **Modded:** `[pack]` | Startup-published validation status, including persisted unchanged results | See `25 - Pack Management.md`. @@ -280,14 +325,16 @@ See `25 - Pack Management.md`. ## Structure: `/iris structure` (`struct`, `str`) +Bukkit `dimension` parameters all carry the alias `dim`. + | Command | Aliases | Platforms | Params | Description | |---------|---------|-----------|--------|-------------| | `list` | `ls` | Both | **Bukkit:** ``. **Modded:** current engine pack | Write `structure-index.json` | | `info` | | Both | **Bukkit:** ` `. **Modded:** `` | Resolve jigsaw graph bounds | -| `place` | `p` | Both | **Bukkit:** ` ` (player). **Modded:** `` | Assemble and place at the player; Bukkit reports the exact changed-block count and rejects air-only or already-identical no-op results | -| `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded message | `` | Import all vanilla/datapack structures as editable Iris resources (overwrites) | -| `capture` | `cap` | **Bukkit**; modded message | `` | Capture code-only structures via scratch world | -| `verify` | `locateall` | Both | **Bukkit:** ` [radius=48]`. **Modded:** `[key]` | Native/Iris structure reachability report | +| `place` | `p` | Both | **Bukkit:** ` `, player origin. **Modded:** `` | Assemble and place at the player; Bukkit reports the exact changed-block count and rejects air-only or already-identical no-op results | +| `import` | `import-all`, `reimport`, `imp`, `all` | **Bukkit**; modded stub | `` | Import all vanilla/datapack structures as editable Iris resources (overwrites) | +| `capture` | `cap` | **Bukkit**; modded stub | `` | Capture code-only structures via a scratch world | +| `verify` | `locateall` | Both | **Bukkit:** ` [radius=48]`, clamped to `1..1000`. **Modded:** `[key]` | Native/Iris structure reachability report | See `18 - Structures Overview.md`, `21 - Jigsaw Structures.md`, `22 - Native Structures & Datapacks.md`. @@ -297,11 +344,11 @@ See `18 - Structures Overview.md`, `21 - Jigsaw Structures.md`, `22 - Native Str | Command | Aliases | Platforms | Params | Description | |---------|---------|-----------|--------|-------------| -| `ingest` | `pull` | **Bukkit**; modded message | `[restart=false]` | Download/install Modrinth `datapackImports` into world datapacks | -| `list` | `ls` | Both | — | **Bukkit:** configured imports + installed. **Modded:** configured/installed world datapacks | -| `remove` | `rm` | **Bukkit**; modded message | `` | Remove installed datapack by id | -| `status` | | **Modded** | — | Check Iris dimension-type overrides vs pack heights | -| `install` | | **Modded** | — | Install dimension-type override datapack for loaded Iris dimensions | +| `ingest` | `pull` | **Bukkit**; modded stub | `[restart=false]` | Download and install Modrinth `datapackImports` into world datapacks | +| `list` | `ls` | Both | — | **Bukkit:** configured imports plus installed. **Modded:** configured/installed world datapacks | +| `remove` | `rm` | **Bukkit**; modded stub | `` | Remove an installed datapack by id | +| `status` | | **Modded** | — | Check Iris dimension-type overrides against pack heights | +| `install` | | **Modded** | — | Install the dimension-type override datapack for loaded Iris dimensions | See `22 - Native Structures & Datapacks.md`. @@ -309,38 +356,38 @@ See `22 - Native Structures & Datapacks.md`. ## World (modded-only group): `/iris world` (`w`) -Bukkit uses root `create` / `load` / `unload` / `remove` / `evacuate` instead. +Bukkit uses root `create` / `loadWorld` / `unloadWorld` / `remove` / `evacuate` instead. `[seed]` accepts a number or the literal `random`; blank means `1337`. | Command | Aliases | Params | Description | |---------|---------|--------|-------------| -| `enable` | `create` | ` [seed\|random]` | Create/inject persistent Iris dimension (downloads pack if missing) | +| `enable` | `create` | ` [seed\|random]` | Create/inject a persistent Iris dimension (downloads the pack if missing) | | `replace-overworld` | | ` [seed\|random]` | Inject primary world routing | -| `mainworld` | | ` [seed\|random]` | Configure main-world preset in `modded.json` | +| `mainworld` | | ` [seed\|random]` | Configure the main-world preset in `modded.json` | | `disable` | | `` | Evacuate and unload; keep disk data | | `delete` | `remove`, `rm` | `` | Disable and wipe chunk/mantle data | | `list` | `ls` | — | List loaded Iris dimensions | -| `status` | | — | Loaded dimensions + primary world config | +| `status` | | — | Loaded dimensions plus primary world config | --- -## Developer: `/iris developer` (`dev`) +## Developer: `/iris Developer` (`dev`) | Command | Aliases | Platforms | Params | Description | |---------|---------|-----------|--------|-------------| | `EngineStatus` | | **Bukkit** | — | Loaded tectonic plate count | -| `Sentry` | `sentry` (modded) | Both | — | Send test exception to error reporter | -| `genhash` | | **Bukkit** | `[world] [radius=4] [center-x=0] [center-z=0]` | Hash generated blocks in fixed area | -| `update-world` | `^world` | **Bukkit** | `[world=] [pack=] [confirm=false] [fresh-download=false]` | Unsafe pack swap into world | -| `mantle` | | **Bukkit** | `[plate=false] [name=…]` | Dump mantle section/plate under dump folder | -| `packBenchmark` | | **Bukkit** | `[pack=overworld] [radius=2048] [gui=false]` | Pack benchmark | +| `Sentry` | `sentry` (modded) | Both | — | Send a test exception to the error reporter | +| `genhash` | | **Bukkit** | `[radius=4] [centerX=0] [centerZ=0]`, contextual `world` | Hash generated blocks in a fixed area. The center parameters are `centerX`/`centerZ` here, not the hyphenated `goldenhash` names | +| `update-world` | `^world` | **Bukkit** | `[confirm=false] [fresh-download=false]`, contextual `world` and `pack` (`pack` alias `dimension`; `confirm` alias `c`; `fresh-download` aliases `fresh`, `new`) | Unsafe pack swap into a world | +| `mantle` | | **Bukkit** | `[plate=false] [name=21474836474]` | Dump a mantle section or plate under the dump folder | +| `packBenchmark` | | **Bukkit** | `[dimension=overworld] [radius=2048] [gui=false]` (`dimension` alias `pack`) | Pack benchmark | | `upgrade` | | **Bukkit** | `[version=latest]` | Data version upgrade helper | -| `mca` | | **Bukkit** | `` | Scan MCA region files | -| `delete-chunk` | `dc` | **Bukkit** | `[radius=0]` | Delete nearby chunk blocks (regen testing) | +| `mca` | | **Bukkit** | `` (a world folder path) | Scan MCA region files | +| `delete-chunk` | `dc` | **Bukkit** | `[radius=0]`, player origin | Delete nearby chunk blocks for regen testing | | `network` | `ip` | Both | — | List network interfaces | -| `regen` | `rg` | **Bukkit** (modded root) | `[radius=5]` | Delete and regenerate nearby chunks | -| `goldenhash` | `gold` | **Bukkit** (modded root) | `[world] [radius=8] [center-x=0] [center-z=0] [reset-mantle=true] [threads=8] [deep=false]` | Buffer golden hash capture/verify | +| `regen` | `rg` | **Bukkit** (modded root) | `[radius=5]`, player origin | Delete and regenerate nearby chunks | +| `goldenhash` | `gold` | **Bukkit** (modded root) | `[radius=8] [center-x=0] [center-z=0] [reset-mantle=true] [threads=8] [deep=false]`, contextual `world` | Buffer golden hash capture/verify | -Modded developer group currently implements only `sentry` and `network`/`ip`. +The modded developer group implements only `sentry` and `network`/`ip`; its help section still advertises a region file scan that has no command node. --- @@ -348,19 +395,20 @@ Modded developer group currently implements only `sentry` and `network`/`ip`. | Feature | Bukkit | Modded | |---------|--------|--------| -| Root permission node | `iris.all` (code) | Gamemaster / all levels | -| World lifecycle | `create`, `load`, `unload`, `remove` | `world enable/disable/delete`, `create`, `mainworld` | +| Root permission node | `iris.all` (code-gated, whole tree) | `LEVEL_GAMEMASTERS` / `LEVEL_ALL` per node | +| World lifecycle | `create`, `loadWorld`, `unloadWorld`, `remove` | `world enable/disable/delete`, `create`, `mainworld` | | Seed print | — | `/iris seed` | | Object expand | — | `/iris object expand` | | Object WE / studio / convert | yes | help stubs only | | Studio loot/profile/spawn/objects/scoreboard/importvanilla | yes | stubs or messages | | Jigsaw Studio create/edit/autosave/export commands and GUI | yes | no; copy a Bukkit-authored Iris pack | -| Structure import/capture | yes | messages (run on Bukkit, copy pack) | +| Structure import/capture | yes | messages (run on Bukkit, copy the pack) | | Datapack Modrinth ingest/remove | yes | messages | | Datapack status/install (dimension types) | — | yes | -| `regen` / `goldenhash` | under `developer` | root | +| `regen` / `goldenhash` | under `Developer` | root | | Pregen flags | `serial`, `gui`, center string | `sync`, `gui`, `nocache`, `at x z` | -| Tree feller permission | `iris.treefeller` | loader-specific node | +| `pack validate` / `status` with no pack | rejected (blank default is required) | validates all packs | +| Tree feller permission | `iris.treefeller` | `irisworldgen:treefeller` via the loader permission API | --- diff --git a/docs/05 - Concepts & Pack Layout.md b/docs/05 - Concepts & Pack Layout.md index b9b379a87..e3de175ac 100644 --- a/docs/05 - Concepts & Pack Layout.md +++ b/docs/05 - Concepts & Pack Layout.md @@ -1,165 +1,229 @@ # 05 - Concepts & Pack Layout -An Iris pack is a directory of JSON, binary objects, and optional assets under the platform packs root (`packs//` on Bukkit-family; `config/irisworldgen/packs//` on Fabric/Forge/NeoForge). `IrisData` is the pack loader: it registers one `ResourceLoader` per registrant type, resolves keys to files, caches loads, and expands snippet references during JSON parse. Production worlds use a copied pack snapshot under the world folder; studio worlds load the live pack directory with hotload. +A pack is a folder of JSON files, binary objects, and images that fully describes one or more worlds. Iris loads it through `IrisData`, which registers one loader per resource type, turns short string keys into files on disk, and caches what it reads. Every world you create gets its own frozen copy of the pack; only Studio worlds read the folder you are editing. See also: `00 - Overview.md`, `01 - Installation & Platforms.md`, `10 - Studio & VSCode Schemas.md`, `11 - Dimensions.md`, `24 - Pack Mods & Snippets.md`, `25 - Pack Management.md`. -## Tutorial: trace one resource through a pack +## What a pack actually is -Prerequisites: a loadable pack under the correct platform packs root, command permission, and an editor that preserves JSON syntax. Use this exercise before authoring a large pack: +There is no manifest file, no registry, and no build step. A pack is a directory whose subfolder names tell Iris what type each file is. `biomes/plains.json` is a biome because it sits in `biomes/`. Move that same file to `regions/` and Iris will try to parse it as a region. -1. Validate the pack before editing: `/iris pack validate pack=overworld` on Bukkit, or `/iris pack validate overworld` on a mod loader. Substitute your pack key consistently when tracing another pack. -2. Open `dimensions/.json` and pick one key from its `regions` array. -3. Open `regions/.json` and pick one root key from `landBiomes`. -4. Open `biomes/.json` and follow its first generator, object, decorator, or structure reference to the matching registrant folder. -5. Confirm every key is the file path relative to that registrant folder with the extension removed. -6. Open the pack in Studio and focus that region or biome while editing. Save one valid change, wait for hotload, then rerun pack validation before creating a production snapshot. +The pack folder's own name is the pack key. A folder called `packs/myworld/` is the pack `myworld`. Rename the folder and you have renamed the pack. -The exercise passes when every reference resolves without guessing a namespace or filename, Studio hotload succeeds, and validation has no blocking errors. If a file exists but never appears, work backward from the dimension graph; unreferenced files are valid but unreachable. +The only hard requirement is at least one `.json` file directly inside `dimensions/`. Everything else is optional, and a folder you never create simply has no resources of that type. -### Resource-resolution recovery - -| Symptom | Likely cause | Recovery | -|---|---|---| -| File exists but its key is unresolved | Extension or type-folder prefix was included, path case differs, or the reference starts from the wrong registrant root | Rebuild the key as the exact relative path under the type folder, without extension | -| File validates but never generates | It is not reachable from the active dimension → region → biome graph, or its chance/filter excludes it | Trace references from the dimension root and test with Studio focus/buffet modes | -| Studio schema does not list a new resource | Workspace schema/resource enums are stale | Run `/iris studio update dimension=` on Bukkit or `/iris studio update ` on modded | -| Two files appear to share a key | Dotted variants or same-base-name candidates are ambiguous | Keep one canonical filename; Iris warns and otherwise selects the sorted first match | -| Production world ignores a corrected resource | It is reading its copied snapshot | Validate in Studio, then use the explicit world-update workflow or create a new world | - -## Content model - -| Concept | Role | -|---------|------| -| Pack | Directory under the packs root; pack folder name is the pack key | -| Dimension | Root world type under `dimensions/`; at least one is required | -| Region | Spatial zone listing biomes and region content | -| Biome | Terrain layers, surface, decorations, objects, structures, spawns | -| Generator | Height / noise generator definitions | -| Object | Placed block models (`.iob`) | -| Structure | Iris multi-piece / native structure graphs under `structures/` | -| Jigsaw pool / piece | Pool and piece JSON for Iris jigsaw assembly | -| Entity / spawner / marker / loot | Entity definitions, spawn rules, markers, loot tables | -| Mod schema | Inactive injector/replacer documents under `mods/`; loaded for schema/tooling but not applied by the engine | -| Expression / block / image | Expressions, custom blocks, PNG sampling maps | -| Snippet | Reusable JSON fragments under `snippet//` | -| Studio | Transient authoring world bound to the live pack with file hotload | -| World pack snapshot | Frozen copy at `/iris/pack` used by non-studio worlds | - -## Pack roots - -| Context | Path | -|---------|------| -| Authoring / download target | Platform data dir `packs//` | -| Production world (non-studio) | World dimension root `iris/pack/` (see `06 - Worlds & Lifecycle.md`) | -| Studio world | Same as authoring pack path; no world copy unless benchmark | -| Prefetch cache | Platform `prefetch/` (loader key indexes) | -| Schemas (studio) | Pack-local `.iris/schema/` | - -Pack folder names listed by Iris must be visible directories (not hidden names starting with `.`). Symbolic-link pack trees are rejected for download/replace and validation safety checks. - -## Registrant folders - -Every pack type that `IrisData` registers maps to a folder name returned by the registrant’s `getFolderName()`. Nested subfolders under a type root are allowed; the load key is the path relative to that root without the file extension. - -| Folder | Type | Files | Required | -|--------|------|-------|----------| -| `dimensions/` | Dimension | `*.json` | **Yes** — pack is not loadable without at least one | -| `regions/` | Region | `*.json` | Optional | -| `biomes/` | Biome | `*.json` | Optional | -| `generators/` | Generator | `*.json` | Optional | -| `objects/` | Object | `*.iob` | Optional | -| `matter/` | Matter object | matter binary (loader-specific) | Optional; loader only, no runtime consumer | -| `structures/` | Structure | `*.json` | Optional | -| `jigsaw-pools/` | Jigsaw pool | `*.json` | Optional | -| `jigsaw-pieces/` | Jigsaw piece | `*.json` | Optional | -| `entities/` | Entity | `*.json` | Optional | -| `spawners/` | Spawner | `*.json` | Optional | -| `markers/` | Marker | `*.json` | Optional | -| `loot/` | Loot table | `*.json` | Optional | -| `mods/` | Pack mod schema | `*.json` | Optional; no runtime application path | -| `blocks/` | Block data | `*.json` | Optional | -| `expressions/` | Expression | `*.json` | Optional | -| `images/` | Image | `*.png` | Optional | -| `snippet/` | Snippet library | `snippet//**.json` | Optional (not a registrant loader) | - -Live overworld also contains authoring-only or empty trees that loaders do not register as types (for example empty `caves/`, `ravines/`, `jigsaw-structures/`, plus pack-local `README.md`, workspace files, and `Schema.json`). Those names are not keys in `IrisData`. - -## Key rules - -- **Load key** is the path under the type folder without extension. Example: `biomes/temperate/plains.json` → key `temperate/plains`. -- Exact `name + extension` wins over dotted variants (`plains.json` beats `plains.disabled.json` when both match the base name rule). -- Ambiguous same-base-name matches log a warning and pick the sorted first file. -- Literal key `"null"` is refused. -- Cross-references between resources use these load keys (region biome lists, structure placements, spawner entity ids, and similar). -- Pack dimension selectors for world create accept `pack`, `pack:dimensionKey`, or `default` (resolves to `settings.generator.defaultWorldType`, default `overworld`). See `04 - Commands & Permissions.md` and `06 - Worlds & Lifecycle.md`. -- Download destination pack keys must match `[a-z0-9_-]+` and are taken from the single dimension load key in the archive. - -## Dimensions required - -`PackValidator` fails a pack as not loadable when: - -1. The pack folder is missing or not a directory. -2. `dimensions/` is missing. -3. `dimensions/` has no `*.json` files. - -A downloaded archive without an expected key is rejected unless it contains exactly one loadable dimension, whose load key becomes the install key. Managed-release and listing downloads carry an exact expected primary key, so their archive may retain additional dimension resources; the expected dimension selects the folder key and the entire pack still validates before publication. Presence of a pack on disk is defined as a safe pack directory with at least one non-symlink `dimensions/*.json` file. - -## Snippets - -Types annotated with `@Snippet("")` may be written inline as JSON objects **or** as a string reference to a snippet file. - -| Rule | Behavior | -|------|----------| -| Path form | `"snippet//"` (optional shorter forms are normalized onto `snippet//`) | -| On-disk file | `snippet//.json` under the pack root | -| Nested snippet keys | Subfolders under `snippet//` are allowed; listed keys keep the `snippet/` prefix | -| Schema | Studio schema builder exposes `snippet//…` enums and pattern matches under `.iris/schema/snippet/` | -| Shipping example | Overworld uses `snippet/decorator/*` and `snippet/style/*` | - -Snippet parse failures log errors and yield `null` for that field; full pack validation treats broken graphs as blocking or warning depending on the validator. - -## Studio pack vs world pack snapshot - -| Mode | Pack path used by engine | Hotload | Copy on create | -|------|--------------------------|---------|----------------| -| Studio (`studio=true`) | Live `packs//` (or studio project path) | Yes — polls pack for `.json`/`.iob` changes (~1s latch), excludes `.iris` | No pack install into world (unless benchmark) | -| Production create | Installs full pack tree into `/iris/pack` | No | Atomic copy via `StudioSVC.installIntoWorld` | -| Benchmark | Installs into world pack path | Studio flag still drives transient cleanup rules | Yes when `benchmark` | - -Hotload opens a new `IrisData` runtime from the same folder, reloads the dimension key, rebuilds engine runtime under a lifecycle lock, retires the previous data, and refreshes workspace/datapacks. Production engines load only the world snapshot; editing `packs/` does not affect existing non-studio worlds until the snapshot is replaced (see `25 - Pack Management.md` `update-world`). - -## Minimal pack layout +## A pack you can read in one screen ```text packs/myworld/ dimensions/ - myworld.json + myworld.json -> key "myworld" regions/ - main.json + main.json -> key "main" biomes/ - plains.json + plains.json -> key "plains" + hills/ + rolling.json -> key "hills/rolling" generators/ - plain.json + plain.json -> key "plain" ``` -A pack with only `dimensions/*.json` is structurally valid for presence and basic validation; generation quality depends on the dimension’s referenced regions/biomes/generators. For a walkthrough see `26 - Example - Minimal Dimension.md`. +Five files. `dimensions/myworld.json` lists `"main"` in its `regions` array. `regions/main.json` lists `"plains"` and `"hills/rolling"` in `landBiomes`. Each biome names `"plain"` as a generator. That chain is the whole pack. -## Live overworld folder map (shipping pack) +Note `hills/rolling`. Subfolders are yours to organize however you like — they become part of the key, and nothing else changes. -Adapter run configurations use these additional paths (`…/packs/overworld/`): +## Keys: the one rule -| Path | Contents (summary) | -|------|--------------------| -| `dimensions/overworld.json` | Root dimension | -| `regions/*.json` | Climate / biome zones | -| `biomes/**` | Nested biome sets (temperate, hot, frozen, ocean, …) | -| `generators/**` | Terrain generators | -| `objects/**` | `.iob` trees, structures, clutter, vanilla imports | -| `structures/*.json` | Structure graphs (including minecraft_* graphs) | +**A key is the file's path under its type folder, with the extension removed.** + +| File on disk | Type folder | Key you write in JSON | +|---|---|---| +| `biomes/plains.json` | `biomes/` | `plains` | +| `biomes/temperate/plains.json` | `biomes/` | `temperate/plains` | +| `objects/trees/oak/big.iob` | `objects/` | `trees/oak/big` | +| `snippet/style/soft.json` | (snippets, see below) | `snippet/style/soft` | + +There is no namespace and no type prefix. You never write `biomes/plains` or `iris:plains` — the field you are filling in already knows it wants a biome, so it searches `biomes/` for you. Cross-references everywhere (region biome lists, object placements, spawner entity ids, structure piece pools) use exactly these keys. + +### What happens when the exact file is missing + +Iris first tries `/.json` and returns it if it exists. That is the normal path and the only one that works for nested keys. + +If there is no exact hit, Iris scans the type folder's own files (not subfolders) for any name whose first dot-segment equals the key. This is what makes `plains.disabled.json` still load for key `plains` — useful for parking a variant, surprising if you forgot you did it. If two files match, Iris logs `Ambiguous in : ...` and takes the alphabetically first one. Keep one canonical filename per key and this never bites you. + +The literal string `"null"` is refused with a warning by direct file lookups and by warning-enabled loads. Silent loads (the cross-pack fallback search) do not refuse it and will look for `null.json`. Do not name a file `null.json`. + +## How the pieces relate + +Generation walks a graph, and the graph starts at exactly one place: the dimension you named when you created the world. + +```text +dimension -> regions -> biomes -> generators (terrain height/noise) + -> objects (.iob models) + -> decorators (surface clutter) + -> structures (jigsaw / native) + -> spawners -> entities + -> loot +``` + +- **Dimension** — the root. Sets world height, environment, seed behavior, and which regions exist. One dimension equals one world type. +- **Region** — a spatial zone. Regions decide which biomes can appear where, and can carry their own objects and structures that span biome edges. +- **Biome** — the workhorse. Block layers, surface treatment, decorations, object placements, structures, and mob spawns. +- **Generator** — noise and height math. Biomes reference generators to get terrain shape; several biomes can share one. +- **Object** — a `.iob` block model with its own placement rules. +- **Structure / jigsaw pool / jigsaw piece** — multi-piece assemblies, either Iris-native or bridged to vanilla structures. + +The practical consequence: **a file that nothing references is inert.** It parses, it validates, it never generates. When a resource you wrote is not showing up, the first question is not "is the JSON wrong" but "is it reachable from the dimension." Work forward from `dimensions/.json` and find where the chain breaks. + +## Trace one reference end to end + +Do this once on a pack you did not write. It takes two minutes and makes everything above concrete. + +Prerequisites: a loadable pack under the packs root, `iris.all` (Bukkit) or gamemaster (modded), and an editor that will not reformat your JSON. + +1. Validate first, so you know a later failure is yours: `/iris pack validate pack=overworld` on Bukkit, `/iris pack validate overworld` on a mod loader. +2. Open `dimensions/overworld.json`. Pick one key out of the `regions` array. +3. Open `regions/.json`. Pick one key out of `landBiomes`. +4. Open `biomes/.json`. Follow its first generator, object, decorator, or structure reference into the matching type folder. +5. At each hop, confirm the key is the path under the type folder with the extension removed — nothing more. +6. Open the pack in Studio, focus that biome, save one valid edit, and wait for hotload. Re-validate. + +You are done when every reference resolved without guessing at a namespace or filename, hotload succeeded, and validation reports no blocking errors. + +## Snippets + +A snippet is a JSON fragment you write once and reference from many places. Types tagged `@Snippet("")` in the engine accept either an inline object or a string pointing at a snippet file. + +```json +"style": "snippet/style/soft-hills" +``` + +resolves to `/snippet/style/soft-hills.json`. + +| Rule | Actual behavior | +|---|---| +| Trigger | Only a JSON **string** value. Inline objects parse normally and never touch the snippet path | +| Required prefix | The string must start with `snippet/`. Anything else resolves to `null` **with no log line at all** — the most common silent snippet failure | +| Re-rooting | If the string starts with `snippet/` but not `snippet//`, Iris strips `snippet/` and re-roots the remainder under this field's own type. So `snippet/decorator/foo` on a style field becomes `snippet/style/decorator/foo`, not an error | +| On-disk path | `/snippet//.json`, resolved from the pack root, not from the type folder | +| Subfolders | Allowed; `` may contain `/`. Discovery walks the tree recursively | +| Missing file | Logs `Couldn't find snippet in ` and yields `null` for that field | +| Unreadable file | Logs `Couldn't read snippet in ()` and yields `null` | +| Inline parse failure | Different path: logs `Failed to read ... faking objects a little`, then substitutes a **default-constructed instance**, not `null` | +| Schema | Studio writes `.iris/schema/snippet/-schema.json` so the editor offers completions for `snippet//…` | + +The shipping overworld uses `snippet/decorator/*` and `snippet/style/*`. + +## Two copies of every pack + +This is the concept that causes the most confusion, so it is worth being blunt about. + +**The pack you edit and the pack a world generates from are different files.** + +When you create a non-Studio world, Iris copies the entire pack tree into `/iris/pack` and the world's engine reads only that copy for the rest of its life. Editing `packs/overworld/` afterwards changes nothing about that world. This is deliberate: a world's terrain must stay reproducible even if you keep authoring. + +Studio worlds are the exception. A Studio world's engine points directly at the live pack folder and watches it for changes, which is what makes hotload possible. + +| Mode | Pack the engine reads | Hotload | Copied into the world? | +|---|---|---|---| +| Studio (`studio=true`) | Live `packs//` (or the studio project path) | Yes | No | +| Production create | `/iris/pack` | No | Yes, atomic stage then publish via `StudioSVC.installIntoWorld` | +| Benchmark | `/iris/pack` | Studio flag still governs transient cleanup | Yes | + +Hotload opens a fresh `IrisData` on the same folder, reloads the dimension by its key, builds a replacement engine runtime under the lifecycle lock, publishes it, retires the old `IrisData`, then refreshes the editor workspace and datapacks in the background. If any step fails it rolls back to the previous runtime and reports the error. + +The watcher polls every 250 ms but only checks the folder about once per second, backing off to once per 4 s during maintenance or within 2 s of chunk generation. It watches `.json` and `.iob` and ignores anything under `.iris`. It runs only while the world is a Studio world that is not closing and not in jigsaw-studio mode. + +To push pack edits into an existing production world, see `update-world` in `25 - Pack Management.md`, or just create a new world — which is the right answer for any change to height or dimension type. + +## Where packs live + +| What | Bukkit-family | Fabric / Forge / NeoForge | +|---|---|---| +| Packs you author and download into | `plugins/Iris/packs//` | `config/irisworldgen/packs//` | +| Platform data dir (`settings.json`, languages, caches) | `plugins/Iris/` | `config/iris/` | +| A world's frozen snapshot | `/iris/pack/` | same, under the modded world root | +| Prefetch key indexes | `/prefetch//.ipfch` | same | +| Studio schemas | `/.iris/schema/` | same | + +On mod loaders the pack root and the platform data dir are two different folders — packs go under `config/irisworldgen/`, everything else under `config/iris/`. If you are hand-placing a pack on a modded server, `config/irisworldgen/packs/` is the one that matters. + +Folders whose names start with `.` are skipped when Iris lists packs, which is why `.iris/` inside a pack is invisible to the pack listing. Pack listing itself follows symbolic links; the stricter check (`requireSafePackTree`, used when installing a pack into a world) refuses a symlinked root, any symlink anywhere in the tree, and any non-regular file, and skips hidden subtrees. + +## Registrant folders + +`IrisData` registers 17 loaders. Each one owns exactly one folder name and one file extension. + +| Folder | Extension | What lives here and when you touch it | +|---|---|---| +| `dimensions/` | `.json` | World roots. Height, environment, region list, imports. **Required** — a pack with none is not loadable | +| `regions/` | `.json` | Which biomes appear in which climate zone, plus region-wide objects and structures | +| `biomes/` | `.json` | Where most authoring time goes: layers, surface, decorators, objects, structures, spawns | +| `generators/` | `.json` | Reusable noise/height math that biomes point at. Edit here to change terrain shape across many biomes at once | +| `objects/` | `.iob` | Binary block models saved from the wand or imported from schematics. Referenced by placements, never edited as text | +| `structures/` | `.json` | Structure graphs, including the `minecraft_*` graphs that bridge vanilla structures | +| `jigsaw-pools/` | `.json` | Weighted sets of pieces a jigsaw connector can pick from | +| `jigsaw-pieces/` | `.json` | One placeable piece: its object, its connectors, its rules | +| `entities/` | `.json` | Entity definitions with equipment, attributes, and custom data, used by spawners and markers | +| `spawners/` | `.json` | When and where entities spawn — time, block, biome, and rate rules | +| `markers/` | `.json` | Named points Iris records during generation so spawners and other systems can find them later | +| `loot/` | `.json` | Iris loot tables applied to generated containers | +| `blocks/` | `.json` | Named custom block states you can reference instead of repeating long block data strings | +| `expressions/` | `.json` | Math expressions callable from generators and placement rules | +| `images/` | `.png` | PNG maps sampled as noise or as direct biome/height input | +| `matter/` | `.mat` | Matter binaries. The loader exists and resolves keys, but no runtime system consumes them | +| `mods/` | `.json` | Injector/replacer documents. Loaded so schemas and tooling see them; the engine has no path that applies them | + +Anything else in a pack directory is not a resource type. The shipping overworld ships empty `caves/`, `ravines/`, and `jigsaw-structures/` folders plus `README.md`, `Schema.json`, and a `.code-workspace` file — none of those names are keys, and none are loaded. + +A reduced init path used by the datapack compiler registers only `biomes` and `dimensions`; that is internal and not something a pack author configures. + +## What makes a pack loadable + +`PackValidator` fails fast on three structural problems, in order: + +1. The pack folder is missing or is not a directory. +2. There is no `dimensions/` directory. +3. There are no `*.json` files **directly inside** `dimensions/`. Nested dimension files do not count toward this check. + +Passing those three does not mean the pack is loadable. `PackValidator` then runs roughly ten content validators — dimension, cave profile, loot, object/surface, structure graph, native structure, spawn, and content-key checks — and any blocking error from those also makes the pack not loadable. Content-key problems are blocking only under strict content mode. Read the first blocking error and fix that one; the rest are usually downstream. + +Presence on disk is a weaker notion than loadability: a pack "exists" if its directory is safe and holds at least one non-symlink `dimensions/*.json`. + +### Download key rules + +Downloaded pack keys must match `[a-z0-9_-]+`, and the check applies both to a caller-supplied expected key and to the key Iris derives from the archive. + +An archive with no expected key must contain exactly one dimension; its load key becomes the install folder name. Managed-release and listing downloads carry an exact expected key, so their archive may hold extra dimensions — the expected key picks the folder name, and the whole pack is validated before publication. + +## When a resource does not resolve + +| Symptom | Likely cause | Fix | +|---|---|---| +| File exists, key does not resolve | You included the extension or the type folder in the key, the case differs, or you counted the path from the wrong root | Rebuild the key as the exact relative path under the type folder, extension removed | +| Nested dotted variant not found | The dotted-name fallback only scans the type folder's own files, never subfolders | Give nested files their exact key name, or move the variant to the type folder root | +| File validates but never generates | Nothing in the dimension → region → biome chain references it, or a chance/filter excludes it | Trace forward from the dimension root; test with Studio focus or buffet mode | +| Snippet silently becomes null | The string does not start with `snippet/` — this failure logs nothing | Write the full `snippet//` form | +| Snippet loaded the wrong file | A `snippet//…` string was re-rooted under this field's own type | Use the type that matches the field | +| Studio does not offer a new resource in completions | Workspace schema enums are stale | `/iris studio update dimension=` on Bukkit, `/iris studio update ` on modded | +| Console warns "Ambiguous \ \" | Two files share a base name before the first dot | Keep one canonical filename; Iris took the alphabetically first | +| Production world ignores your fix | It is reading `/iris/pack`, not your live pack | Validate in Studio, then run the explicit world-update workflow or create a new world | + +## The shipping overworld pack + +For orientation when reading `packs/overworld/`: + +| Path | What is in it | +|---|---| +| `dimensions/overworld.json` | The single root dimension | +| `regions/*.json` | Climate zones that partition the biome set | +| `biomes/**` | Nested biome sets — temperate, hot, frozen, ocean, and so on | +| `generators/**` | Shared terrain generators | +| `objects/**` | `.iob` trees, structures, clutter, and vanilla imports | +| `structures/*.json` | Structure graphs, including `minecraft_*` bridges | | `jigsaw-pieces/**`, `jigsaw-pools/**` | Jigsaw assembly data | -| `entities/standard/**`, `spawners/**`, `loot/**` | Entities, spawners, loot | -| `images/*.png` | Noise / map images | -| `snippet/decorator/**`, `snippet/style/**` | Shared snippets | +| `entities/standard/**`, `spawners/**`, `loot/**` | Mob and loot content | +| `images/*.png` | Noise and map images | +| `snippet/decorator/**`, `snippet/style/**` | Shared fragments referenced across biomes | -Related feature docs: `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `18 - Structures Overview.md`, `19 - Objects.md`, `21 - Jigsaw Structures.md`, `23 - Loot, Entities, Spawners, Markers.md`. +Feature-level detail: `12 - Regions.md`, `13 - Biomes.md`, `14 - Generators & Noise.md`, `18 - Structures Overview.md`, `19 - Objects.md`, `21 - Jigsaw Structures.md`, `23 - Loot, Entities, Spawners, Markers.md`. diff --git a/docs/06 - Worlds & Lifecycle.md b/docs/06 - Worlds & Lifecycle.md index 4ab78bcbe..b7829a3f3 100644 --- a/docs/06 - Worlds & Lifecycle.md +++ b/docs/06 - Worlds & Lifecycle.md @@ -1,63 +1,287 @@ # 06 - Worlds & Lifecycle -Iris manages world identity, storage paths, pack installation, creation, persistence, and removal across Bukkit-family servers and the three mod loaders. Bukkit-managed Iris worlds live under the level root as `dimensions/iris//` with namespace `iris`; modded dimensions persist through `iris-dimensions.json`. Non-Studio worlds carry a frozen pack at `iris/pack`, validated by its exact normalized root rather than the common `pack` folder name, while Studio worlds bind the live packs directory. +Creating an Iris world copies the pack into the world folder, registers the world so the server rebuilds it on every boot, and hands generation to the Iris engine. This page covers the full lifecycle on Bukkit-family servers and on Fabric, Forge, and NeoForge: create, load, unload, remove, main-world promotion, and the exact-slot replacement path. Iris worlds are managed under the level root as `dimensions/iris//` on Bukkit; mod loaders keep theirs in `iris-dimensions.json`. -See also: `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`. +See also: `02 - Getting Started.md`, `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `07 - Pregeneration.md`, `10 - Studio & VSCode Schemas.md`, `30 - Platform Differences.md`. -## Tutorial: promote a tested pack to a persistent world +## Create a world you intend to keep -Prerequisites: a validated pack, a fixed test seed, a current backup, and no active lifecycle or pack-publish operation. The commands below use the installed `overworld` pack and disposable world name `release_candidate`; substitute one pack key consistently when promoting a different pack. +The difference between a throwaway world and one you will still be running in six months is that you decide the pack, seed, and height **before** the first chunk generates. None of those are editable afterwards without regenerating terrain. + +Before you start: a pack that validates, a seed you have written down, a current backup, and no other lifecycle command running. ### Bukkit-family -1. Validate the live pack: `/iris pack validate pack=overworld`. -2. Open it with `/iris studio open overworld seed=1337`, generate representative terrain, then run `/iris studio close` after the final hotload succeeds. -3. Create a new world with explicit identity: `/iris create release_candidate type=overworld seed=1337`. -4. On Folia, stop and restart after the staging message. On other Bukkit-family servers, continue after `/iris worlds` lists `release_candidate` as loaded. -5. Enter it: `/iris tp release_candidate`. -6. Generate a bounded baseline: `/iris pregen start 352 world=release_candidate center=0,0 gui=false`. -7. Wait for completion, restart cleanly, return with `/iris tp release_candidate`, and generate one new boundary chunk. +```text +/iris pack validate pack=overworld +/iris studio open overworld seed=1337 +``` -The workflow passes when the world reloads with the same seed and dimension, the pregenerated area loads without generation failures, and new terrain still comes from `/iris/pack`. Never replace or delete that snapshot while its world is loaded. Continued edits under `packs/overworld/` affect Studio only; publish deliberately through `25 - Pack Management.md` or create a new world for breaking height/type changes. +Fly around, look at the terrain, then close the studio: + +```text +/iris studio close +``` + +Now create the real world. This is the step that freezes the pack: + +```text +/iris create release_candidate type=overworld seed=1337 +``` + +On Folia, this stages files and prints a restart instruction — stop the server, start it again, and the world loads on boot. On every other Bukkit-family server the world is created immediately. + +```text +/iris worlds +/iris tp release_candidate +``` + +**Success looks like:** `release_candidate` appears in `/iris worlds` as a loaded Iris world, you spawn in it, and chunks generate as you fly. + +Now prove it survives a restart, because a world that only works in the session that created it is not actually created: + +```text +/iris pregen start 352 world=release_candidate center=0,0 gui=false +``` + +Wait for it to finish (see `07 - Pregeneration.md`), restart the server cleanly, teleport back in, and fly past the pregenerated boundary. New terrain must still appear. + +**The world is now committed.** It generates from `/iris/pack`, its own frozen copy. Continuing to edit `packs/overworld/` affects Studio only. Never delete or replace that snapshot while the world is loaded. To ship pack changes into it later, use the deliberate path in `25 - Pack Management.md`; for anything that changes height or dimension type, create a new world instead. ### Fabric / Forge / NeoForge -1. Validate the installed pack: `/iris pack validate overworld`. -2. Enable a persistent dimension: `/iris world enable irisworldgen:release_candidate overworld 1337`. -3. Confirm it in `/iris world status`, then enter it with `/iris tp irisworldgen:release_candidate`. -4. Run `/iris pregen start 352 irisworldgen:release_candidate at 0 0` and wait for completion. -5. Restart the server. Confirm `/iris world status` restores the same dimension and pack, then run `/iris info irisworldgen:release_candidate` as a gamemaster to verify seed `1337` from `iris-dimensions.json`. +```text +/iris pack validate overworld +/iris world enable irisworldgen:release_candidate overworld 1337 +/iris world status +/iris tp irisworldgen:release_candidate +``` -This workflow passes when the dimension is re-injected after restart and generates normally. `/iris world disable` unloads while retaining persistent data; `/iris world delete` is the destructive removal path. +The seed argument is optional and defaults to `1337`. `enable` also accepts the alias `create`, and the whole group is reachable as `/iris w`. -### Lifecycle recovery +```text +/iris pregen start 352 irisworldgen:release_candidate at 0 0 +``` -| Symptom | Meaning | Recovery | +Restart the server when it finishes. + +**Success looks like:** `/iris world status` lists the same dimension with the same pack after restart, and `/iris info irisworldgen:release_candidate` as a gamemaster reports seed `1337` read back from `iris-dimensions.json`. + +From here, `/iris world disable ` unloads it and keeps the files; `/iris world delete ` is the destructive path. Both require the dimension argument. + +## Remove a world without losing anything else + +Removal is the operation most likely to cost you data, so the order matters. + +1. **Back up first.** Nothing below is undoable. +2. **Get everyone out.** `/iris evacuate ` moves players to another loaded world, or kicks them if there is nowhere to go. Removal does this for you, but doing it deliberately means you see who was in there. +3. **Unload it.** `/iris unload `. This marks the world for maintenance, evacuates, unloads through the lifecycle service, and closes the generator. +4. **Remove it.** `/iris remove ` deletes the files. `/iris remove delete=false` keeps them and only unregisters — use this when you want the directory back later. +5. **Read the status Iris prints.** It tells you what actually happened; see the status table below. + +**Success looks like:** `UNREGISTERED` (files kept) or `DELETED` (files gone), the world is absent from `/iris worlds`, and its directory under `/dimensions/iris/` matches what you asked for. + +`DELETE_QUEUED` means the files could not be deleted now and were quarantined for deletion at next startup. Restart and confirm the target is gone before reusing that name. + +On mod loaders the equivalent is `/iris world delete `, which disables and then wipes chunk and mantle data. + +### If unload hangs + +Unload has a hard 150-second ceiling. If the world, generator, or scheduler work has not settled by then, Iris marks a terminal timeout, requests a server restart, and fails the command. Let the restart happen. Do not delete a live world directory to force the issue. + +## Lifecycle recovery + +| Symptom | What it means | What to do | |---|---|---| -| Command reports busy | Another `WORLD_MUTATION` or `PACK_MUTATION` lease owns the lifecycle coordinator | Let that operation finish; do not retry concurrent create/remove/update commands | -| Login or create reports startup validation pending/failed/restart-required | External datapacks or dimension-pack validation has not reached a safe state | Fix the first logged failure or complete the requested restart; do not create folders or add `bukkit.yml` entries manually | -| Folia create succeeds but teleport cannot find the world | Creation staged files and registration only | Restart, then load/teleport as instructed by the staging result | -| Bukkit load reports missing or inconsistent data | Managed dimension root, registration, or `iris/pack` snapshot is incomplete | Keep the directory, restore from backup, and reconcile registration before retrying; load never redownloads the snapshot | -| Unload reaches its terminal timeout | World, generator, or scheduler work did not settle within 150 seconds | Allow the requested restart; do not force-delete the live directory | -| Remove returns `DELETE_QUEUED` | Files were quarantined for startup deletion | Restart and confirm the target is gone before reusing its name | -| Modded registry is quarantined as `.broken-` | Whole-file JSON could not be parsed | Keep the backup, recreate or repair each logged id with the original pack/dimension/seed, then verify status | +| "busy" response | Another lifecycle operation holds the coordinator. It is one global mutex, so a pack download or publish blocks world create just as much as another create does | Wait for the running operation. Retrying concurrently will not help | +| Startup validation pending / failed / restart-required on login or create | External datapack ingestion or dimension-pack validation has not reached a safe state | Fix the first logged failure, or complete the requested restart. Do not hand-create world folders or hand-edit `bukkit.yml` | +| Folia create succeeded but teleport says no such world | Folia create only stages files and registration | Restart, then load or teleport | +| Load reports missing or inconsistent data | The dimension root, the `bukkit.yml` registration, or the `iris/pack` snapshot is incomplete | Keep the directory and restore from backup. Load never re-downloads a snapshot | +| Unload hits its terminal timeout | Work did not drain in 150 s | Allow the restart. Do not force-delete the live directory | +| Remove returns `DELETE_QUEUED` | Files were quarantined for startup deletion | Restart, confirm the target is gone, then reuse the name | +| Modded registry renamed to `.broken-` | The whole `iris-dimensions.json` failed to parse | Keep the backup. Iris logs whatever ids it could salvage from the raw text; recreate each with its original pack, dimension, and seed, then verify with `/iris world status` | ## Identity and storage | Item | Rule | -|------|------| -| Managed namespace | `iris` only for Iris-managed create/load/remove targets | -| Logical name | For `iris:foo` the logical name is `foo` | -| Storage root | Level root (`Server#getLevelDirectory` on Paper; else `world-container/level-name`) | +|---|---| +| Managed namespace | The safe/managed API accepts `iris` only, so create, load, and remove can never touch a `minecraft:` or third-party dimension folder | +| Logical name | For `iris:foo` the logical name is `foo` — that is what you type in commands | +| Storage root | `Server#getLevelDirectory` on Paper. If that method is missing, Iris latches a permanent fallback to `/` read from `server.properties` (default `world`) | | Dimension folder | `/dimensions/iris//` | | Pack snapshot | `/iris/pack/` | -| Pregen cache dir | `/iris/pregen/` | -| Registry | `worlds.json` in Iris data + `bukkit.yml` worlds section for production worlds | -| Name constraints | Safe single path segment `[a-z0-9_-]+`; no `/`, `\`, `..`; reserved create names `iris` and `benchmark` rejected | +| Pregen cache | `/iris/pregen/` | +| Registry | `worlds.json` in the Iris data folder (a flat `worldIdentity → dimensionType` map, written atomically) plus the `worlds:` section of `bukkit.yml`, which stores `generator: "Iris:"` and the seed | +| Name normalization | The name is lowercased and spaces become `_` before validation, so `My World` becomes `my_world` rather than being rejected | +| Name constraints | After normalization the key must match `[a-z0-9_-]+`; `/`, `\`, and `..` are rejected as unsafe path segments, and symlinks on any component of the dimension root are refused | +| Reserved names | `/iris create` rejects `iris` and `benchmark` case-insensitively. This is a create-time check only; the storage layer does not enforce it | -Vanilla main/nether/end map to minecraft keys from `level-name` / `level-name_nether` / `level-name_the_end` and are not Iris-managed dimension folders. +The vanilla main, nether, and end worlds map to `minecraft:` keys derived from `level-name`, `level-name_nether`, and `level-name_the_end`. They are not Iris-managed dimension folders and are only reachable through the exact-slot replacement path below. -### Modded persistent-dimension registry +## Command surface (Bukkit) + +| Command | What it does | +|---|---| +| `/iris create [type=default] [seed=1337] [main=false] [overwrite=false]` | Create a managed world now, stage one for Folia's next boot, or stage an exact-slot replacement | +| `/iris load ` / `/iris import ` | Reconcile a world that already exists on disk back into the server. Never downloads anything | +| `/iris unload ` | Evacuate, unload, close the generator. The safe first half of removal | +| `/iris remove [delete=true]` | Unregister the world, and by default delete its files | +| `/iris evacuate ` | Move every player out of an Iris world, or kick them if no other world is loaded | +| `/iris tp [player=]` | Teleport yourself, or a named player, to the world spawn | +| `/iris worlds` | List which loaded worlds are Iris worlds and which are not | + +Aliases and permissions: `04 - Commands & Permissions.md`. + +### Create parameters + +| Param | Default | What it controls | +|---|---|---| +| `name` (`world-name`) | required | Becomes `iris:`. With `overwrite=true` it may instead name the configured main world or its `_nether` / `_the_end` alias | +| `type` (`dimension`, `pack`) | `default` | Which pack and dimension to generate from. `default` resolves to `settings.generator.defaultWorldType` (`overworld`); otherwise a pack name or `pack:dimensionKey` | +| `seed` | `1337` | The world seed. Ignored for exact vanilla-slot overwrites, which must keep the level's existing authoritative seed | +| `main` (`main-world`) | `false` | Promote this world to `level-name` in `server.properties`. Happens in a JVM shutdown hook on Paper-family, or inline during Folia staging | +| `overwrite` (`force`) | `false` | Stage a validated replacement of an existing world slot for the next restart. Never touches a loaded world in place | + +Create refuses to run on the primary thread. Before it takes a lifecycle lease it requires startup datapack validation to be ready and the chosen pack to have a loadable validation result; then the `WORLD_MUTATION` / `WORLD_CREATE` lease must be free or the command fails busy. A refusal at any of those gates leaves no dimension folder and no registration behind. + +## What create actually does (non-Folia) + +1. Resolve the managed key and dimension. No directory is created yet. +2. Require startup datapack readiness and a loadable validation result for the owning pack. +3. Install datapacks for the dimension types. If the types are not loaded yet, queue a restart. +4. Copy the pack into `/iris/pack` through `StudioSVC.installIntoWorld` — staged into a temp directory, published atomically, then validated at that exact root. A validation failure rolls the publication back. +5. Build a `WorldCreator` with the Iris generator and `studio=false`. +6. Create the world through `WorldLifecycleService` / NMS async create, with a 120-second timeout. A timeout triggers a server restart rather than leaving a half-created world. +7. Register the world in `bukkit.yml` with the Iris generator, dimension key, and seed. Update the Multiverse link if Multiverse is present — that step has its own 30-second budget and also escalates to a restart. +8. Run creation-time pregen if the caller attached a `PregenTask` through the API. + +Rollback phases carry the same 120-second budget. + +## Folia staging + +Folia cannot create worlds at runtime, so `/iris create` becomes a staging operation: + +1. Require startup datapack readiness and a loadable pack validation result. +2. Acquire the `WORLD_CREATE` lease. +3. Install datapacks if they changed. +4. Abort if the dimension folder already exists. +5. Stage the pack into the managed dimension root through `installIntoWorld`; the published snapshot must pass exact-root validation. +6. Register the world in `bukkit.yml`. +7. If `main=true`, promote the main-world files immediately under the same lease. A failure here rolls back `bukkit.yml` and deletes the staged folder. +8. Tell the operator to restart. Generation and loading happen on the next boot. + +`WorldLifecycleStaging` holds the staged generator and biome provider for the backend that picks them up at load. + +## Exact world-slot replacement + +`overwrite=true` is how you put Iris generation into a slot that already exists — including the vanilla overworld, nether, or end. It uses lifecycle kind `WORLD_REPLACE` and always stages for a full restart. There is no in-place variant. + +It requires a Paper-family early bootstrap, which plain Spigot never runs; on Spigot the command fails closed. The target dimension folder must already exist — ordinary create is still the path for a new world. + +Accepted targets are safe `iris:*` keys and exactly three vanilla slots resolved from the configured level name: `minecraft:overworld`, `minecraft:the_nether`, and `minecraft:the_end`. A vanilla slot additionally requires: + +- a pack whose environment matches the slot (`NORMAL`, `NETHER`, or `THE_END`), checked both before staging and after install; +- `allow-nether` or `allow-end` enabled in the server config for those two slots. + +Foreign namespaces, other `minecraft:*` keys, path traversal, symlinks, and special filesystem entries all fail closed. `main=true` may accompany `overwrite` only when the target is `minecraft:overworld`. + +Minecraft stores one authoritative seed for a level, so every vanilla-slot replacement keeps the loaded overworld's seed and Iris warns you when that differs from the `seed` you passed. Changing the level seed is the ordinary new-main promotion workflow, not this one. + +### How the transaction is made safe + +The stage copies and validates a fresh frozen pack on the same filesystem, fingerprints it, binds a journal to the canonical level root and logical world name, records the original target and the existing `bukkit.yml` generator and seed, then compare-and-swaps that one configuration entry. Several distinct slots can be queued before a single restart. + +At the next boot, Paper's bootstrap reconciles each authorized transaction before Iris compiles its aggregate datapack and before Minecraft builds registries: it atomically moves the old dimension directory to a retained sibling backup and publishes the stage. The filesystem must support atomic replacement for the world directories, the journal, and `bukkit.yml`; without it Iris refuses rather than falling back to a destructive move. + +Publication retains Paper's per-world `data/paper/metadata.dat`, `data/paper/level_overrides.dat`, and `data/minecraft/world_gen_settings.dat` so the slot keeps its metadata and authoritative seed. Old `region`, `entities`, `poi`, and Iris runtime data are never merged — they stay in the backup, and the replacement starts from the staged snapshot. + +The backup is only eligible for deletion after `WorldLoad` proves the exact namespaced identity, Iris generator, selected dimension, seed, vanilla-slot environment, and an unchanged pack fingerprint. A failed check journals a rollback and requests another restart, after which cold bootstrap restores the retained directory and the prior `bukkit.yml` entry. A crash between any move, config write, or journal phase is retried idempotently. Conflicting manual configuration, changed roots or names, changed staged bytes, unsafe storage, or a duplicate or corrupt journal aborts early bootstrap and preserves the artifacts rather than guessing. + +## Studio create + +Studio worlds use `IrisCreator.studio(true)` and differ from production worlds in ways that matter: + +- Startup datapack validation and the pack's own validation must both be loadable before any Studio folder, snapshot, generator, or Bukkit world is created. Missing validation fails closed. +- The pack is **not** copied into the world folder, except for benchmark runs. The engine reads the live pack directly, which is what enables hotload. +- Studio worlds are transient. Unloaded Studio worlds are cleaned up, and their `bukkit.yml` entries are removed during shutdown cleanup. +- Open and close go through the `StudioSVC` transition queue (`10 - Studio & VSCode Schemas.md`). +- Biome Buffet prepares a changed focus before opening the chunk generation session; its exclusive fair-stage admission downgrades straight to the retained chunk permit so no other transition can slip in between the focus hotload and that chunk. +- Ordinary Studio suppresses native structure starts only while the initial FULL entry chunk loads, then restores them for later preview chunks. +- A failed open never unloads or closes the generator while that asynchronous entry request is still active. Another Studio open is rejected in the meantime; cleanup starts once it settles, and if it is still active 120 seconds later the transient world is queued for deletion at the next clean startup. + +## Load + +`/iris load` (alias `/iris import`) reconciles a world that already exists on disk: + +1. Parse the managed key and require the dimension root directory to exist. +2. Run `BukkitWorldReconciler.loadWorld(bukkit.yml, worldKey)`. +3. Report success, busy, restart-required, or failure. + +Load never downloads a pack. The world must already have `iris/pack` content and registration data consistent with Iris. Reconciliation checks startup readiness and then lazily validates that world's exact snapshot root before it touches `bukkit.yml` or calls a world backend. Validation results are path-scoped, so two worlds whose snapshot folders are both named `pack` cannot authorize or reject one another. + +## Unload + +`/iris unload` runs synchronously from a player origin: + +1. Require an Iris world and acquire the `WORLD_UNLOAD` lease. +2. Mark the world for maintenance. +3. `IrisToolbelt.evacuateAsync` → `WorldLifecycleService.unloadAsync(world, true)` → `generator.closeAsync()`. +4. On a 150-second terminal timeout, mark the timeout, request a server restart, and fail the future. + +There are two timers in play: the inner `WorldLifecycleService` unload has its own 120-second budget, and the command wraps the whole sequence in the 150-second ceiling. + +`WorldUnloadEvent` stops Iris engine maintenance immediately, but Iris does not treat it as proof that Paper's chunk scheduler has drained. Generator close waits for the raw world-lifecycle backend to confirm a successful unload, and the 26.2 noise pipeline holds one generation lease through terrain generation and worldgen-heightmap priming. + +## Evacuate + +`/iris evacuate ` moves every player out of an Iris world into another loaded world, or kicks them when there is nowhere else to go. It runs as a step inside both unload and removal, and is worth running on its own first so you can see who was affected. + +## Remove + +`/iris remove [delete=true]` delegates to `IrisWorldRemovalService`, which reports one of 18 statuses. + +| Status | Meaning | +|---|---| +| `UNREGISTERED` | Unloaded and unregistered; files kept. This is success for `delete=false` | +| `DELETED` | Unregistered and files deleted. Success for the default | +| `DELETE_QUEUED` | Files could not be deleted now and were quarantined for deletion at next startup. Restart and confirm before reusing the name | +| `BUSY` | Another world or pack mutation holds the coordinator | +| `INVALID_IDENTIFIER` | The name is not a parseable managed key | +| `PROTECTED_WORLD` | The target is a world Iris refuses to remove | +| `NOT_IRIS_WORLD` | The target exists but is not Iris-managed | +| `UNSAFE_PATH` | The resolved directory failed a path-safety check (traversal, symlink, wrong namespace) | +| `NOT_FOUND` | No such managed world | +| `RESOLUTION_FAILED` | Iris could not resolve the world identity to a directory | +| `TELEPORT_FAILED` | Players could not be evacuated, so removal stopped before touching files | +| `GENERATOR_CLOSE_FAILED` | The Iris generator did not close cleanly; the world may still hold resources | +| `UNLOAD_FAILED` | The server refused or failed to unload the world | +| `CONFIGURATION_FAILED` | The `bukkit.yml` entry could not be updated | +| `REGISTRY_FAILED` | The `worlds.json` registry could not be updated | +| `QUARANTINE_FAILED` | The world directory could not be moved to the quarantine name | +| `DELETE_FAILED` | Quarantine succeeded but deletion did not | +| `INTERNAL_FAILURE` | An unexpected error; read the logged cause | + +Any status other than `UNREGISTERED`, `DELETED`, or `DELETE_QUEUED` means the registry may have changed without the files being removed, and a quarantine directory may still exist. Check the world directory before retrying. + +Only safe `iris` namespace dimension paths are mutable. Each phase has a 120-second timeout and can request a restart when it gets stuck. + +With `delete=true`, Iris records the exact quarantine name in a durable startup queue **before** moving the directory, so a crash mid-delete still gets cleaned up on the next boot. Both immediate cleanup and the startup retry snapshot every directory's direct children before deleting, reject symlinks and special filesystem entries, and keep the queue entry with the full error when a concurrent writer or filesystem failure leaves content behind. + +## Main world promotion + +`main=true` on a non-Folia server installs a JVM shutdown hook. At shutdown it rewrites `level-name` and `level-seed` in `server.properties` and publishes files: + +1. Stage a temp directory (`..promoting-`) under the world container. +2. Copy the shared `data`, `datapacks`, and `players` folders from the current level root. +3. Copy the Iris dimension tree into `/dimensions/minecraft/overworld`. +4. Re-check that the target is absent, then move the stage into place with a plain rename. +5. Write `server.properties` atomically, with an fsync and an `ATOMIC_MOVE` (falling back to a non-atomic write if the filesystem refuses). + +Only step 5 is atomic; the directory move in step 4 is an ordinary rename. Promotion requires the target level folder to be absent and aborts if it finds a symlink anywhere in the copied tree. The whole sequence runs under a `WORLD_MUTATION` / `WORLD_PROMOTE` lease. Folia performs the same publish inline during staging rather than deferring to shutdown. + +To put Iris into the currently configured main slot **without** creating a new level root, name that exact main world and use `overwrite=true`. That keeps the top-level level root, shared datapacks, player data, and non-target dimensions intact. Plain `main=true` remains the new-level-root workflow above. + +## Modded persistent-dimension registry Fabric, Forge, and NeoForge persist dynamic Iris worlds in `/iris/iris-dimensions.json`: @@ -69,146 +293,26 @@ Fabric, Forge, and NeoForge persist dynamic Iris worlds in `/iris/ir } ``` -`id` is the registered dimension id, `pack` is the installed pack folder, `dimension` is its dimension load key, and `seed` is the generation seed. Writes use a temporary file plus atomic replacement when the filesystem supports it. Invalid individual entries are logged and preserved verbatim during ordinary updates; duplicate ids keep the first valid entry. +All four fields are required per entry: `id` is the registered dimension id, `pack` is the installed pack folder, `dimension` is its dimension load key, and `seed` is the generation seed. Writes go to a sibling `iris-dimensions.json.tmp`, get an fsync, and are moved into place with `ATOMIC_MOVE` where the filesystem supports it. -If the whole registry cannot be parsed during startup, Iris moves it to `iris-dimensions.json.broken-`, logs any ids it can recover from the raw text, and continues with no persistent Iris dimensions. Keep the quarantined file, repair or recreate each reported world with `/iris world create`, and verify pack/dimension/seed values before deleting the backup. +Entries that are individually invalid are logged, kept verbatim, and re-appended on the next write — Iris never silently drops one. Duplicate ids keep the first valid entry and warn. -## Command surface (Bukkit) +If the whole file fails to parse, only the startup load path quarantines it as `iris-dimensions.json.broken-`, salvages whatever ids it can from the raw text into the log, and continues with no persistent Iris dimensions. Every other code path throws rather than discard persistent worlds. Keep the quarantined file, recreate each reported world with `/iris world create`, verify pack, dimension, and seed, then delete the backup. -| Command | Effect | -|---------|--------| -| `/iris create [type=default] [seed=1337] [main=false] [overwrite=false]` | Create/Folia-stage a managed world, or stage an exact restart replacement | -| `/iris load ` / `/iris import ` | Load a disk Iris world via reconciler | -| `/iris unload ` | Evacuate → unload → close generator | -| `/iris remove [delete=true]` | Unregister / delete managed world | -| `/iris evacuate ` | Move players out of the Iris world | -| `/iris tp [player=]` | Teleport to world spawn | -| `/iris worlds` | List Iris vs non-Iris loaded worlds | +## Pack snapshot vs studio -Full permission table: `04 - Commands & Permissions.md`. - -### Create parameters - -| Param | Default | Notes | -|-------|---------|-------| -| `name` | required | Normally becomes `iris:`; with overwrite it may also be the exact configured main, `_nether`, or `_the_end` alias | -| `type` | `default` | Pack/dimension selector: `default` → `settings.generator.defaultWorldType` (`overworld`); else pack name or `pack:dimensionKey` | -| `seed` | `1337` | World seed; exact vanilla-slot overwrite preserves the existing level's shared authoritative seed instead | -| `main` | `false` | Schedule main-world promotion on JVM shutdown (Paper path) or promote during Folia staging | -| `overwrite` (`force`) | `false` | Stage a validated exact-slot replacement for the next restart; never deletes a loaded world live | - -Create refuses the primary Bukkit thread. Startup datapack validation must be ready and the selected source pack must have a loadable validation result before the lifecycle lease, datapack preparation, dimension folder, pack snapshot, registration, or Bukkit/NMS create path is entered; lifecycle domain `WORLD_MUTATION` / kind `WORLD_CREATE` must then be free or create fails busy. - -## Production create flow (non-Folia) - -1. Resolve the managed key and dimension without creating the dimension root. -2. Require startup datapack readiness and a loadable validation result for the dimension's owning pack. -3. Ensure datapacks for the dimension types are installed; queue restart if types not yet loaded. -4. Copy the pack into `/iris/pack` (`StudioSVC.installIntoWorld`) — atomic stage → publish; refuses primary thread. Iris invalidates any prior result for that exact root and validates the final published tree before generator creation; failure rolls the publication back. -5. Build `WorldCreator` with Iris generator (`studio=false`). -6. Create the world through `WorldLifecycleService` / NMS async create (timeout 120s; timeout triggers server restart). -7. Register the world in `bukkit.yml` with generator `Iris` dimension key and seed; update the Multiverse link when present. -8. Run optional creation-time pregen if a `PregenTask` was attached by the creator API. - -## Folia staging - -Runtime world creation is disabled on Folia. `/iris create` instead: - -1. Requires startup datapack readiness and a loadable validation result for the selected pack; refusal leaves no dimension folder or registration. -2. Acquires the `WORLD_CREATE` lease. -3. Installs datapacks if changed. -4. Stages the pack into the managed dimension root via `installIntoWorld`; the final published snapshot must pass exact-root validation before registration. -5. Registers the world in `bukkit.yml` (`BukkitWorldConfiguration.register`). -6. If `main=true`, promotes main-world files immediately under lease (failure rolls back bukkit.yml + deletes staged folder). -7. Instructs the operator to restart; generation/load happens on next startup. - -`WorldLifecycleStaging` holds staged generators/biome providers for the backend that consumes them at load. - -## Exact world-slot replacement - -`overwrite=true` uses lifecycle kind `WORLD_REPLACE` and always stages for a complete Paper-family restart, including Paper, Purpur, Leaf, and Folia; Spigot has no early registry bootstrap and rejects this mode. The exact target dimension folder must already exist; ordinary create remains the path for a new world. It accepts safe `iris:*` keys and only the three exact vanilla slots resolved from the configured level name: `minecraft:overworld`, `minecraft:the_nether`, and `minecraft:the_end`. A vanilla slot requires a matching pack environment (`NORMAL`, `NETHER`, or `THE_END`), and Nether/End replacement requires the server's matching allow setting to be enabled; foreign namespaces, other `minecraft:*` keys, path traversal, links, and special filesystem entries fail closed. `main=true` may accompany overwrite only for the configured main-world name. Minecraft stores one authoritative seed for the existing level, so all three exact vanilla slots preserve that loaded primary-world seed and report when it differs from the command's `seed`; changing the level seed remains the ordinary new-main promotion workflow. - -The transaction copies and validates a fresh frozen pack under a same-filesystem sibling stage, fingerprints it, binds its journal to the canonical level root and logical world name, records the original target and `bukkit.yml` generator/seed, then compare-and-swaps that one configuration entry. Distinct slots can be queued before one restart. Paper bootstrap reconciles each authorized transaction before Iris compiles its aggregate datapack or Minecraft builds registries: it atomically moves the old exact dimension directory to a retained sibling backup and publishes the stage. The filesystem must support atomic replacement for the world directories, journal, and `bukkit.yml`; Iris refuses the operation without falling back to a non-atomic destructive move. Publication retains Paper's per-world `data/paper/metadata.dat`, `data/paper/level_overrides.dat`, and `data/minecraft/world_gen_settings.dat` so the replacement keeps the exact slot metadata and authoritative seed. Old `region`, `entities`, `poi`, and Iris runtime data are never merged; they remain only in the backup while the target starts with the staged pack snapshot. - -The backup is eligible for asynchronous deletion only after `WorldLoad` proves the exact namespaced identity, Iris generator, selected dimension, seed, vanilla-slot environment, and unchanged pack fingerprint; cleanup failure retains its committed journal and retries without rolling back a verified world. A failed runtime check journals rollback and requests another restart, then cold bootstrap restores the retained directory and prior `bukkit.yml` generator/seed before registry or world loading. A crash between any move, configuration write, or journal phase is retried idempotently. Conflicting manual configuration, changed roots or logical names, changed staged bytes, unsafe storage, duplicate/corrupt journals, or irreconcilable transaction state abort the early bootstrap and preserve recoverable artifacts instead of guessing or deleting. - -## Studio create - -Studio uses `IrisCreator.studio(true)`: - -- Startup datapack validation and the selected pack's validation must be loadable before a Studio project/world folder, snapshot, generator, or Bukkit world is created. Missing validation fails closed. -- Does **not** copy the pack into the world folder (except benchmark). -- Engine data folder is the live pack path; hotloader starts after engine setup. -- Biome Buffet prepares a changed focus before opening the chunk generation session. Its exclusive fair-stage admission downgrades directly to the retained chunk permit, so no other transition can enter between the focus hotload and that chunk. -- Studio worlds are transient: unloaded studio worlds are cleaned; `bukkit.yml` studio entries are removed on shutdown cleanup paths. -- Studio open/close uses `StudioSVC` transition queue (see `10 - Studio & VSCode Schemas.md`). -- Ordinary Studio suppresses native structure starts only while its initial FULL entry chunk is loading, then restores them for later preview chunks. A failed open never unloads or closes the generator while that exact asynchronous entry request remains active; another Studio open is rejected, cleanup begins after it settles, or its transient world is queued for deletion at the next clean startup if it remains active for another 120 seconds. - -## Load - -`/iris load` / `/iris import`: - -1. Parses managed key; requires dimension root directory on disk. -2. `BukkitWorldReconciler.loadWorld(bukkit.yml, worldKey)`. -3. Reports success, busy, restart-required, or failure. - -Load does not re-download packs; the world must already have `iris/pack` content and registration data consistent with Iris. Reconciliation checks startup readiness, then lazily validates that world's exact snapshot root before touching `bukkit.yml` or calling a world backend. Results are path-scoped, so separate worlds whose snapshot folders are both named `pack` cannot authorize or reject one another. - -## Unload - -`/iris unload` (player origin, sync): - -1. Requires Iris world; acquires `WORLD_UNLOAD` lease. -2. Marks world maintenance. -3. `IrisToolbelt.evacuateAsync` → `WorldLifecycleService.unloadAsync(world, true)` → `generator.closeAsync()`. -4. Terminal timeout **150 seconds**: if unload has not settled, marks timeout, requests server restart (`ServerConfigurator.restart`), and fails the future. - -`WorldUnloadEvent` stops Iris engine maintenance immediately, but it is not treated as proof that Paper's chunk scheduler has drained. Generator close waits for the raw world-lifecycle backend to confirm a successful unload, and the 26.2 noise pipeline retains one generation lease through terrain generation and worldgen-heightmap priming. - -## Evacuate - -`/iris evacuate` moves all players out of the Iris world into another loaded world (or kicks if none). Used as a step inside unload and removal. - -## Remove - -`/iris remove [delete=true]` delegates to `IrisWorldRemovalService`: - -| Status | Meaning | -|--------|---------| -| `UNREGISTERED` | Unloaded/unregistered; files kept (`delete=false`) | -| `DELETED` | Files deleted | -| `DELETE_QUEUED` | Quarantined for delete at next startup | -| `BUSY` | Another world/pack mutation holds the coordinator | -| `INVALID_IDENTIFIER` / `PROTECTED_WORLD` / `NOT_IRIS_WORLD` / `UNSAFE_PATH` / `NOT_FOUND` | Refused | -| Other failure statuses | Partial registry change without delete; quarantine path may remain | - -Only safe `iris` namespace dimension paths are mutable. Phase timeouts use 120s and can request restart on stuck phases. - -With `delete=true`, Iris records the exact quarantine name in the durable startup queue before moving the world directory. Immediate cleanup and startup retry both snapshot every directory's direct children before deleting them, reject symbolic links and special filesystem entries, and retain the queue entry with the full error when a concurrent writer or filesystem failure leaves content behind. - -## Main world promotion - -When create sets `main=true` (non-Folia), a shutdown hook rewrites `server.properties` `level-name` / `level-seed` and publishes files: - -1. Stage temp directory under world container. -2. Copy shared `data`, `datapacks`, `players` from current level root. -3. Copy Iris dimension tree into staged overworld dimension path. -4. Atomic move stage → new level root; write `server.properties`. - -Promotion requires absent target level folder and refuses symlink world data. Folia with `main=true` performs the same publish during staging instead of deferring to shutdown. - -To replace the currently configured main slot in place, name that exact main world and use `overwrite=true`; this keeps the top-level level root, shared datapacks, player data, and non-target dimensions intact. Ordinary `main=true` without overwrite remains the new-level-root promotion workflow above. - -## Pack snapshot vs studio (lifecycle view) - -| Operation | Pack effect | -|-----------|-------------| -| Production create | Full pack tree installed under world `iris/pack` | -| Studio open | Engine reads live packs root; no world pack install | -| `/iris studio package` | Export only; does not change world | -| `/iris dev update-world` | Replaces world `iris/pack` (unsafe; restart if engine active) | -| Hotload | Studio only; production snapshot stays fixed | +| Operation | Effect on the pack | +|---|---| +| Production create | Full pack tree installed under the world's `iris/pack` and frozen there | +| Studio open | Engine reads the live packs root; nothing is installed into the world | +| `/iris studio package` | Exports an archive; no world is touched | +| `/iris dev update-world` | Replaces a world's `iris/pack`. Unsafe, and restarts the server if an engine still holds that pack | +| Hotload | Studio only. A production snapshot never changes underneath a running world | ## Concurrent lifecycle guards -`LifecycleOperationCoordinator` serializes domains including `WORLD_MUTATION` and `PACK_MUTATION`. Overlapping create/load/unload/remove/replace/pack-publish returns busy to the operator. Ordinary world create refuses if the dimension root already exists or the world is already loaded; exact replacement uses a separately journaled restart transaction and never relaxes removal-path protection. +`LifecycleOperationCoordinator` is a **single global mutex** shared by the `WORLD_MUTATION` and `PACK_MUTATION` domains. It is not one lock per domain: a pack download or publish will make a world create report busy, and vice versa. A third domain, `SERVER_LIFECYCLE`, is reserved and cannot be acquired. + +Thirteen operation kinds run under it: `WORLD_CREATE`, `WORLD_LOAD`, `WORLD_UNLOAD`, `WORLD_REMOVE`, `WORLD_REPLACE`, `WORLD_PROMOTE`, `STUDIO_OPEN`, `STUDIO_CLOSE`, `PACK_CREATE`, `PACK_DOWNLOAD`, `PACK_PUBLISH`, `DATAPACK_COMPILE`, and `SERVER_RESTART`. + +Ordinary create also refuses when the dimension root already exists or the world is already loaded. Exact replacement runs as a separately journaled restart transaction and never relaxes the removal-path protections. diff --git a/docs/07 - Pregeneration.md b/docs/07 - Pregeneration.md index 95fa97845..fc4e0d88e 100644 --- a/docs/07 - Pregeneration.md +++ b/docs/07 - Pregeneration.md @@ -1,154 +1,207 @@ # 07 - Pregeneration -Pregeneration walks a rectangular (by default square) block radius around a center and forces chunk generation so players do not trigger generation on first visit. Bukkit command `/iris pregen` (alias `pregenerate`) drives a single active `PregeneratorJob` backed by `IrisPregenerator` and a `PregeneratorMethod`. Settings under `settings.json` → `pregen` and `world.globalPregenCache` control timeouts, mantle residency, scheduler mode, and optional durable skip-cache. +Pregeneration forces chunks to generate ahead of time so players never wait on terrain generation when they explore. You give it a block radius and a center; Iris walks the square area region by region in a spiral and generates every chunk in it. One pregen job runs at a time per server, driven by `/iris pregen` on Bukkit-family and by the same subcommand tree on mod loaders. -See also: `03 - Configuration.md`, `04 - Commands & Permissions.md`, `02 - Getting Started.md`, `06 - Worlds & Lifecycle.md`, `29 - Client HUD & Protocol.md`, `33 - Performance Tuning.md`. +See also: `02 - Getting Started.md`, `03 - Configuration.md`, `04 - Commands & Permissions.md`, `06 - Worlds & Lifecycle.md`, `29 - Client HUD & Protocol.md`, `33 - Performance Tuning.md`. -## Tutorial: run a bounded pregen safely +## Pregenerate 10,000 blocks around spawn -Prerequisites: a disposable or backed-up world, enough free disk for the requested area, ordinary on-demand chunk generation already proven, and no other active pregen job. The commands use `release_candidate` from `06 - Worlds & Lifecycle.md`; substitute the exact loaded world or dimension id if yours differs. +This is the common production task: generate a large area once, up front, so the server never generates terrain during play. -1. Choose a block radius. The standard smoke uses `352` blocks centered at `0,0`, which covers 2,025 chunks. -2. Start without the desktop GUI on a headless server. +Before you start you need a world whose ordinary chunk generation already works, free disk space for the area, a backup or a world you can afford to lose, and no other pregen job running. - Bukkit-family: +**Radius is in blocks, not chunks and not regions.** A radius of `10000` covers 20,000 blocks across, which is 1,251 chunks per axis and **1,565,001 chunks total**. That is hours of work and tens of gigabytes. Do not type it first. - ```text - /iris pregen start 352 world=release_candidate center=0,0 gui=false serial=false - ``` +### 1. Prove the pipeline with a small run - Fabric / Forge / NeoForge: +```text +/iris pregen start 352 world=myworld center=0,0 gui=false +``` - ```text - /iris pregen start 352 irisworldgen:release_candidate at 0 0 - ``` +That is 2,025 chunks and finishes in a minute or two. Watch it: -3. Run `/iris pregen status`. Confirm the target, 2,025 total chunks, generated count, rate, ETA, and failed count. -4. Run `/iris pregen pause`, wait for progress to stop, then `/iris pregen resume` and confirm it continues. -5. Let the run complete. To test cancellation instead, run `/iris pregen stop` once and wait for in-flight work to close before starting another job. -6. Restart the server and visit chunks near the generated boundary. +```text +/iris pregen status +``` -The workflow passes when status reaches completion without accumulating failures, no job remains active after restart, and the generated boundary loads normally. Change concurrency, scheduler, or cache settings only after this baseline succeeds; compare one change at a time using `33 - Performance Tuning.md`. +You should see the world name, `2,025` total chunks, a rising generated count, a chunks-per-second rate, an ETA, and the method name. **Failed count must stay at zero.** If failures accumulate, stop now — a big run will only produce more of them. -### Recovery +### 2. Run the real thing -| Symptom | Check | Recovery | +```text +/iris pregen start 10000 world=myworld center=0,0 gui=false +``` + +If your spawn is not at `0,0`, stand at spawn and use `center=me` instead. That token only works for a player sender; from console, pass explicit coordinates. + +Drop `gui=false` only if the server has a desktop and you want the visual renderer. + +### 3. Know when it is done + +Poll `/iris pregen status`. The job is finished when: + +- generated equals total (1,565,001 for this run), +- failed is still `0`, +- and `/iris pregen status` reports no active task after it ends. + +The last one is the real signal. While a job exists, status prints progress; once the job closes, status tells you there is no active pregeneration task. That transition is the completion condition — not the percentage, which can sit at 100% while in-flight chunks finish writing. + +Then restart the server cleanly and fly to the edge of the generated area. Chunks inside must load without generating; chunks past the boundary must generate normally. + +### Pausing and stopping + +```text +/iris pregen pause +``` + +`pause` is a **toggle**, and `resume` is just an alias for the same command. Running `/iris pregen resume` on a job that is currently running will pause it. The command echoes the resulting state, so read the reply rather than assuming. + +```text +/iris pregen stop +``` + +Stop lets in-flight chunks finish, then cancels. Wait for it to actually close before starting another job — starting a new one closes the previous instance, which is not the same as it having shut down cleanly. + +Unloading or removing a world also stops a pregen targeting that world. That path blocks for up to 15 seconds waiting for the job to close and throws if it does not. + +### Fabric / Forge / NeoForge + +```text +/iris pregen start 352 irisworldgen:myworld at 0 0 +/iris pregen start 10000 irisworldgen:myworld at 0 0 nocache +``` + +The dimension argument comes after the radius, `at ` after that, and `gui`, `sync`, and `nocache` are order-free literal flags you can combine. Radius accepts `1`–`100000`. Modded pregen shows a boss bar automatically unless the player is running the Iris client mod, which draws its own HUD instead. + +## Recovery + +| Symptom | Check | What to do | |---|---|---| -| Start reports an active job | One job is already server-wide | Inspect `/iris pregen status`; finish it or stop it and wait for closure before retrying | -| Status total is unexpected | Radius is in blocks and center-to-chunk rounding changes bounds | Verify radius and center; use the 352-at-0,0 baseline before larger runs | -| Failed count increases | Chunk load timeout, generation exception, disk failure, or lifecycle interruption | Stop the job, fix the first logged failure, verify ordinary generation, then retry the same small area | -| `serial=true` is rejected | Strict serial generation is unavailable on this Bukkit platform | Use the normal method or run the diagnostic on a Paper-compatible server | -| Desktop GUI does not open | Server is headless or GUI launch is disabled | Use `gui=false` and monitor status, console, boss bar, or client HUD | -| Memory pressure repeatedly pauses progress | Effective mantle/heap cap is being reached | Keep the job stopped while tuning; lower residency/in-flight work before increasing heap-sensitive limits | -| Restart does not skip completed work | Cache wrapper was disabled, Folia routing disabled it, `nocache` was used, or cache files were removed | Treat the rerun as uncached; do not infer corruption from regeneration alone | +| Start reports an active job | There is one pregen job server-wide, not one per world | Check `/iris pregen status`; finish or stop it, and wait for closure before retrying | +| Total chunk count is not what you expected | Bounds are inclusive and round outward to whole chunks, so the area is slightly larger than `radius × 2` | Recompute: chunks per axis is `ceil(radius/16) - floor(-radius/16) + 1` centered on your center chunk | +| Failed count climbing | Chunk load timeout, a generation exception, disk failure, or a lifecycle interruption | Stop, fix the first logged failure, confirm ordinary generation works, then retry the same small area | +| `serial=true` rejected | Strict serial generation needs a Paper-compatible server | Use the normal method, or run the diagnostic on Paper | +| Desktop GUI never opens | The server is headless, or `gui.useServerLaunchedGuis` is off | Use `gui=false` and watch status, console, or the client HUD | +| Progress repeatedly stalls | Heap high-water or mantle plate backpressure is engaging | Stop the job before tuning. Lower resident plates and in-flight work before raising anything heap-sensitive | +| Restart regenerates work you already did | The cache wrapper was off — Folia routing disables it, `nocache` was passed, the world has no engine access, or the files under `iris/pregen` were deleted | Treat the rerun as uncached. Regeneration alone is not evidence of corruption | +| "world may not be fully loaded" warning | A player sender started pregen in a world Iris has no engine access to | Confirm the world is loaded and its engine initialized before trusting the run | ## Commands -| Command | Behavior | -|---------|----------| -| `/iris pregen start [world=] [center=0,0] [gui=true] [serial=false]` | Start job | -| `/iris pregen stop` / `x` | Request stop; finishes in-flight work then cancels | -| `/iris pregen pause` / `resume` | Toggle pause on the active job | -| `/iris pregen status` | Print progress snapshot (chunks, %, speed, ETA, method, failed) | +| Command | What it does | +|---|---| +| `/iris pregen start [world=] [center=0,0] [gui=true] [serial=false]` | Start a job. Closes any previous job instance first | +| `/iris pregen stop` (alias `x`) | Request stop. In-flight chunks finish, then the job cancels asynchronously | +| `/iris pregen pause` (alias `resume`) | Toggle pause. One command, two names — it flips whatever state the job is in | +| `/iris pregen status` | Print a progress snapshot for the active job, or report that none exists | -Only one pregen job instance is active. Starting a new job closes the previous instance. +The command root is `/iris pregen` with alias `/iris pregenerate`. ### `start` parameters -| Param | Default | Notes | -|-------|---------|-------| -| `radius` | required | Blocks from center on X and Z (`radiusX` = `radiusZ`). Must be `> 0`. Reported span is `(radius * 2)` by `(radius * 2)` blocks | -| `world` | contextual | Target world (Iris preferred; non-Iris uses hybrid method without engine cache wrapper when no access) | -| `center` | `0,0` | Block X/Z center; `me` uses player location when supported by director parsing | -| `gui` | `true` | Open desktop pregen GUI when host supports it; headless servers log and continue | -| `serial` | `false` | One chunk at a time via strict serial hybrid method; **requires Paper** (`supportsStrictSerialPregeneration`) | - -If the sender is a player without engine access, Iris warns that the world may not be fully loaded. +| Param | Default | What it controls | +|---|---|---| +| `radius` (`size`) | required | Blocks from center on both X and Z. Must be greater than zero. The chat confirmation reports the span as `radius × 2` blocks, which slightly understates the real area because bounds round outward to whole chunks | +| `world` | contextual | Which world to generate. Falls back to your current world. A non-Iris world runs the hybrid method with no engine, so no engine-backed cache wrapper | +| `center` (`middle`) | `0,0` | Block X/Z the square is centered on. Accepts `me`/`here`/`self` for your position, `look`/`cursor` for your look target, and `player:` — all player-sender only | +| `gui` | `true` | Open the desktop renderer when the host supports it. Headless servers log and carry on | +| `serial` | `false` | Generate one chunk at a time through the strict serial hybrid method. **Requires Paper**; rejected elsewhere. For diagnosing instability, not for throughput | ## Area model -`PregenTask` builds saturating block bounds `center ± radius`, converts to chunk and region ranges, and iterates regions in spiral order with per-region chunk order pulled toward the center. +`PregenTask` builds saturating block bounds at `center ± radius`, converts them to chunk and region ranges, then iterates regions in a spiral from the center, ordering chunks within each region toward the center too. That ordering is why the area around your center becomes playable first. + +Bounds are inclusive on both edges: the minimum block floors to a chunk, the maximum ceils. For radius 352 at `0,0` that gives chunks `-22..22` on each axis — 45 per axis, 2,025 total. | Limit | Value | -|-------|-------| -| Max region span per axis | `117189` regions (~±30M blocks Minecraft world limit) | -| Oversized request | `IllegalArgumentException` at construction (does not hang) | +|---|---| +| Maximum region span per axis | 117,189 regions, which is the ±30,000,000 block Minecraft world limit | +| Oversized or non-positive request | `IllegalArgumentException` at construction, so the command fails immediately instead of hanging | +| Modded radius argument range | 1 to 100,000 | ## Generation methods -| Path | Method | -|------|--------| -| Iris world, parallel | `HybridPregenMethod(world, threadCount)` with concurrency from settings parallelism | +| Situation | Method used | +|---|---| +| Iris world, parallel (default) | `HybridPregenMethod(world, threadCount)` | | Iris world, `serial=true` | `HybridPregenMethod.strictSerial(world)` | -| Non-Iris world | Hybrid without engine | -| Cached wrapper | `CachedPregenMethod` around method when caching enabled and runtime scheduler mode is **not** Folia | +| Non-Iris world | The same hybrid method with a null engine | +| Caching enabled, engine present, scheduler not Folia | `CachedPregenMethod` wrapped around whichever of the above applies | -Other method classes (`AsyncPregenMethod`, `MedievalPregenMethod`, `AsyncOrMedievalPregenMethod`) exist for specialized/API paths; the command path uses hybrid. +`HybridPregenMethod` delegates to `AsyncOrMedievalPregenMethod`, which picks `AsyncPregenMethod` on Paper and `MedievalPregenMethod` elsewhere. Region-at-a-time generation is not supported on this path; it is always chunk by chunk. + +The `threadCount` argument is vestigial — `AsyncPregenMethod` ignores it and recomputes concurrency from the server's worker pool, CPU count, and world-gen thread settings, and `MedievalPregenMethod` takes no thread count at all. Tune concurrency through the settings in `33 - Performance Tuning.md`, not by expecting that parameter to do something. ## Cache -| Setting | Location | Behavior | -|---------|----------|----------| -| Per-job skip cache | World `iris/pregen/` via `GlobalCacheSVC.createDefault` | Records generated chunks/regions so restarts can skip work when wrapper is active | -| `world.globalPregenCache` | `settings.json` | When true, maintains global per-world caches on world init/chunk load; when false, service stays idle after enable | -| Folia | Runtime scheduler resolved as Folia | Cached wrapper **disabled** for pregen | +The cache records which chunks are already generated so a restarted or repeated run can skip them. -Cache write happens on world unload and service disable. Empty cache is used when the service is disabled. +| Piece | Where | Behavior | +|---|---|---| +| Per-world skip cache | `/iris/pregen/` | Created through `GlobalCacheSVC.createDefault`. Records generated chunks and regions. Only consulted when the `CachedPregenMethod` wrapper is active | +| `world.globalPregenCache` | `settings.json`, default `false` | When true, Iris also maintains the cache during ordinary play — creating it at world init and marking chunks on every `ChunkLoadEvent`, so normal exploration counts toward it. When false, the pregen job still gets a real on-disk cache; it just is not fed by ordinary chunk loads | +| Folia | Resolved runtime scheduler is Folia | The cached wrapper is disabled entirely for pregen | +| No engine | Non-Iris world | The wrapper is skipped, since the cache is keyed to the engine's world identity | + +Cache contents are written on world unload, on service disable, when the setting is toggled off, and when the cached method closes or saves. If the Iris service itself is disabled, `createDefault` hands back an empty cache rather than touching disk. + +Modded pregen keeps its cache in the equivalent `/iris/pregen`. ## Mantle and heap caps -Pregen applies mantle backpressure and heap high-water checks so tectonic plates do not exhaust memory. +Pregen generates faster than chunks get saved, so Iris throttles itself against tectonic plate residency and heap use. These are the knobs that decide whether a large run finishes or thrashes. -| Control | Default / rule | -|---------|----------------| -| `pregen.maxResidentTectonicPlates` | Default `96`, minimum effective floor `16` | -| Effective plate cap | `min(baseCap, heightScaledCap, heapBudgetCap)` using world height vs 384 and ~60% of process heap / estimated plate size | -| Backpressure wait | `mantleBackpressureWaitMs` default `25` (clamped 5–1000) | -| Backpressure timeout | `mantleBackpressureTimeoutMs` default `60000` (clamped 5s–600s) | -| Hard cap trigger | Loaded plates `> effectiveCap * 2` forces wait/evict | -| Heap high water | Pause generation while heap used ≥ **92%**; release at **82%** | -| Heap panic | ≥ **96%** requests panic reclaim / GC (throttled) | -| Save interval | `saveIntervalMs` default `30000` (clamped 5s–900s) during pregen loop | +| Control | Default and rule | Why you would change it | +|---|---|---| +| `pregen.maxResidentTectonicPlates` | `96`, floored at `16` | The headline speed/memory tradeoff. Raise it to keep more mantle in RAM and cut re-reads; lower it when the run is pushing the heap | +| Effective plate cap | `max(16, min(baseCap, heightScaledCap, heapBudgetCap))` | Computed, not configured. `heightScaledCap` scales the base cap by `384 / worldHeight`, so tall worlds automatically hold fewer plates; `heapBudgetCap` allows about 60% of max heap against a 48 MB reference plate | +| `mantleBackpressureWaitMs` | `25`, clamped 5–1000 | How long the generator sleeps per backpressure check. Rarely worth changing | +| `mantleBackpressureTimeoutMs` | `60000`, clamped 5s–600s | How long backpressure waits before giving up. On timeout Iris logs and proceeds anyway — it never deadlocks the run | +| Hard cap trigger | Loaded plates greater than `effectiveCap × 2` | Forces a wait-and-evict cycle. Seeing this in logs means the cap is too high for your heap | +| Heap high water | Pause at 92% used, release at 82% | Deliberate hysteresis. Generation stalls at 92% and does not resume until it drops to 82%, so brief spikes do not cause flapping | +| Heap panic | 96% requests a panic reclaim and GC | Throttled to once per 30 seconds. Repeated panic lines mean the heap is undersized for the settings | +| `pregen.saveIntervalMs` | `30000`, clamped 5s–900s | How often the pregen loop flushes. Shorter means less lost work on a crash and more I/O | -Raising `maxResidentTectonicPlates` increases memory headroom for speed; lowering reduces peak RAM. See `33 - Performance Tuning.md`. +Full settings reference: `03 - Configuration.md`. Tuning guidance: `33 - Performance Tuning.md`. ## Other `pregen` settings -| Key | Default | Role | -|-----|---------|------| -| `runtimeSchedulerMode` | `AUTO` | Influences Folia vs paper-like scheduling for pregen cache and related paths | -| `paperLikeBackendMode` | `AUTO` | Paper-like lifecycle backend selection | -| `chunkLoadTimeoutSeconds` | `15` (5–120) | Chunk load timeout during pregen | -| `timeoutWarnIntervalMs` | `500` (≥250) | Warning interval for stalled loads | -| `moddedPregenInFlight` | `0` → auto `max(16, min(48, cpu*2))` | In-flight cap for modded pregen adapters | +| Key | Default | What it controls | +|---|---|---| +| `runtimeSchedulerMode` | `AUTO` | Whether Iris treats the server as Folia or Paper-like. This is what decides if the pregen cache wrapper is available. `AUTO` probes the server; a regionized server always resolves to `FOLIA`, and configuring `FOLIA` on a non-regionized server is forced back to `PAPER_LIKE` | +| `paperLikeBackendMode` | `AUTO` | Which Paper-like lifecycle backend loads chunks. `AUTO` resolves to `TICKET` | +| `chunkLoadTimeoutSeconds` | `15`, clamped 5–120 | How long a single chunk load may take before it counts as a failure. Raise it on slow storage; a rising failed count with a low value here is usually I/O, not corruption. Modded pregen floors this at 120 seconds regardless of the setting | +| `timeoutWarnIntervalMs` | `500`, minimum 250 | How often stalled loads warn. Purely log volume | +| `moddedPregenInFlight` | `0` | In-flight chunk budget for modded pregen. `0` auto-resolves to `max(16, min(48, cpu × 2))`; an explicit value is capped at 512 and floored at 8 | -## Pause / stop / status +## Pause, stop, and status -| Action | Behavior | -|--------|----------| -| Pause | `PregeneratorJob.pauseResume()` flips pause; generator loop spins while paused or heap high-water | -| Stop | `shutdownInstance()` closes pregenerator and interrupts worker asynchronously | -| Status | `progressSnapshot()`: percent, generated, total, chunks/s, ETA, elapsed, method name, paused flag, failed count, world name | +| Action | What happens | +|---|---| +| Pause | `PregeneratorJob.pauseResume()` flips the flag. The generator loop spins while paused, and also while heap high-water is engaged | +| Stop | `shutdownInstance()` closes the pregenerator and interrupts the worker asynchronously, so the command returns before the job is fully closed | +| Status | `progressSnapshot()` returns percent, generated, total chunks, chunks remaining, chunks per second, ETA, elapsed time, method name, paused flag, failed count, world name, and world identity | -Failed chunks are counted separately and shown in status when non-zero. +Failed chunks are counted separately from generated ones and only appear in the status line when the count is above zero. A run can reach 100% with failures — check the failed count, not just the percentage. -## HUD / GUI / protocol +## HUD, GUI, and protocol | Surface | Behavior | -|---------|----------| -| Desktop GUI | `PregenRenderer` when `gui=true` and GUI host available; colors mark existing, generating, network, generated, cleaned, mantle states | -| Boss bar / loader HUD | Create and some pregen attach paths use HUD slot claims for progress (creation pregen and studio progress reporters) | -| Client protocol | `IrisProtocolServer.broadcastPregenProgress` sends progress to connected Iris client sessions | +|---|---| +| Desktop GUI (Bukkit) | `PregenRenderer` opens when `gui=true` and a GUI host is available. It draws the progress text and a pause hint over a chunk map — there is no color legend on screen. Chunks being generated are muted green and network-sourced chunks purple. For an Iris world, finished and pre-existing chunks are painted with the engine's biome colors instead of flat status colors; the flat green and dark-green status colors only appear when there is no engine | +| Boss bar | **`/iris pregen` on Bukkit shows no boss bar.** Only creation-time pregen and studio progress claim a HUD slot. Modded pregen does show a boss bar — green while running, yellow while paused — and skips it entirely for players running the Iris client mod | +| Client HUD | `IrisProtocolServer.broadcastPregenProgress` sends progress every tick to connected Iris client sessions that hold the pregen capability, plus per-region deltas. This is the only path client HUDs are fed on any platform | -Client HUD details: `29 - Client HUD & Protocol.md`. GUI toggles: `settings.gui.useServerLaunchedGuis`, `maximumPregenGuiFPS`. +GUI toggles live at `settings.gui.useServerLaunchedGuis` and `settings.gui.maximumPregenGuiFPS`. Client HUD detail: `29 - Client HUD & Protocol.md`. ## Performance profile -Starting pregen applies `PregenPerformanceProfile` to the engine (or global) before the job runs. Studio `profile` command can also apply the pregen performance profile while measuring pack cost. +Starting a pregen applies `PregenPerformanceProfile` to the engine before the job is constructed. It raises the noise cache to at least 4096 entries and enables the fast cache path, then rebuilds the biome complex if anything actually changed. The studio `profile` command applies the same profile while measuring pack cost, so pregen and profiling numbers are comparable. ## Operator notes -- Radius is in **blocks**, not chunks or regions. -- Re-running pregen over the same area is faster when the chunk cache wrapper is active and cache files under `iris/pregen` remain. -- Unload/remove of a world with active pregen should stop the job for that world identity when lifecycle hooks call shutdown-for-world. -- Serial mode is for diagnosis/stability on Paper, not peak throughput. +- Radius is in **blocks**. Every mistake in this area is someone typing a chunk count. +- Re-running over the same area is fast only when the cache wrapper was active and the files under `iris/pregen` still exist. +- Serial mode is a diagnostic. Use it to reproduce a generation failure deterministically, not to go faster. +- Stop a job before tuning mantle or heap settings. Changing them mid-run makes the before/after meaningless. +- Change one setting at a time against the 352-block baseline before scaling back up. See `33 - Performance Tuning.md`. diff --git a/docs/08 - Localization.md b/docs/08 - Localization.md index 425ea25bf..f3585ad49 100644 --- a/docs/08 - Localization.md +++ b/docs/08 - Localization.md @@ -1,70 +1,77 @@ # 08 - Localization -Iris localizes command, Studio, runtime, HUD, and UI strings through typed Java message catalogs and optional locale overlays. Server locale is selected by `general.language` in `settings.json`. Client keybind labels use Minecraft lang assets under `assets/irisworldgen/lang/`. See also `03 - Configuration.md`, `04 - Commands & Permissions.md`, and `29 - Client HUD & Protocol.md`. +Iris ships its command, Studio, runtime, HUD, and UI text as typed Java message catalogs, with translated overlays for seventeen languages and an operator-editable override file per locale. You pick the server language with `general.language` in `settings.json`; you change individual strings by dropping a partial JSON file into `languages/overrides/`. Client keybind labels are a separate surface and live in the mod jar's Minecraft lang assets. See also `03 - Configuration.md`, `04 - Commands & Permissions.md`, and `29 - Client HUD & Protocol.md`. -## Tutorial: select a locale and verify an override +## Change one message -Prerequisites: write access to the Iris data folder, a backup of `settings.json`, and an operator account that can run `/iris reload`. +Say you want German, but you dislike the wording Iris uses when someone mistypes a subcommand. You need two things: the locale setting, and an override file that redefines exactly that one key. -1. Set `general.language` in `settings.json` to an exact bundled id, for example `de_DE`. -2. Create `/languages/overrides/de_DE.json` with one unmistakable local override: +Prerequisites: write access to the Iris data folder, a backup of `settings.json`, and an account that can run `/iris reload`. + +1. Set `general.language` to `de_DE` in `settings.json` (`plugins/Iris/settings.json` on Bukkit-family, `/settings.json` on a mod loader). +2. Create `/languages/overrides/de_DE.json`. Iris creates the `languages/overrides/` folder itself on the first locale load, so it should already exist. ```json { "locale": "de_DE", "messages": { - "iris.command.unknown": "Lokaler Test: unbekannter Iris-Befehl" + "iris.command.unknown": "Kenn ich nicht. Probier /iris help" } } ``` -3. Run `/iris reload` and confirm the response reports `de_DE` as the active locale. -4. Run `/iris help`, then run `/iris locale-override-test` to exercise the overridden unknown-command key. -5. Confirm the local override appears, other messages come from the bundled German overlay, and any omitted key falls back to canonical English instead of printing a raw identifier. -6. Edit the override text, save it, and confirm the hotload path picks up the change. Remove the test override when verification is complete. +3. Run `/iris reload`. A green `Hotloaded settings and locale de_DE.` means the settings and the locale both applied. A yellow `Settings were reloaded, but locale ... was rejected` means the overlay failed validation and the previous locale is still active — read the errors in the console before going further. +4. Run `/iris` with a subcommand that does not exist, for example `/iris zzz`. -The workflow passes when the selected locale remains active across a clean restart and the partial override wins only for its named key. When authoring a new locale, validate a small command group before translating the full catalog. Server locale files do not change client keybind labels; client assets are a separate surface. +Success looks like your override text appearing verbatim. Everything else in the same session — help output, pregen status, Studio messages — should be in German from the bundled `de_DE` overlay, and any key that neither file defines falls back to the built-in English rather than printing a raw key id. + +Edit the file again and save it. The settings hotload poll calls `IrisLanguage.update()`, which compares the override file's path, modification time, and length against what it last loaded, so an edit is picked up on the next poll with no command and no restart. Delete the test override when you are done. + +If you are authoring a whole new locale, translate one command group first and confirm it loads before you translate the rest. A single bad key rejects the entire file. ### Recovery -| Symptom | Meaning | Recovery | +| Symptom | What actually happened | Fix | |---|---|---| -| Requested locale is rejected | Id is invalid, file id differs, JSON is malformed, or overlay validation failed | Keep the previous locale active, fix the logged validation errors, and reload again | -| Raw message key appears | The key is not in the typed catalog or the calling surface bypassed localization | Verify the catalog key first; adding an arbitrary override key cannot create a new message definition | -| Override is ignored | Wrong data folder, wrong locale filename/id, or unchanged watched file | Confirm `/languages/overrides/.json`, update its contents, then run `/iris reload` explicitly | -| Formatting or placeholders break | Override changed `{name}` tokens or the value type | Match the English key's placeholders and text/lines/plural shape exactly | -| Server text changes but keybind labels do not | Client assets are independent | Update/install the matching `assets/irisworldgen/lang/.json` client resource | +| `Rejected locale setting '...'` in the log | The value does not match `[A-Za-z0-9_-]+`, so it never reached the loader | Correct the string in `settings.json`; the previously active locale keeps running in the meantime | +| `Rejected locale reload for ` | The overlay failed validation. The console then lists up to 12 concrete errors and a count of any it omitted | Fix the listed keys and reload. Nothing partial is applied — the previous locale stays active in full | +| `Locale overlay key is not declared by the message catalog` | You invented a key name. Overrides can only redefine keys that already exist in code | Copy the exact key id from the bundled locale file for your language | +| `Expected [x, y] but found [x]` | Your text dropped or renamed a `{name}` placeholder | Match the English template's placeholder set exactly. Order and surrounding words are free; the set of names is not | +| `Expected 5 lines but found 4` / `Expected plural forms [...]` | A lines key needs the same line count as English, and a plural key needs the same form names | Restore the missing entries | +| Override edits do nothing | Wrong data folder, or the filename does not match the active locale id | The file must be `/languages/overrides/.json`, spelled exactly as `general.language`. Run `/iris reload` to force it | +| Server text is translated but keybind labels are still English | Those labels come from the mod jar's client assets, not from the server locale | See "Client language assets" below | -## English and catalogs +## Where English comes from -Canonical English is code-owned in `core/.../localization` (`IrisMessages` and the surface catalogs it assembles). Iris does not ship an English server translation file. English locale id is `en_US` (`VolmitLocales.ENGLISH`). +Canonical English is owned by code in `core/.../localization`. `IrisMessages` assembles the catalog from every surface class plus the shared Director command keys from VolmLib. There is no English server translation file, and there does not need to be — English is the fallback layer under every locale. -Catalog surfaces: - -| Catalog | Surface | +| Catalog | Covers | |---|---| -| `IrisMessages` | Shared command deny / reload / modded help keys | -| `BukkitCommandMessages`, `BukkitCommandMessagesExtended` | Bukkit `/iris` feedback | -| `DirectorCommandMessages` | Director parameter/help copy (Bukkit command tree) | -| `ModdedCommandMessages`, `ModdedHelpMessages` | Fabric/Forge/NeoForge command and help | -| `RuntimeUiMessages`, `RuntimeProgressMessages`, `BukkitRuntimeMessages` | Pregen, chunk jobs, runtime status | -| `PackDownloadMessages` | Pack download progress | -| `ClientUiMessages` | Client Vision, What overlay, pregen HUD, toasts, create-world gates | -| `BukkitUiMessages`, `DesktopUiMessages` | Bukkit/desktop UI strings | +| `DirectorMessages` (VolmLib) | Shared command framework text: parameter errors, argument prompts, help chrome | +| `IrisMessages` | Permission denials, unknown command, player-only, "not an Iris world", reload results, modded help chrome | +| `BukkitCommandMessages`, `BukkitCommandMessagesExtended` | Feedback from the Bukkit `/iris` command tree | +| `DirectorCommandMessages` | Per-command and per-parameter descriptions shown in `/iris help` | +| `ModdedCommandMessages`, `ModdedHelpMessages` | The Fabric/Forge/NeoForge command tree and its help pages | +| `RuntimeUiMessages`, `RuntimeProgressMessages`, `BukkitRuntimeMessages` | Pregen headers and boss bar titles, chunk job progress, runtime status lines | +| `PackDownloadMessages` | Pack download progress and results | +| `ClientUiMessages` | Client mod strings: Vision map, What overlay, pregen HUD stats, toasts, create-world gates | +| `BukkitUiMessages`, `DesktopUiMessages` | Bukkit inventory UI and desktop pregen window strings | -Resolution entry points: `IrisLanguage.text(...)` (color codes allowed) and `IrisLanguage.plain(...)` (legacy section colors stripped). Argument-free `plain` results are memoized per locale snapshot for hot UI paths. +Code resolves text through `IrisLanguage.text(...)` when color codes should survive, or `IrisLanguage.plain(...)` when they should be stripped. Argument-free `plain` calls are memoized per locale snapshot because HUD code calls them several times per frame; a locale reload publishes a new snapshot and throws the whole memo away. ## Selecting a locale | Setting | Default | Location | |---|---|---| -| `general.language` | `en_US` | `plugins/Iris/settings.json` (plugin) or Iris data-folder `settings.json` (mod) | +| `general.language` | `en_US` | `plugins/Iris/settings.json` (plugin) or `/settings.json` (mod) | -Locale names must match `[A-Za-z0-9_-]+`. Invalid values are rejected and the previous active locale continues. `/iris reload` (and settings hotload) reloads settings and locale; success/failure messages report the requested and active locale ids. +The value must match `[A-Za-z0-9_-]+`. Anything else is rejected outright and the previously active locale keeps running. Both `/iris reload` and the automatic settings hotload re-read the setting and reload the locale; the command reports the requested and the active id so you can tell a successful switch from a silent no-op. + +On a successful load Iris logs `Loaded locale with N fallback entries.` That count is the number of catalog keys each overlay did not define, summed across overlays. A one-key override file therefore produces a very large number. It is informational, not an error. ## Bundled server locales -Complete non-English server bundles ship as jar resources under `/languages/.json`. Bundled locale ids: +Complete translations ship inside the jar as `/languages/.json`. | Locale id | Language | |---|---| @@ -74,7 +81,7 @@ Complete non-English server bundles ship as jar resources under `/languages//languages/overrides/.json`. +Path: `/languages/overrides/.json`, created as a folder on locale load. -Iris creates `languages/overrides/` on locale load. Overrides are optional partial files: omitted keys resolve from the bundled overlay (if any), then from code-owned English. - -Shape: +Overrides are partial by design. Define only the keys you want to change; the rest resolve from the bundled overlay, then from English. ```json { @@ -105,30 +110,30 @@ Shape: } ``` -Rules: - | Rule | Behavior | |---|---| -| Root keys | Only `locale` and `messages` are allowed | -| `locale` | If present, must equal the file's locale id after normalize | -| Values | String (text), string array (lines), or object of plural forms for plural keys | -| Nesting | Objects nest into dotted keys; keys must exist in the message catalog | +| Root keys | Only `locale` and `messages`. Any other root key throws and the reload is rejected | +| `locale` | Optional. If present it must equal the filename's locale id after trimming, otherwise the file is rejected | +| Key existence | Every key must already exist in the catalog. An unknown key is an ERROR that rejects the whole file — you cannot mint new messages from an override | +| Value shapes | A string for text keys, a string array for lines keys, an object of plural forms for plural keys. Using the wrong shape rejects the file | +| Nesting | Nested objects flatten into dotted keys, so `{"iris": {"command": {"unknown": "..."}}}` is the same as `"iris.command.unknown"`. The exception is a plural key, where an object is read as the plural forms | +| Placeholders | The set of `{name}` tokens must match the English template exactly. A lines key must also match the English line count, and each line's placeholder set | | Size | Max 2 MiB | -| Hotload | Override file mtime/size is watched; change triggers locale reload without a full restart when settings hotload runs | +| Hotload | The file's path, mtime, and length are watched. A change reloads the locale on the next settings-hotload poll | -Rejected reloads leave the previous locale active and log up to 12 validation errors. +Validation is all-or-nothing. A rejected reload leaves the previous locale fully intact and logs the first 12 errors plus a count of the remainder. ## Resolution order -For non-`en_US` locales: operator override overlay → bundled `/languages/.json` → English catalog defaults. For `en_US`: override overlay only (no English server bundle). +For a non-`en_US` locale a key resolves as: operator override → bundled `/languages/.json` → English catalog default. For `en_US` the bundled layer is skipped entirely, so it is: operator override → English catalog default. -Template placeholders use `{name}` tokens. Trusted arguments may contain color codes; untrusted arguments strip legacy section codes and rewrite `&`, `<`, `>`. +Templates use `{name}` tokens. Arguments are classified as trusted or untrusted at the call site. Trusted arguments may carry color codes. Untrusted arguments — player names, world names, pack-authored strings, exception text — have legacy section codes stripped and have `&`, `<`, and `>` rewritten to lookalike characters so they cannot inject formatting. -`&` color codes in templates are translated to section-sign codes before send (`0-9a-f`, `k-o`, `r`, `x`). +`&` codes in the template itself are translated to section-sign codes before send, for `0-9`, `a-f`, `k-o`, `r`, and `x`. ## Client language assets -Minecraft client assets live at `assets/irisworldgen/lang/.json` inside the mod jar. `en_us.json` is required and currently holds keybind category and key names only: +The Minecraft client reads its own lang files from `assets/irisworldgen/lang/.json` inside the mod jar. These are not the server catalogs and are not affected by `general.language`. They currently define only the keybind category and the three key names: | Key | English | |---|---| @@ -137,8 +142,10 @@ Minecraft client assets live at `assets/irisworldgen/lang/.json` inside | `key.irisworldgen.open_vision_map` | Open Iris Vision Map | | `key.irisworldgen.toggle_what_overlay` | Toggle Iris What Overlay | -Minecraft codes are derived from server locale ids by replacing `-` with `_` and lowercasing (`ja-JP` → `ja_jp`). Matching translated client assets ship for every non-English bundled locale. Server HUD/Vision/toast strings still resolve through `IrisLanguage` / `ClientUiMessages` on the process that renders them, not through these four Minecraft keys. +`en_us.json` is required; a translated file ships for every bundled locale. The Minecraft code is the server locale id with `-` replaced by `_` and lowercased, so `ja-JP` becomes `ja_jp`. + +Everything else the client draws — HUD stats, Vision map labels, What overlay rows, toasts — resolves through `IrisLanguage` and `ClientUiMessages` on whichever process renders it, not through these four keys. That is why a translated boss bar and an English keybind label can coexist. ## Platforms -Localization runs on Bukkit-family and modded (Fabric/Forge/NeoForge). Client keybind lang assets apply only where the client mod is installed. PlaceholderAPI and Bukkit-only command catalogs do not affect mod command trees; modded uses the modded catalogs. See `30 - Platform Differences.md`. +Localization works the same on Bukkit-family and on Fabric/Forge/NeoForge, from the same catalogs and the same override file. Only the surfaces differ: the modded command tree uses `ModdedCommandMessages`/`ModdedHelpMessages`, the Bukkit tree uses the Bukkit catalogs, and each ignores the other's keys. Keybind lang assets apply only where the client mod is installed. See `30 - Platform Differences.md`. diff --git a/docs/09 - PlaceholderAPI.md b/docs/09 - PlaceholderAPI.md index cae1f5166..d567a0e56 100644 --- a/docs/09 - PlaceholderAPI.md +++ b/docs/09 - PlaceholderAPI.md @@ -1,99 +1,105 @@ # 09 - PlaceholderAPI -Iris registers the `iris` PlaceholderAPI expansion on Bukkit-family servers when PlaceholderAPI is enabled at Iris enable time. It publishes sixteen keys: seven world-family readings for the player and nine global pregeneration keys. This is an operator board contract, not a Java API; plugins that need the same data with more precision use `90 - API - Getting Started.md`, `91 - API - Terrain.md`, and `92 - API - World Events.md`. PlaceholderAPI is not available on Fabric/Forge/NeoForge; related runtime and integration details are in `07 - Pregeneration.md` and `28 - Integrations.md`. +Iris registers a PlaceholderAPI expansion with id `iris` on Bukkit-family servers, publishing sixteen read-only values: one service flag, six terrain readings scoped to a player's position, and nine global pregeneration readings. It exists so scoreboard, chat, and HUD plugins can show Iris state without writing Java. Plugins that need the same data with real types and no string parsing should use `90 - API - Getting Started.md`, `91 - API - Terrain.md`, and `92 - API - World Events.md` instead. There is no PlaceholderAPI on Fabric/Forge/NeoForge; see `07 - Pregeneration.md` and `28 - Integrations.md` for the surrounding runtime. -## Tutorial: verify a placeholder before using it in another plugin +## Put an Iris value on a scoreboard -Prerequisites: Bukkit-family Iris, PlaceholderAPI installed before Iris enables, a full server restart, and a player in a loaded Iris world. +Work outward: prove the placeholder resolves in PlaceholderAPI itself before you touch the plugin that will display it. Half of all "the placeholder is broken" reports are a formatting mistake in the consumer. -1. Confirm registration: `/papi info iris`. The output must list expansion id `iris` and its published paths. -2. Confirm the service: `/papi parse me %iris_available%`. Expect `true` while Iris terrain service is live. -3. Confirm player context: `/papi parse me %iris_world.available%`. Expect `true` while the named player is in an Iris world. -4. Parse a concrete terrain value: `/papi parse me %iris_world.biome-key%`. Expect a load key such as `desert/hot-dunes`, not `---`. -5. While standing in the Iris world, run `/iris pregen start 352 center=0,0 gui=false`, then parse `/papi parse me %iris_pregen.percent%`. Expect a numeric value from `0.00` through `100.00` with no percent sign. -6. Stop with `/iris pregen stop` or let the job finish, then run `/papi parse me %iris_pregen.available%`. Expect `false`; other pregen values return `---` after the snapshot clears. -7. Copy the exact verified placeholder into the scoreboard, chat, or HUD plugin and test that consumer once more. +Prerequisites: Bukkit-family Iris, PlaceholderAPI installed *before* Iris starts, a full server restart, and a player standing in a loaded Iris world. -The workflow passes when registration, player-scoped terrain, and global pregen values each produce their documented value shape. Do not debug formatting in the consuming plugin until direct `/papi parse` succeeds. +1. `/papi info iris` — the expansion must be listed with author `Volmit Software` and version `2.0.0`, along with all sixteen paths. If it is not listed at all, skip to the recovery table; nothing else will work. +2. `/papi parse me %iris_available%` — expect `true`. This only means Iris registered its terrain service, not that you are in an Iris world. +3. `/papi parse me %iris_world.available%` — expect `true`. This is the guard you will use in the board template. +4. `/papi parse me %iris_world.biome-key%` — expect a load key such as `desert/hot-dunes`. A `---` here means Iris has no reading for you yet; see the recovery table. +5. Start a job to test the pregen family: `/iris pregen start 352 center=0,0 gui=false`, then `/papi parse me %iris_pregen.percent%`. Expect a bare number between `0.00` and `100.00` with no percent sign. +6. `/iris pregen stop`, then `/papi parse me %iris_pregen.available%`. Expect `false`, and every other `pregen.*` key to read `---`. +7. Now paste the exact string you verified into the consumer plugin, wrap it in whatever guard that plugin offers, and reload it. + +Success is the board showing the same text `/papi parse` showed. If step 7 disagrees with step 4, the bug is in the consumer's template or refresh interval, not in Iris. ### Recovery -| Symptom | Meaning | Recovery | +| Symptom | What actually happened | Fix | |---|---|---| -| `/papi info iris` has no expansion | PlaceholderAPI was unavailable when Iris scheduled registration | Perform a full restart with both plugins installed; `/papi reload` alone does not trigger Iris registration | -| Placeholder remains literal | Path is unknown or uses a removed pre-2.0 name | Copy an exact path from `/papi info iris` or the full table below | -| World value is `---` | No online player context, player is outside Iris, or terrain service has no reading | Parse as a named online player after entering a loaded Iris world | -| `world.available` is true but biome lags movement | Player view cache is within its one-second TTL | Wait one second or trigger an immediate publish by teleport/world change before diagnosing the consumer | -| Pregen value is `---` | No global job snapshot is active | Start a job and wait for its first event; use `pregen.available` as the guard in board templates | -| Scoreboard adds `%` twice | `pregen.percent` deliberately omits the suffix | Add one literal `%` in the consumer format, not in the placeholder | +| `/papi info iris` reports no such expansion | PlaceholderAPI was not enabled at the moment Iris ran its registration task, one tick after enable | Full server restart with both plugins present. There is no late retry on `PluginEnableEvent`, and `/papi reload` will not make Iris try again | +| The placeholder renders literally as `%iris_...%` | The path is not one Iris publishes. Unknown paths return null to PlaceholderAPI, which leaves the text alone | Copy an exact path from `/papi info iris` or the table below. Pre-2.0 underscore names are gone | +| A `world.*` key reads `---` | No player context (console or offline player), the player has no tracked position yet, the player is not in an Iris world, or the terrain service returned nothing for that column | Parse as a named online player who is standing in a loaded Iris world | +| `world.available` is `true` but the biome lags your movement | The per-player world view has a one-second TTL | Wait a second, or teleport — teleports publish immediately. Do not go looking for a bug in the consumer first | +| A `pregen.*` key reads `---` | No job snapshot is currently published | Guard the board template on `pregen.available` rather than testing the value keys for `---` | +| The board shows `47.5%%` or `47.5` with no sign | `pregen.percent` deliberately returns a bare number | Put the literal `%` in the consumer's format string | ## Registration | Item | Value | |---|---| | Expansion id | `iris` | -| Expansion version | `2.0.0` | -| Author string | `Volmit Software` | +| Version | `2.0.0` | +| Author | `Volmit Software` | | Required plugin | `Iris` | -| Soft-depend | `PlaceholderAPI` in `plugin.yml` | -| `persist()` | `true` — survives `/papi reload` without Iris restart | +| Declared in `plugin.yml` | `softdepend: PlaceholderAPI` | +| `persist()` | `true` — the expansion survives `/papi reload` without restarting Iris | -Iris schedules setup after enable. If PlaceholderAPI is not enabled then, the expansion is not registered and there is no late `PluginEnableEvent` re-attempt. Soft-depend alone does not load PlaceholderAPI. +Iris checks `isPluginEnabled("PlaceholderAPI")` inside a task scheduled just after its own enable, and gives up silently if the answer is no. Registration also installs a listener; if that listener fails to attach, Iris unregisters the expansion again and logs a warning, so you never end up with an expansion publishing stale positions. -List published paths with `/papi info iris`. +Soft-depend only affects load order. It does not install or load PlaceholderAPI. ## Value grammar | Rule | Detail | |---|---| -| Path form | Dot-separated, lowercase, no underscores. Iris lowercases the path before resolve, so `%iris_WORLD.BIOME%` works, but write lowercase | -| Plain text only | No color codes, no unit suffixes, no `%` in values, `.` as decimal separator, no thousands grouping | -| Pack name scrubbing | Section-sign sequences and `%` characters inside pack-authored names are stripped before return | -| Real zero | `0` (or `0.00` for two-decimal numbers), never `---` | +| Path form | Dot-separated, lowercase `a-z`, `0-9`, and `-`. The path is lowercased before lookup, so `%iris_WORLD.BIOME%` resolves, but write it lowercase | +| Plain text only | No color codes, no unit suffixes, no `%` character in any value, `.` as the decimal separator, no thousands separators | +| Pack-name scrubbing | Section-sign sequences and `%` characters inside pack-authored names are removed before the value is returned, so a mischievous biome name cannot inject formatting into a scoreboard | +| Genuine zero | Returned as `0`, or `0.00` for two-decimal values. Never `---` | -Three answers: +Every key answers in one of three ways: -| Answer | When | Board shows | +| Answer | When | What the board shows | |---|---|---| -| A value | Known key with data | The value | -| `---` | Known key with no data right now | `---` | -| Nothing (null to PAPI) | Unknown path | Literal `%iris_...%` | +| The value | Known path, data available | The value | +| `---` | Known path, nothing to report right now | `---` | +| Null | Unknown path | The literal `%iris_...%` | -Unknown paths stay visible on purpose. There is no catch-all blank fallback. +Unknown paths stay visible on purpose. There is no blanket empty-string fallback that would hide a typo. -## Full key table +## Key reference ### World family -| Placeholder | Value | -|---|---| -| `%iris_available%` | `true` when the Iris terrain service is live, `false` otherwise | -| `%iris_world.available%` | `true` when the reading player is in an Iris world and a reading exists | -| `%iris_world.biome%` | Surface biome display name at the player column (example: `Hot Desert Dunes`) | -| `%iris_world.biome-key%` | Surface biome load key (example: `desert/hot-dunes`) | -| `%iris_world.region%` | Region display name at the player column | -| `%iris_world.region-key%` | Region load key | -| `%iris_world.dimension%` | Dimension (pack) load key of the player's world | +Everything except `%iris_available%` needs an online player with a tracked position. -`%iris_available%` does not need a player. Every other `world.*` key needs a tracked online player. Console, offline player, or untracked position: `world.available` is `false` and the rest are `---`. +| Placeholder | What it reports | +|---|---| +| `%iris_available%` | `true` when Iris has registered its terrain service on this server. Works from the console. Says nothing about the player's world | +| `%iris_world.available%` | `true` when the reading player's tracked position is in a world Iris generates. The guard for every other `world.*` key | +| `%iris_world.biome%` | Display name of the surface biome at the player's X/Z column, for example `Hot Desert Dunes` | +| `%iris_world.biome-key%` | Load key of that same biome, for example `desert/hot-dunes`. This is what a pack file is named after | +| `%iris_world.region%` | Display name of the region covering the player's X/Z | +| `%iris_world.region-key%` | Load key of that region | +| `%iris_world.dimension%` | Load key of the dimension the player's world generates from, for example `overworld`. This is the dimension file's key, which is usually but not necessarily the pack folder name | + +From the console, for an offline player, or before a player's first tracked position: `world.available` is `false` and the rest are `---`. ### Pregeneration family -| Placeholder | Value | +One job runs per server, so these are global. Every player and the console see identical values. + +| Placeholder | What it reports | |---|---| -| `%iris_pregen.available%` | `true` while a pregeneration job is running | -| `%iris_pregen.world%` | World name the running job is pregenerating | -| `%iris_pregen.percent%` | Completion `0.00`–`100.00`, no `%` character | -| `%iris_pregen.eta%` | Estimated seconds remaining, whole number | -| `%iris_pregen.eta-text%` | Same estimate as `45s`, `2m 5s`, or `1h 30m` | -| `%iris_pregen.chunks%` | Chunks generated so far | -| `%iris_pregen.total%` | Chunks in the job | -| `%iris_pregen.chunks-per-second%` | Current rate, two decimal places | +| `%iris_pregen.available%` | `true` while a job snapshot is published. Use this as the guard | +| `%iris_pregen.world%` | Name of the world being pregenerated | +| `%iris_pregen.percent%` | Completion from `0.00` to `100.00`, two decimals, no `%` character | +| `%iris_pregen.eta%` | Whole seconds remaining | +| `%iris_pregen.eta-text%` | The same estimate formatted for humans: `45s` under a minute, `2m 5s` under an hour, `1h 30m` above it | +| `%iris_pregen.chunks%` | Chunks finished so far | +| `%iris_pregen.total%` | Chunks the job will generate in total | +| `%iris_pregen.chunks-per-second%` | Current generation rate, two decimals | | `%iris_pregen.paused%` | `true` while the job is paused | -`pregen.*` is global (one job per server). Values match for every player and the console. Snapshot is published on pregen events (`STARTED`, `TICK`, `PAUSED`, `RESUMED`, `SAVING`) and cleared on `COMPLETED` or `CANCELLED`. After clear, `pregen.available` is `false` and other `pregen.*` keys are `---`. Before enough chunks exist for an ETA, `eta`/`eta-text` read `0` / `0s`. +The snapshot is republished on the `STARTED`, `TICK`, `PAUSED`, `RESUMED`, and `SAVING` pregen phases, and cleared on `COMPLETED` and `CANCELLED`. After the clear, `pregen.available` is `false` and every value key is `---`. Before the job has run long enough to estimate, `eta` reads `0` and `eta-text` reads `0s`. -### Paths as reported by `/papi info iris` +### Paths as `/papi info iris` prints them ``` available @@ -116,65 +122,64 @@ world.region-key Prefix each with `%iris_` and suffix with `%`. -## Surface readings and cache +## What "surface" means, and what a board costs -`world.biome`, `world.biome-key`, `world.region`, and `world.region-key` are **surface** column readings: the biome/region the generator places at ground level for that X/Z. A player in a cave under an overhang still reads the surface biome above, not the cave biome. +`world.biome`, `world.biome-key`, `world.region`, and `world.region-key` are **surface column** readings: whatever the generator places at ground level for that X/Z. Y is not part of the query. A player 60 blocks down in a cave still reads the surface biome overhead, not the cave biome. If you need the biome at an actual Y, that is a terrain API call, not a placeholder — see `91 - API - Terrain.md`. -### Position tracking +### When a position is published -| Event | Publish | +| Event | Timing | |---|---| -| Walking (`PlayerMoveEvent`) | At most once per second per player; skipped while the player stays in the same block column | -| Join, respawn, world change, portal, any teleport (including `/iris goto`, `/tp`, ender pearl, random TP) | Immediate | +| Walking (`PlayerMoveEvent`) | At most once per second, and skipped entirely while the player stays inside the same block column | +| Join, respawn, world change, portal, and every teleport — `/iris goto`, `/tp`, ender pearls, random-TP plugins | Immediately, bypassing the one-second interval | +| Quit | The player's tracked position and cached view are released | -Standing still never keeps a stale column from a previous place after an immediate publish. Quit releases the player's position and world view. +Because teleports publish immediately, a player who arrives somewhere and stands still never reads a stale column from where they came from. -### View rebuild TTL +### View rebuild cost -World views rebuild at most **once per second per player** (`VIEW_TTL_MS = 1000`), and only when something reads a `world.*` key that needs the view. Consequences: +A player's world view is rebuilt at most once per second (`VIEW_TTL_MS = 1000`), and only when something actually reads a `world.*` key. Three consequences worth knowing before you design a board: -- A board full of `world.*` keys costs one rebuild per player per second -- Values can lag a sprinting player by up to one second -- An unread board costs no terrain queries +- A board with six `world.*` keys costs one rebuild per player per second, not six. +- Values can trail a sprinting player by up to a second. +- A board nobody is reading costs nothing. Iris does not poll terrain in the background for this. -### Pregen snapshot - -Pregen values come from a single global snapshot updated by `IrisPregenerationEvent`, not per-player polling. +Pregen values are not polled per player either — they come from one global snapshot updated by `IrisPregenerationEvent`. ## Permissions -Iris never gates a placeholder on a permission. Values that should not be public (for example world seed) are not published. +No placeholder is permission-gated. Anything sensitive is simply not published: there is no seed key, no file path key, and no key that mutates engine state on read. ## Failure policy -| Situation | Shown | +| Situation | Result | |---|---| -| Unknown path | Nothing (literal `%iris_...%`) | +| Unknown path | Null to PlaceholderAPI, so the literal `%iris_...%` stays on screen | | Known path, no data | `---` | -| No player context on `world.*` | `---` and `world.available` = `false` | -| Player not in an Iris world | `---` and `world.available` = `false` | -| Terrain service not registered | `---` / `world.available` = `false` / `%iris_available%` = `false` | -| No pregen job | `---` / `pregen.available` = `false` | -| Resolver throws | `---`; one warning per distinct path, max 64 distinct paths | +| No player context on a `world.*` key | `---`, and `world.available` is `false` | +| Player outside an Iris world | `---`, and `world.available` is `false` | +| Terrain service not registered | `---`, `world.available` is `false`, `%iris_available%` is `false` | +| No pregen job | `---`, and `pregen.available` is `false` | +| A resolver throws | `---`, plus one logged warning for that path. Logging stops after 64 distinct paths have warned | -Failed keys are not quarantined; they keep answering `---`. +A key that threw is not quarantined. It keeps being called and keeps answering `---` until whatever was wrong resolves itself. ## Migration from pre-2.0 keys -Pre-2.0 underscore keys are gone. No alias and no dual-accept window. Old keys render literally. +The old underscore keys are gone with no aliases and no dual-accept window. They now render literally, which is deliberate — a silently empty scoreboard line is worse than a visibly broken one. -| Old key | New key | Notes | +| Old key | New key | Why | |---|---|---| | `%iris_biome_name%` | `%iris_world.biome%` | Dot grammar | -| `%iris_biome_id%` | `%iris_world.biome-key%` | `id` was always the load key | +| `%iris_biome_id%` | `%iris_world.biome-key%` | `id` was always the load key; the name now says so | | `%iris_region_name%` | `%iris_world.region%` | Dot grammar | -| `%iris_region_id%` | `%iris_world.region-key%` | `id` was always the load key | -| `%iris_biome_file%` | removed | Exposed absolute server paths; threw without a backing file | -| `%iris_region_file%` | removed | Same as `biome_file` | -| `%iris_world_seed%` | removed | No permission context on scoreboards; use terrain API `IrisWorldInfo.seed()` when a plugin needs seed | -| `%iris_terrain_height%` | removed | Generated height before objects/edits; disagreed with the block underfoot | -| `%iris_terrain_slope%` | removed | Expensive pack-authoring diagnostic | -| `%iris_world_mode%` | removed | Studio vs production is not a live-board concern | -| `%iris_world_speed%` | removed | Mutated engine rate-window state on read; use `%iris_pregen.chunks-per-second%` for pregen rate | +| `%iris_region_id%` | `%iris_world.region-key%` | Same as `biome_id` | +| `%iris_biome_file%` | removed | Leaked absolute server paths, and threw whenever the biome had no backing file | +| `%iris_region_file%` | removed | Same problem | +| `%iris_world_seed%` | removed | A scoreboard has no permission context. Read `IrisWorldInfo.seed()` from the terrain API if a plugin genuinely needs it | +| `%iris_terrain_height%` | removed | Reported generated height before objects and player edits, so it regularly disagreed with the block under the player's feet | +| `%iris_terrain_slope%` | removed | A pack-authoring diagnostic, far too expensive to run once per player per board refresh | +| `%iris_world_mode%` | removed | Studio versus production is not something a live board needs | +| `%iris_world_speed%` | removed | Mutated engine rate-window state as a side effect of being read. Use `%iris_pregen.chunks-per-second%` | -Behavior change inside the renames: old keys sampled two blocks above the player's feet (cave/overhang Y). New keys are always surface for the column. `%iris_world.dimension%` is new and has no pre-2.0 equivalent. +One behavior change hides inside the renames: the old biome and region keys sampled two blocks above the player's feet, so they picked up cave and overhang biomes. The new keys are always the surface column. `%iris_world.dimension%` has no pre-2.0 equivalent. diff --git a/docs/10 - Studio & VSCode Schemas.md b/docs/10 - Studio & VSCode Schemas.md index 3d467ed05..bc7a833d5 100644 --- a/docs/10 - Studio & VSCode Schemas.md +++ b/docs/10 - Studio & VSCode Schemas.md @@ -1,91 +1,238 @@ # 10 - Studio & VSCode Schemas -Studio is Iris’s live pack-authoring workflow: open a pack as a transient world, edit JSON under `packs//`, and hotload changes without a full server restart. VSCode (or IntelliJ) gets JSON Schema bindings generated from the Java models so field names, enums, and pack resource keys autocomplete against the real loaders. +Studio is the live pack-authoring loop: open a pack as a throwaway world, edit its JSON in an editor that autocompletes against schemas generated from the Java models, save, and watch the running engine rebuild itself. This page walks the loop end to end first, then documents the commands, the hotload rules, and how the schemas are produced. -Related: see `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `02 - Getting Started.md`, `21 - Jigsaw Structures.md`, `25 - Pack Management.md`, `30 - Platform Differences.md`. +Related: see `04 - Commands & Permissions.md`, `05 - Concepts & Pack Layout.md`, `02 - Getting Started.md`, `11 - Dimensions.md`, `21 - Jigsaw Structures.md`, `25 - Pack Management.md`, `30 - Platform Differences.md`. -## Tutorial: use the Studio edit loop +## The edit loop -Prerequisites: a writable packs directory, command permission, a fixed seed, and VSCode/Cursor with JSON Schema support or IntelliJ with an existing project. Keep the server console visible while editing. +Prerequisites: a writable packs directory, operator access on Bukkit or gamemaster access on a mod loader, and VSCode/Cursor (or IntelliJ) on the machine that holds the pack folder. Keep the server console visible — hotload reports success and failure there. -### Bukkit-family starter pack +### Bukkit-family -1. Create the project: `/iris studio create name=tutorial`. -2. Open its transient world: `/iris studio open tutorial seed=1337`. -3. Generate/open the workspace: `/iris studio vscode dimension=tutorial`. -4. Edit `packs/tutorial/biomes/starter.json` and change only its display `name`. -5. Save once and wait for the hotload result before making another change. -6. In newly generated Studio terrain, run `/iris what biome` and confirm the new display name. Existing blocks are not rewritten by hotload. -7. Validate the project: `/iris pack validate pack=tutorial`. -8. Close the transient world: `/iris studio close`. +1. **Create a project.** `/iris studio create name=tutorial` + Writes `packs/tutorial/` with a dimension, region, biome, generator, and a `tutorial.code-workspace`. The command reports the completed project path; creation runs asynchronously and may report that a restart is needed before the pack can be opened. +2. **Open it as a world.** `/iris studio open tutorial seed=1337` + You are teleported into a transient world generated from the live pack folder. A fixed seed matters — you will be comparing the same coordinates across reloads. +3. **Open the editor workspace.** `/iris studio vscode dimension=tutorial` + Generates `.iris/schema/*` if missing and opens the `*.code-workspace`. On a headless server nothing launches; copy the pack folder to your machine and open the workspace file yourself. + *Success condition:* typing `"` inside any object in `biomes/starter.json` offers field names, and hovering a field shows its description, type, and default value. If it does not, the workspace was not opened or the schemas were never written — run `/iris studio update dimension=tutorial`. +4. **Make one change.** Edit `packs/tutorial/biomes/starter.json` and change only its display `name`. Save once. +5. **Wait for the hotload result** in console before saving anything else. A failed hotload leaves the previous runtime active and reports the error; stacking more edits on top makes the first failure hard to find. +6. **Verify in fresh terrain.** Walk into chunks that have never generated and run `/iris what biome`. The new display name appears there. Hotload never rewrites blocks that already exist, so standing still and expecting the world to change is the usual false negative. +7. **Validate.** `/iris pack validate pack=tutorial` — no blocking errors. +8. **Close.** `/iris studio close` -### Fabric / Forge / NeoForge project +### Fabric / Forge / NeoForge -1. Create from the modded default template explicitly: `/iris studio create tutorial example`. -2. Open it: `/iris studio open tutorial 1337`. -3. Generate/open schemas: `/iris studio vscode tutorial`. -4. Trace the active dimension to one referenced biome, change one low-risk display or palette value, and save once. -5. Wait for hotload, enter newly generated terrain, and inspect it with `/iris what biome`. -6. Validate with `/iris pack validate tutorial`, then close with `/iris studio close`. +Same loop, positional arguments, and the modded studio create always copies a template (`example` by default): -The loop passes when the editor binds the generated schema, hotload succeeds, pack validation has no blocking errors, and newly generated chunks show the change. Create a separate production world only after this gate. A rejected runtime-contract change such as dimension height requires closing and reopening Studio; it is not evidence that hotload is broken. +1. `/iris studio create tutorial example` +2. `/iris studio open tutorial 1337` +3. `/iris studio vscode tutorial` +4. Trace the active dimension to one referenced biome, change one display or palette value, save once. +5. Wait for the hotload result, then enter newly generated terrain and check it with `/iris what biome`. +6. `/iris pack validate tutorial`, then `/iris studio close`. -### Recovery +The loop passes when the editor binds the generated schema, hotload succeeds, validation reports no blocking errors, and newly generated chunks show the change. Create a production world only after that gate. + +A rejected height or dimension-type change is not evidence that hotload is broken — those are refused by design. See **Hotload rules** below. + +### When something goes wrong | Symptom | Meaning | Recovery | |---|---|---| | `open` reports startup validation pending, missing, failed, restart-required, or blocking pack errors | Datapacks or the pack graph cannot safely build an engine | Complete the requested restart or run the platform's `pack validate` form, fix the first blocking error, and retry; do not bypass validation | -| Save reports hotload failure | New data/runtime build failed and the previous runtime may remain active | Fix the first console error and save again before making unrelated edits | -| Height, logical height, or dimension type change is rejected | Change violates `IrisDimensionRuntimeContract` | Close Studio and reopen; on modded, restart when regenerated dimension-type datapacks require registry reload | -| Valid change is invisible | Existing chunks are already materialized or the edited resource is unreachable | Move to new chunks and trace the active dimension graph; use focus/buffet modes for isolation | -| Workspace has no autocomplete or stale resource keys | Schemas were not generated/refreshed or the editor did not open the workspace | Run `studio update`, then open the pack's `.code-workspace`; on headless servers open it manually | -| Studio world disappears after restart | Studio worlds are intentionally transient and purged | Reopen the pack; content under `packs//` remains the source of truth | +| Save reports hotload failure | The new data or runtime build failed; the previous runtime may remain active | Fix the first console error and save again before making unrelated edits | +| Height, logical height, or dimension type change is rejected | The edit violates `IrisDimensionRuntimeContract` | Close Studio and reopen; on modded, restart when regenerated dimension-type datapacks require a registry reload | +| A valid change is invisible | The chunks you are standing in are already materialized, or the edited resource is unreachable from the active dimension | Move to new chunks; trace dimension → region → biome to confirm the resource is actually referenced; use `focus`/`focusRegion` or a buffet studio mode to isolate | +| No autocomplete, or resource keys are stale | Schemas were not generated or refreshed, or the editor never opened the workspace | Run `/iris studio update`, then open the pack's `.code-workspace`; on headless servers open it manually | +| The Studio world disappears after a restart | Studio worlds are transient and purged on purpose | Reopen the pack; `packs//` is the source of truth, not the world folder | -## What Studio Is +## What Studio is | Concept | Behavior | |---------|----------| -| Pack workspace | Packs live under the platform data directory folder named `packs` (`StudioSVC.WORKSPACE_NAME`). | -| Studio world | Opened from a pack dimension key; uses a studio chunk generator with live file watching. | -| Hotload | On ordinary Studio worlds only: a low-priority looper polls pack files; when content changes, `EngineHotloader` waits for already-admitted top-level Bukkit chunk stages, reloads the pack data, and rebuilds engine runtime under exclusive generator control. Fair stage admission keeps later chunk stages behind the waiting transition, and Studio close uses the same drain boundary. Biome Buffet resolves its chunk focus and completes any required complex hotload under exclusive admission before that noise stage opens a generation session, then downgrades directly to one ordinary stage permit. | -| Hotload contract | `IrisDimensionRuntimeContract` refuses hotload if dimension type key, min height, total height, or logical height change. Restart the world after those edits. | -| Non-studio worlds | No pack file watcher looper; production worlds keep the pack snapshot installed at create/update time. | +| Pack workspace | Packs live under the platform data directory in the folder named `packs` (`StudioSVC.WORKSPACE_NAME`) | +| Studio world | Opened from a pack dimension key; uses a studio chunk generator bound to the live pack folder with file watching | +| Hotload | Studio worlds only. A low-priority looper polls pack files; on change, `EngineHotloader` waits for already-admitted top-level Bukkit chunk stages, reloads the pack data, and rebuilds the engine runtime under exclusive generator control. Fair stage admission keeps later chunk stages behind the waiting transition, and Studio close uses the same drain boundary. Biome Buffet resolves its chunk focus and completes any required complex hotload under exclusive admission before that noise stage opens a generation session, then downgrades directly to one ordinary stage permit | +| Hotload contract | `IrisDimensionRuntimeContract` refuses hotload if the dimension type key, min height, total height, or logical height change | +| Non-studio worlds | No pack file watcher. Production worlds keep the pack snapshot installed at create or update time | -Studio settings in `settings.json` → `studio` (`IrisSettings.IrisSettingsStudio`): +Studio settings live in `settings.json` under `studio` (`IrisSettings.IrisSettingsStudio`): | Key | Default | Meaning | |-----|---------|---------| -| `openVSCode` | `true` | When true and the JVM is not headless, `open` / `vscode` may launch the desktop opener on the pack’s `*.code-workspace` file. | -| `disableTimeAndWeather` | `true` | Studio world time/weather lock preference. | -| `entitySpawning` | `true` | Whether studio entity spawning is allowed. | -| `autoStartDefaultStudio` | `false` | Auto-open default studio on boot when enabled. | +| `openVSCode` | `true` | When true and the JVM is not headless, `open`/`vscode` may launch the desktop opener on the pack's `*.code-workspace`. Set false on servers where a desktop launch would be pointless or unwanted | +| `entitySpawning` | `true` | Only affects Studio worlds. False stops Iris ambient entity spawning there; production worlds always spawn regardless of this key | +| `disableTimeAndWeather` | `true` | Present in the settings model but not read by any code path today | +| `autoStartDefaultStudio` | `false` | Present in the settings model but not read by any code path today | + +## Hotload rules + +- The watcher runs only when `PlatformChunkGenerator.isStudio()` is true, the world is not closing, and no Jigsaw Studio session is active. Jigsaw Studio deliberately suppresses ordinary pack-file hotload. +- On change: load a new `IrisData` from the same folder, reload the dimension key, check the hotload contract, build a new engine runtime, retire the previous data, refresh the workspace and schemas, reload datapacks when a platform world is bound, and broadcast a client studio-hotload toast on success or failure. +- `hotloadComplex` is a narrower rebuild that reconstructs `IrisComplex` without reopening the pack. +- A failed hotload rolls the runtime back where possible and reports the error. + +Four values are pinned for the life of the world and cannot hotload: the dimension type key (derived from the dimension load key), min height, total height, and logical height. Editing `dimensionHeight`, `logicalHeight`, `environment`, or the dimension file name means closing and reopening Studio. See `11 - Dimensions.md`. ## Commands (Bukkit) -Root: `/iris studio` (aliases `std`, `s`). Implemented by `CommandStudio` + `StudioSVC`. +Root: `/iris studio`, aliases `std` and `s`. Implemented by `CommandStudio` and `StudioSVC`. Keyed arguments; the first column shows the primary subcommand name. | Subcommand | Aliases | What it does | |------------|---------|--------------| -| `open [seed=1337]` | `o` | Close any open studio, open pack as studio world. Blocks unless startup datapack validation is ready and the selected pack has a loadable validation result. | -| `close` | `x` | Close the active studio project/world. | -| `create [name=studio] [template=]` | `+` | Create a new pack under `packs/` only after startup validation is ready. An optional template must itself be validated and loadable; without a template, writes the starter skeleton (see below). | -| `vscode [dimension=default]` | `vsc` | Open the pack’s VSCode workspace (generates it if missing). | -| `update [dimension=default]` | | Rewrite `/.code-workspace` and regenerate `.iris/schema/*` mappings. | -| `version [dimension=default]` | | Print dimension `version` field. | -| `package [dimension=default] [obfuscate=false] [minify=true]` | `pkg` | Compile pack into a distributable archive. | -| `importvanilla [variants=3] [structures=true]` | `importv`, `iv` | Capture vanilla features/structures into the pack (Bukkit NMS). | -| `scoreboard` | `board`, `sidebar`, `sb` | Toggle studio debug scoreboard (player, must be in studio world). | -| `noise [generator=] [seed=12345]` | `nmap` | External noise explorer GUI. | -| `map [world=]` | `render` | External biome/terrain map GUI for an Iris world. | -| `regions [radius=500]` | | Sample region rarity over a chunk spiral (player in Iris world). | -| `loot [fast=false] [add=true]` | | Open a virtual chest with loot tables for the block under the player (studio). | -| `profile [dimension=default]` | | Write a pack performance profile report. | -| `spawn` / `summon` | | Spawn a pack entity definition at the player. | -| `stp` | | Teleport to the active studio world spawn in creative. | -| `objects` / `find-objects` | | Capture nearby chunk object placement report. | +| `open [seed=1337]` | `o` | Closes any open studio and opens the pack as a studio world. Blocked unless startup datapack validation is ready and the selected pack has a loadable validation result | +| `close` | `x` | Closes the active studio project and world | +| `create [name=studio] [template=]` | `+` | Creates a pack under `packs/` after startup validation is ready. With a template, that template must itself validate as loadable and is downloaded first if missing; without one, writes the starter skeleton below | +| `vscode [dimension=default]` | `vsc` | Opens the pack's VSCode workspace, generating it if missing | +| `update [dimension=default]` | | Rewrites `/.code-workspace` and queues regeneration of `.iris/schema/*` | +| `version [dimension=default]` | | Prints the dimension's `version` field | +| `pkg [dimension=default] [obfuscate=false] [minify=true]` | `package` | Compiles the pack into a distributable archive | +| `importvanilla [variants=3] [structures=true]` | `importv`, `iv` | Captures vanilla features and structures into the pack through Bukkit NMS | +| `scoreboard` | `board`, `sidebar`, `sb` | Toggles the studio debug scoreboard; player must be in the studio world | +| `noise [generator=] [seed=12345]` | `nmap` | Opens the external noise explorer GUI | +| `map [world=]` | `render` | Opens the external biome/terrain map GUI for an Iris world | +| `regions [radius=500]` | | Samples region rarity over a chunk spiral; player must be in an Iris world | +| `loot [fast=false] [add=true]` | | Opens a virtual chest showing loot tables for the block under the player | +| `profile [dimension=default]` | | Writes a pack performance profile report | +| `spawn` | `summon` | Spawns a pack entity definition at the player | +| `tpstudio` | `stp` | Teleports to the active studio world spawn in creative | +| `objects` | `find-objects` | Captures a nearby-chunk object placement report | Permissions and the full `/iris` tree: see `04 - Commands & Permissions.md`. +## Commands (Modded) + +`/iris studio` on Fabric, Forge and NeoForge is implemented by `ModdedStudioCommands` with positional arguments. Supported: `create`/`+`, `package`/`pkg`, `version`, `regions`, `open`/`o`, `close`/`x`, `tpstudio`/`stp`, `status`, `vscode`/`vsc`, `update`, `noise`/`nmap`, `map`/`render`. + +`create` with no arguments creates a project named `studio` from the `example` template; `create ` uses the same template; `create