This commit is contained in:
Brian Neumann-Fopiano
2026-07-30 12:31:39 -04:00
parent 0afbfcb7a2
commit 324cf9e095
374 changed files with 22705 additions and 25650 deletions
+29 -11
View File
@@ -27,22 +27,40 @@ body:
placeholder: The code to place a is missing b and c...
validations:
required: false
- type: dropdown
id: platform
attributes:
label: Platform
description: Which server platform is Iris running on? Pick the loader or server software, not the launcher.
options:
- Paper
- Purpur
- Leaf
- Canvas
- Folia
- Spigot / CraftBukkit
- Fabric
- Forge
- NeoForge
- Other (describe under Problem)
validations:
required: true
- type: dropdown
id: mcversion
attributes:
label: Minecraft Version
description: What version of Minecraft is the server on?
description: What version of Minecraft is the server on? Iris 4.x targets 26.2 only; older versions are not supported.
options:
- 1.14.X
- 1.15.X
- 1.16.X
- 1.17
- 1.17.1
- 1.18
- 1.19
- 1.20
- 1.21
- 1.22
- '26.2'
- Other (unsupported)
validations:
required: true
- type: input
id: loaderversion
attributes:
label: Platform / Loader Version
description: Exact server or loader build (see console). For example "Paper 26.2-60", "Fabric Loader 0.19.3", "NeoForge 26.2.0.12-beta".
placeholder: DO NOT SAY "LATEST"
validations:
required: true
- type: input
+6 -2
View File
@@ -26,9 +26,13 @@ jobs:
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Run verification gates
run: ./gradlew :core:check :adapters:bukkit:plugin:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests
run: ./gradlew :core:check :adapters:bukkit:plugin:test :adapters:bukkit:nms:v26_2_R1:test :spi:build :probe:test :probe:run :probe:deserializationProbe -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Fabric)
run: ./gradlew -p adapters/fabric test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (Forge)
run: ./gradlew -p adapters/forge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Run modded shared tests (NeoForge)
run: ./gradlew -p adapters/neoforge test -PuseLocalVolmLib=false --console=plain --stacktrace
- name: Test modded artifact verifier
run: ./gradlew -p buildSrc test --console=plain --stacktrace
- name: Verify modded artifacts
+16 -6
View File
@@ -20,8 +20,16 @@ TreeGenStuff/
dist/
core/plugins/
adapters/bukkit/plugin/plugins/
adapters/*/run/
adapters/**/logs/*.gz
# Dev-run artifacts. `run/` is a loom/ForgeGradle/ModDevGradle working directory and `logs/` is
# server output; neither is ever source. No source directory in this repo is named run or logs.
run/
logs/
*.log
*.log.gz
crash-reports/
hs_err_pid*.log
replay_pid*.log
.codegraph/
@@ -40,12 +48,14 @@ local.properties
credentials.json
service-account*.json
docs/*
!docs/api/
CROSSPLATFORM_PLAN.md
# docs/ is hand-written and tracked (maintainer checklists + docs/api/). Nothing under it is
# generated, so there is nothing to ignore here.
.qa/
.repro/
# Throwaway worktree copies used by the API / PlaceholderAPI rebuild lanes. Generated, never source.
.apiwt/
.papiwt/
__pycache__/
+39
View File
@@ -192,10 +192,49 @@ Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `build
developer SPI jar (the pure-JVM platform API contract) is built to `spi/build/libs/` by
`./gradlew :spi:jar`.
`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a
consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo;
override with `-Plocation=/path/to/consumers`.
If you need help compiling as a developer or contributor, ask in the Discord. Do not come to the
Discord asking for free copies or a compile tutorial.
## Adapters / modded development
`core/` and `spi/` are pure JVM. `adapters/bukkit/` is part of the root Gradle build; the three
modded adapters (`adapters/fabric`, `adapters/forge`, `adapters/neoforge`) are standalone builds
with their own `settings.gradle`, which is what keeps Loom, ForgeGradle, and ModDevGradle off one
plugin classpath. Drive them with `-p`:
```
./gradlew -p adapters/fabric runServer # or runClient
./gradlew -p adapters/forge runServer
./gradlew -p adapters/neoforge runServer
./gradlew -p adapters/fabric test # shared adapters/modded-common test suite
```
Each `runServer` accepts determinism and world-integrity flags, forwarded to the game as system
properties:
| Flag | System property | Purpose |
|---|---|---|
| `-PirisParity=<pack>` | `iris.parity` | Run the cross-platform parity harness for a pack |
| `-PirisParityGolden=<file>` | `iris.parity.golden` | Compare against a captured golden-hash file |
| `-PirisParityDeep=true` | `iris.parity.deep` | Deep (per-block) parity instead of hash-only |
| `-PirisWorldCheck=<world>` | `iris.worldcheck` | Post-generation world integrity check |
Fabric additionally takes `-PirisClientRunDir=<dir>` to relocate the `runClient` working directory.
Shared code lives in `adapters/minecraft-common` (all adapters), `adapters/modded-common`
(loaders + the shared test suite), and `adapters/client-common` (client HUD and world-type
screens); every adapter adds those source directories, so one edit reaches all three loaders.
For IDE import you can surface the three adapter builds in the root composite with
`-PincludeModdedAdapters=true`. It is off by default: each adapter includes the root build back to
substitute `art.arcane:core` and `art.arcane:spi`, so including them from the root closes a
composite cycle. The build and release paths do not need it.
## Maintainer docs
- [Minecraft version bump checklist](docs/mc-version-bump.md)
- [Release checklist](docs/release-checklist.md)
- [Release readiness checklist](docs/release-readiness-checklist.md)
@@ -106,11 +106,26 @@ public class CustomBiomeSource extends BiomeSource {
return o;
}
return invokeFor(type, source);
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
private static Object invokeFor(Class<?> returns, Object in) {
@@ -120,8 +135,9 @@ public class CustomBiomeSource extends BiomeSource {
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
@@ -137,8 +153,9 @@ public class CustomBiomeSource extends BiomeSource {
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
@@ -241,7 +258,13 @@ public class CustomBiomeSource extends BiomeSource {
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
@Override
@@ -15,6 +15,11 @@ import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureSurfaceFitter;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.reflect.WrappedField;
@@ -184,7 +189,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step()));
if (!decision.generate()) {
continue;
}
@@ -290,7 +295,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(engine,
@@ -304,7 +309,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
continue;
}
try {
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
@@ -317,7 +322,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()));
access.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
@@ -448,8 +453,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
@@ -461,7 +466,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(engine,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
@@ -474,17 +479,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
resolvedPlacements.add(new NativePlacement(start, decision));
heightmapStarts.add(start);
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget(
structureId, start,
NativeStructurePostProcessor.resolveNativeTerrain(
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructurePostProcessor
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
@@ -507,7 +512,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> engine.getHeight(x, z, true) + engine.getMinHeight());
} catch (Throwable error) {
@@ -516,7 +521,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
@@ -741,27 +746,30 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
static {
Field biomeSource = null;
List<Field> biomeSources = new ArrayList<>(1);
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
if (!field.getType().equals(BiomeSource.class))
continue;
biomeSource = field;
break;
biomeSources.add(field);
}
if (biomeSource == null)
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
if (biomeSources.size() != 1)
throw new IllegalStateException("Expected exactly one BiomeSource field in ChunkGenerator, found "
+ biomeSources.size() + " " + biomeSources.stream().map(Field::getName).toList());
Field biomeSource = biomeSources.getFirst();
Method setHeight = null;
List<Method> setHeights = new ArrayList<>(1);
for (Method method : Heightmap.class.getDeclaredMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
if (!method.getName().equals("setHeight")
|| !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|| !method.getReturnType().equals(void.class))
continue;
setHeight = method;
break;
setHeights.add(method);
}
if (setHeight == null)
throw new RuntimeException("Could not find setHeight method in Heightmap!");
if (setHeights.size() != 1)
throw new IllegalStateException("Expected exactly one Heightmap.setHeight(int,int,int) method, found "
+ setHeights.size());
Method setHeight = setHeights.getFirst();
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
@@ -171,7 +171,14 @@ public class NMSBinding implements INMSBinding {
return o;
}
return invokeFor(type, source);
o = invokeFor(type, source);
if (o != null) {
return o;
}
throw new IllegalStateException("Iris cannot resolve a " + type.getName()
+ " from " + source.getClass().getName() + " on this server version");
}
private static Object invokeFor(Class<?> returns, Object in) {
@@ -181,8 +188,9 @@ public class NMSBinding implements INMSBinding {
try {
IrisLogging.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
return i.invoke(in);
} catch (Throwable e) {
e.printStackTrace();
} catch (ReflectiveOperationException | RuntimeException e) {
throw new IllegalStateException("Iris failed to invoke " + in.getClass().getName() + "."
+ i.getName() + "() for " + returns.getName(), e);
}
}
}
@@ -191,7 +199,15 @@ public class NMSBinding implements INMSBinding {
}
private static Object fieldFor(Class<?> returns, Object in) {
return fieldForClass(returns, in.getClass(), in);
for (Class<?> sourceType = in.getClass(); sourceType != null; sourceType = sourceType.getSuperclass()) {
Object o = fieldForClass(returns, sourceType, in);
if (o != null) {
return o;
}
}
return null;
}
@SuppressWarnings("unchecked")
@@ -202,8 +218,9 @@ public class NMSBinding implements INMSBinding {
try {
IrisLogging.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
return (T) i.get(in);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalAccessException | RuntimeException e) {
throw new IllegalStateException("Iris failed to read " + sourceType.getName() + "."
+ i.getName() + " for " + returnType.getName(), e);
}
}
}
@@ -366,11 +383,18 @@ public class NMSBinding implements INMSBinding {
}
private RegistryAccess registry() {
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
RegistryAccess access = registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
if (access == null) {
throw new IllegalStateException("Iris cannot resolve the Minecraft registry access on this server version");
}
return access;
}
private Registry<net.minecraft.world.level.biome.Biome> getCustomBiomeRegistry() {
return registry().lookup(Registries.BIOME).orElse(null);
return registry().lookup(Registries.BIOME).orElseThrow(() -> new IllegalStateException(
"Iris cannot resolve the Minecraft biome registry on this server version"));
}
private Registry<Block> getBlockRegistry() {
@@ -435,7 +459,26 @@ public class NMSBinding implements INMSBinding {
@Override
public String getKeyForBiomeBase(Object biomeBase) {
return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something
net.minecraft.world.level.biome.Biome biome;
if (biomeBase instanceof Holder<?> holder) {
Object value = holder.value();
if (!(value instanceof net.minecraft.world.level.biome.Biome held)) {
throw new IllegalArgumentException("Iris cannot read a biome key from holder value "
+ (value == null ? "null" : value.getClass().getName()));
}
biome = held;
} else if (biomeBase instanceof net.minecraft.world.level.biome.Biome direct) {
biome = direct;
} else {
throw new IllegalArgumentException("Iris cannot read a biome key from "
+ (biomeBase == null ? "null" : biomeBase.getClass().getName()));
}
Identifier key = getCustomBiomeRegistry().getKey(biome);
if (key == null) {
throw new IllegalStateException("Iris found no registry key for biome " + biome);
}
return key.getPath(); // something, not something:something
}
@Override
@@ -805,7 +848,7 @@ public class NMSBinding implements INMSBinding {
@Override
public MCAPaletteAccess createPalette() {
MCAIdMapper<BlockState> registry = registryCache.aquireNasty(() -> {
MCAIdMapper<BlockState> registry = registryCache.aquireNastyPrint(() -> {
Field cf = IdMapper.class.getDeclaredField("tToId");
Field df = IdMapper.class.getDeclaredField("idToT");
Field bf = IdMapper.class.getDeclaredField("nextId");
@@ -818,7 +861,13 @@ public class NMSBinding implements INMSBinding {
List<BlockState> d = (List<BlockState>) df.get(blockData);
return new MCAIdMapper<BlockState>(c, d, b);
});
MCAPalette<BlockState> global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
if (registry == null) {
throw new IllegalStateException("Iris cannot mirror the Minecraft block state id map on this server version");
}
MCAPalette<BlockState> global = globalCache.aquireNastyPrint(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
if (global == null) {
throw new IllegalStateException("Iris cannot build the global block state palette on this server version");
}
java.util.Map<CompoundTag, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
@@ -91,6 +91,27 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(source.contains("engine.acquireGenerationLease(\"bukkit_nms_worldgen_heightmaps\")"));
}
@Test
public void worldgenHeightmapPrimingLivesInTheSharedNativegenSources() throws IOException {
Path nativegen = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")).getParent();
Path shared = nativegen.resolve("WorldgenTerrainHeightmaps.java");
assertTrue("Worldgen heightmap priming must be shared with the modded loaders through "
+ nativegen, Files.isRegularFile(shared));
String heightmaps = Files.readString(shared);
assertTrue(heightmaps.contains("package art.arcane.iris.nativegen;"));
assertTrue(heightmaps.contains("public static void primeTerrain("));
assertTrue(heightmaps.contains("public static void primeStructurePlacement("));
assertFalse(heightmaps.contains("org.bukkit"));
assertFalse(heightmaps.contains("craftbukkit"));
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
assertTrue(source.contains("import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;"));
}
private static int occurrences(String source, String needle) {
int count = 0;
int index = source.indexOf(needle);
@@ -79,23 +79,32 @@ public class IrisChunkGeneratorMonumentLocateContractTest {
@Test
public void stiltSupportUsesPlacedSolidOccupancyWithoutSnapshotDifferenceRequirement() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
Path processor = Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"));
String source = Files.readString(processor);
String foundation = Files.readString(
processor.resolveSibling("NativeStructureFoundationBuilder.java"));
int placement = source.indexOf("start.placeInChunk(world, structureManager, generator");
int stiltPlacement = source.indexOf("placeStilts(world, area, structureId, start", placement);
int occupancyCheck = source.indexOf("if (state.isSolid())", stiltPlacement);
int terrainFloor = source.indexOf("Math.max(terrainY,", stiltPlacement);
int stiltDefinition = foundation.indexOf("static void placeStilts(");
int occupancyCheck = foundation.indexOf("if (state.isSolid())", stiltDefinition);
int terrainFloor = foundation.indexOf("Math.max(terrainY,", stiltDefinition);
assertTrue(placement >= 0);
assertTrue(stiltPlacement > placement);
assertTrue(occupancyCheck > stiltPlacement);
assertTrue(terrainFloor > stiltPlacement);
assertTrue(stiltDefinition >= 0);
assertTrue(occupancyCheck > stiltDefinition);
assertTrue(terrainFloor > stiltDefinition);
assertFalse(source.contains("state.equals("));
assertFalse(foundation.contains("state.equals("));
assertFalse(source.contains("snapshot.states"));
assertFalse(foundation.contains("snapshot.states"));
}
@Test
public void verticalPlacementMovesPiecesMonumentChildrenJigsawJunctionsAndCachedBoundsTogether() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructurePostProcessorSource")));
String source = Files.readString(
Path.of(System.getProperty("iris.nativeStructurePostProcessorSource"))
.resolveSibling("NativeStructureVerticalPlacer.java"));
int placementStart = source.indexOf("public static int applyVerticalPlacement");
int shiftStart = source.indexOf("public static int applyVerticalShift", placementStart);
int alignmentStart = source.indexOf("static int alignOceanMonumentToSeaLevel", shiftStart);
@@ -1,5 +1,6 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
@@ -95,7 +95,7 @@ public class NativeStructureFactoryTest {
new PiecesContainer(List.of(piece))
);
StructureStart relocated = NativeStructurePostProcessor.relocateToMinY(
StructureStart relocated = NativeStructureVerticalPlacer.relocateToMinY(
start, source, -20, LevelHeightAccessor.create(-64, 384));
assertNotSame(start, relocated);
@@ -61,7 +61,7 @@ public class NativeStructurePostProcessorEncaseTest {
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.DIRT.defaultBlockState());
put(blocks, bounds.maxX(), bounds.minY(), bounds.maxZ(), Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
@@ -86,7 +86,7 @@ public class NativeStructurePostProcessorEncaseTest {
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
Map<BlockPos, BlockState> blocks = new HashMap<>();
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.ENCASE),
null);
@@ -105,7 +105,7 @@ public class NativeStructurePostProcessorEncaseTest {
Map<BlockPos, BlockState> blocks = new HashMap<>();
IrisMaterialPalette palette = new IrisMaterialPalette().qclear().qadd("minecraft:tuff");
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:stronghold", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
@@ -131,7 +131,7 @@ public class NativeStructurePostProcessorEncaseTest {
@Test
public void buryAndEncapsulateAdaptationsAutoDefaultToEncase() {
for (TerrainAdjustment adjustment : List.of(TerrainAdjustment.BURY, TerrainAdjustment.ENCAPSULATE)) {
IrisStructureTerrain resolved = NativeStructurePostProcessor.resolveNativeTerrain(
IrisStructureTerrain resolved = NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null);
assertEquals(IrisStructureTerrainMode.ENCASE, resolved.resolvedMode());
@@ -146,7 +146,7 @@ public class NativeStructurePostProcessorEncaseTest {
public void otherAdaptationsNeverAutoDefaultToEncase() {
for (TerrainAdjustment adjustment : List.of(
TerrainAdjustment.NONE, TerrainAdjustment.BEARD_THIN, TerrainAdjustment.BEARD_BOX)) {
assertNull(NativeStructurePostProcessor.resolveNativeTerrain(
assertNull(NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(adjustment, 64), null));
}
}
@@ -156,7 +156,7 @@ public class NativeStructurePostProcessorEncaseTest {
IrisStructureTerrain configured = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.SOURCE);
assertSame(configured, NativeStructurePostProcessor.resolveNativeTerrain(
assertSame(configured, NativeStructureTerrainIntegrator.resolveNativeTerrain(
start(TerrainAdjustment.BURY, 64), configured));
}
@@ -189,9 +189,9 @@ public class NativeStructurePostProcessorEncaseTest {
StructureStart first = start(TerrainAdjustment.BURY, 64);
StructureStart second = start(TerrainAdjustment.BURY, 64);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
second, -64, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = first.getBoundingBox();
@@ -199,7 +199,7 @@ public class NativeStructurePostProcessorEncaseTest {
assertTrue(bounds.minY() >= -120);
assertTrue(bounds.maxY() <= -20);
int repeated = NativeStructurePostProcessor.applyVerticalShift(
int repeated = NativeStructureVerticalPlacer.applyVerticalShift(
first, -64, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(0, repeated);
}
@@ -209,7 +209,7 @@ public class NativeStructurePostProcessorEncaseTest {
IrisStructureYBand band = new IrisStructureYBand().setMin(-50).setMax(-45);
StructureStart start = start(TerrainAdjustment.BURY, 64);
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, false, band, (x, z) -> 40);
BoundingBox bounds = start.getBoundingBox();
@@ -225,7 +225,7 @@ public class NativeStructurePostProcessorEncaseTest {
BoundingBox bounds = start.getBoundingBox();
int height = bounds.maxY() - bounds.minY();
NativeStructurePostProcessor.applyVerticalShift(
NativeStructureVerticalPlacer.applyVerticalShift(
start, -200, -256, 320, true, false, band, (x, z) -> 40);
assertEquals(-64 - height / 2, start.getBoundingBox().minY());
@@ -237,7 +237,7 @@ public class NativeStructurePostProcessorEncaseTest {
StructureStart start = start(TerrainAdjustment.BURY, 64);
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -256, 320, true, true, band, (x, z) -> 40);
assertEquals(0, offset);
@@ -33,7 +33,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void vanillaSeaLevelKeepsTheVanillaMonumentHeight() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 63, -64, 320, false, false, null, (x, z) -> 0);
assertEquals(0, offset);
@@ -45,11 +45,11 @@ public class NativeStructurePostProcessorMonumentTest {
public void shiftedSeaLevelMovesTheShellAndEveryRoomTogether() {
StructureStart start = monumentStart(1337L);
OceanMonumentPieces.MonumentBuilding building = monumentBuilding(start);
List<StructurePiece> children = NativeStructurePostProcessor.monumentChildPieces(building);
List<StructurePiece> children = NativeStructureVerticalPlacer.monumentChildPieces(building);
int[] childMinY = minimumYs(children);
BoundingBox cachedBounds = start.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
@@ -63,7 +63,7 @@ public class NativeStructurePostProcessorMonumentTest {
assertEquals(childMinY[i] - 13, children.get(i).getBoundingBox().minY());
}
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(0, repeatedOffset);
}
@@ -72,7 +72,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void configuredOffsetIsRelativeToTheActualSeaLevel() {
StructureStart start = monumentStart(1337L);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 3, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-10, offset);
@@ -85,7 +85,7 @@ public class NativeStructurePostProcessorMonumentTest {
long seed = 1337L;
ChunkPos chunkPos = new ChunkPos(0, 0);
StructureStart initial = monumentStart(seed);
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
initial, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
PiecesContainer regenerated = OceanMonumentStructure.regeneratePiecesAfterLoad(
@@ -94,7 +94,7 @@ public class NativeStructurePostProcessorMonumentTest {
monumentStructure(), chunkPos, 0, regenerated);
assertEquals(39, reloaded.getBoundingBox().minY());
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
reloaded, "minecraft:monument", 0, 50, -256, 512, false, false, null, (x, z) -> 0);
assertEquals(-13, offset);
assertEquals(26, reloaded.getBoundingBox().minY());
@@ -105,7 +105,7 @@ public class NativeStructurePostProcessorMonumentTest {
public void impossibleSeaLevelAlignmentFailsInsteadOfClippingTheMonument() {
StructureStart start = monumentStart(1337L);
try {
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:monument", 0, -50, -64, 320, false, false, null, (x, z) -> 0);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("cannot align"));
@@ -45,7 +45,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
BoundingBox cachedBounds = start.getBoundingBox();
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.maxX() && z == footprint.maxZ() ? 64 : 92);
@@ -63,7 +63,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
StructureStart start = jungleStart(piece);
BoundingBox footprint = piece.getBoundingBox();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:jungle_pyramid", 4, 63, -64, 320, false, false, null,
(x, z) -> x == footprint.minX() && z == footprint.minZ() ? 88 : 70);
@@ -78,9 +78,9 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(23L), 0, 0);
StructureStart start = desertStart(piece);
int initialOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int initialOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
int repeatedOffset = NativeStructurePostProcessor.applyVerticalPlacement(
int repeatedOffset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 80);
assertEquals(17, initialOffset);
@@ -93,7 +93,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
TestDesertPyramidPiece piece = new TestDesertPyramidPiece(RandomSource.create(29L), 0, 0);
StructureStart start = desertStart(piece);
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
start, "minecraft:desert_pyramid", 0, 63, -64, 320, false, false, null, (x, z) -> 318);
assertEquals(241, offset);
@@ -110,12 +110,12 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
StructureStart jungleStart = jungleStart(jungle);
StructureStart swampStart = swampStart(swamp);
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructurePostProcessor.requiresSurfaceTerrain(swampStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(desertStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(jungleStart));
assertFalse(NativeStructureSurfaceFitter.requiresSurfaceTerrain(swampStart));
AtomicInteger terrainQueries = new AtomicInteger();
int offset = NativeStructurePostProcessor.applyVerticalPlacement(
int offset = NativeStructureVerticalPlacer.applyVerticalPlacement(
swampStart, "minecraft:swamp_hut", 0, 63, -64, 320, false, false, null,
(x, z) -> terrainQueries.incrementAndGet());
assertEquals(0, offset);
@@ -125,7 +125,7 @@ public class NativeStructurePostProcessorScatteredFeatureTest {
@Test
public void scatteredHeightFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveScatteredHeightPositionField();
Field field = NativeStructureReflection.resolveScatteredHeightPositionField();
assertEquals(ScatteredFeaturePiece.class, field.getDeclaringClass());
assertEquals(int.class, field.getType());
@@ -61,13 +61,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void onlySurfaceBeardThinStructuresPrepareTerrain() {
assertTrue(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertTrue(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_THIN, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BURY, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.BEARD_BOX, GenerationStep.Decoration.SURFACE_STRUCTURES));
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
TerrainAdjustment.ENCAPSULATE, GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@@ -78,86 +78,86 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
TerrainAdjustment.BURY,
TerrainAdjustment.BEARD_BOX,
TerrainAdjustment.ENCAPSULATE)) {
assertFalse(NativeStructurePostProcessor.shouldPrepareSurfaceTerrain(
assertFalse(NativeStructureSurfaceFitter.shouldPrepareSurfaceTerrain(
adjustment, GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
}
}
@Test
public void surfaceAnchorIsFlushInsideAndUnchangedAtRadius() {
NativeStructurePostProcessor.SurfaceAnchor anchor = anchor(80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor anchor = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 2, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 16, 2, 64));
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(anchor), 17, 2, 64));
}
@Test
public void surfaceAnchorRaisesAndLowersThroughTheTaper() {
NativeStructurePostProcessor.SurfaceAnchor raised = anchor(80, 2);
NativeStructurePostProcessor.SurfaceAnchor lowered = anchor(64, 2);
NativeStructureSurfaceFitter.SurfaceAnchor raised = anchor(80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor lowered = anchor(64, 2);
assertEquals(68, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(68, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(raised), 10, 2, 64));
assertEquals(76, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(76, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(lowered), 10, 2, 80));
}
@Test
public void containingRigidFloorsHaveDeterministicPriority() {
NativeStructurePostProcessor.SurfaceAnchor rigid = anchor(70, 2);
NativeStructurePostProcessor.SurfaceAnchor junction = anchor(90, 1);
NativeStructureSurfaceFitter.SurfaceAnchor rigid = anchor(70, 2);
NativeStructureSurfaceFitter.SurfaceAnchor junction = anchor(90, 1);
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(rigid, junction), 2, 2, 64));
assertEquals(70, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(70, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(junction, rigid), 2, 2, 64));
NativeStructurePostProcessor.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructurePostProcessor.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
NativeStructureSurfaceFitter.SurfaceAnchor weakTie = anchor(48, 1);
NativeStructureSurfaceFitter.SurfaceAnchor strongTie = anchor(80, 2);
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(weakTie, strongTie), 2, 2, 64));
assertEquals(80, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(80, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(strongTie, weakTie), 2, 2, 64));
}
@Test
public void containingFootprintOverridesAnAdjacentPiecesFalloff() {
NativeStructurePostProcessor.SurfaceAnchor local =
new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructurePostProcessor.SurfaceAnchor adjacent =
new NativeStructurePostProcessor.SurfaceAnchor(5, 9, 0, 4, 80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor local =
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, 65, 2);
NativeStructureSurfaceFitter.SurfaceAnchor adjacent =
new NativeStructureSurfaceFitter.SurfaceAnchor(5, 9, 0, 4, 80, 2);
assertEquals(77, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(77, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(local, adjacent), 4, 2, 64));
assertEquals(65, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(65, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(adjacent, local), 4, 2, 64));
}
@Test
public void opposingFalloffsBlendWithoutAnAbruptMidpointSeam() {
NativeStructurePostProcessor.SurfaceAnchor high =
new NativeStructurePostProcessor.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructurePostProcessor.SurfaceAnchor low =
new NativeStructurePostProcessor.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructurePostProcessor.resolveSurfaceTarget(
NativeStructureSurfaceFitter.SurfaceAnchor high =
new NativeStructureSurfaceFitter.SurfaceAnchor(0, 0, 0, 0, 80, 2);
NativeStructureSurfaceFitter.SurfaceAnchor low =
new NativeStructureSurfaceFitter.SurfaceAnchor(12, 12, 0, 0, 48, 2);
int previous = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), 0, 0, 64);
for (int x = 1; x <= 12; x++) {
int forward = NativeStructurePostProcessor.resolveSurfaceTarget(
int forward = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), x, 0, 64);
int reversed = NativeStructurePostProcessor.resolveSurfaceTarget(
int reversed = NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(low, high), x, 0, 64);
assertEquals(forward, reversed);
assertTrue(Math.abs(forward - previous) <= 4);
previous = forward;
}
assertEquals(64, NativeStructurePostProcessor.resolveSurfaceTarget(
assertEquals(64, NativeStructureSurfaceFitter.resolveSurfaceTarget(
List.of(high, low), 6, 0, 64));
}
@@ -170,7 +170,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(lowered, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(lowered, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(lowered), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
@@ -185,7 +185,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(raised, 0, 64, 0, Blocks.GRASS_BLOCK.defaultBlockState());
put(raised, 0, 65, 0, Blocks.DANDELION.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(raised), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
@@ -205,7 +205,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, 66, 0, log);
put(blocks, 0, 68, 0, leaves);
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 68, -64, 319);
@@ -221,7 +221,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, 0, 64, 0, Blocks.GRAVEL.defaultBlockState());
put(blocks, 0, 65, 0, Blocks.WATER.defaultBlockState());
NativeStructurePostProcessor.applySurfaceColumn(
NativeStructureSurfaceFitter.applySurfaceColumn(
world(blocks), new BlockPos.MutableBlockPos(),
0, 0, 64, 62, -64, 319);
@@ -236,7 +236,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -64, 320, true, true, null, (x, z) -> 40);
assertEquals(0, offset);
@@ -248,7 +248,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
int minY = start.getBoundingBox().minY();
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, -8, -64, 320, true, true, null, (x, z) -> 40);
assertEquals(-8, offset);
@@ -262,7 +262,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int maxY = start.getBoundingBox().maxY();
int expected = 40 - 1 - maxY;
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, -64, 320, true, false, null, (x, z) -> 40);
assertEquals(expected, offset);
@@ -275,7 +275,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int minY = start.getBoundingBox().minY();
int worldMinY = minY - 4;
int offset = NativeStructurePostProcessor.applyVerticalShift(
int offset = NativeStructureVerticalPlacer.applyVerticalShift(
start, 0, worldMinY, 320, true, false, null, (x, z) -> worldMinY);
assertEquals(-4, offset);
@@ -289,7 +289,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, bounds.minX(), bounds.minY(), bounds.minZ(), Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), bounds, "minecraft:desert_pyramid", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM), null);
@@ -310,7 +310,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, bounds.maxX(), bounds.maxY() + 1, bounds.maxZ(),
Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:desert_pyramid", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
@@ -345,7 +345,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
put(blocks, firstBounds.minX(), y, z, Blocks.STONE.defaultBlockState());
put(blocks, gapX, y, z, Blocks.STONE.defaultBlockState());
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain().setMode(IrisStructureTerrainMode.FORCE_CARVE), null);
@@ -372,7 +372,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BlockPos(1, 2, 0), Blocks.DEEPSLATE.defaultBlockState(), null)));
Map<Long, int[]> columns = new HashMap<>();
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns(
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
new BoundingBox(0, 0, 0, 1, 2, 0),
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
@@ -392,7 +392,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
new BlockPos(0, 1, 0), Blocks.STRUCTURE_VOID.defaultBlockState(), null)));
Map<Long, int[]> columns = new HashMap<>();
assertTrue(NativeStructurePostProcessor.emitTemplateColumns(
assertTrue(NativeStructureTerrainIntegrator.emitTemplateColumns(
List.of(template), new BlockPos(0, 0, 0), Rotation.NONE,
new BoundingBox(0, 0, 0, 0, 1, 0),
(x, z, minY, maxY) -> columns.put((long) x << 32 | z & 0xffffffffL,
@@ -406,7 +406,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
StructureStart start = desertStart();
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
StructureCarvingFootprint footprint = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint footprint = NativeStructureTerrainIntegrator.carveFootprint(
start, 4, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertEquals(bounds.minX() - 4, footprint.minX());
@@ -423,13 +423,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void carveFootprintIsComputedOncePerStartAndPadding() {
StructureStart start = desertStart();
StructureCarvingFootprint first = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint first = NativeStructureTerrainIntegrator.carveFootprint(
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint repeated = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint repeated = NativeStructureTerrainIntegrator.carveFootprint(
start, 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint widened = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint widened = NativeStructureTerrainIntegrator.carveFootprint(
start, 7, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
StructureCarvingFootprint other = NativeStructurePostProcessor.carveFootprint(
StructureCarvingFootprint other = NativeStructureTerrainIntegrator.carveFootprint(
desertStart(), 6, NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager);
assertSame(first, repeated);
@@ -441,13 +441,13 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void organicCarveNeverCutsBelowTheColumnSupportingFloor() {
StructureStart start = desertStart();
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
NativeStructurePostProcessor.OrganicCarve carve = organicCarve(start, 6);
NativeStructureTerrainIntegrator.OrganicCarve carve = organicCarve(start, 6);
BoundingBox area = new BoundingBox(
bounds.minX() - 6, bounds.minY() - 4, bounds.minZ() - 6,
bounds.maxX() + 6, bounds.maxY() + 12, bounds.maxZ() + 6);
Map<BlockPos, BlockState> blocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area, carve);
NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area, carve);
int centerX = bounds.minX() + bounds.getXSpan() / 2;
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
@@ -471,9 +471,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> uniformBlocks = fill(area);
Map<BlockPos, BlockState> lobedBlocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(uniformBlocks), area, organicCarve(start, 10, 0D));
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(lobedBlocks), area, organicCarve(start, 10, 0.85D));
int uniform = 0;
@@ -521,9 +521,9 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
Map<BlockPos, BlockState> narrowBlocks = fill(narrow);
// Each chunk context rebuilds its own noise channels from the shared start identity.
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(wideBlocks), wide, organicCarve(start, 6));
NativeStructurePostProcessor.carveOrganicColumns(
NativeStructureTerrainIntegrator.carveOrganicColumns(
world(narrowBlocks), narrow, organicCarve(start, 6));
int carved = 0;
@@ -552,7 +552,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
int centerX = bounds.minX() + bounds.getXSpan() / 2;
int centerZ = bounds.minZ() + bounds.getZSpan() / 2;
NativeStructurePostProcessor.integrateTerrain(
NativeStructureTerrainIntegrator.integrateTerrain(
world(blocks), area, "minecraft:ancient_city", start,
new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
@@ -572,21 +572,21 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void sparseStiltGridIsDeterministicAndPreflightsGround() {
assertTrue(NativeStructurePostProcessor.isStiltColumn(0, 0, 4));
assertTrue(NativeStructurePostProcessor.isStiltColumn(-4, 8, 4));
assertFalse(NativeStructurePostProcessor.isStiltColumn(1, 0, 4));
assertTrue(NativeStructurePostProcessor.isStiltColumn(1, 1, 1));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(0, 0, 4));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(-4, 8, 4));
assertFalse(NativeStructureFoundationBuilder.isStiltColumn(1, 0, 4));
assertTrue(NativeStructureFoundationBuilder.isStiltColumn(1, 1, 1));
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, 7, 0, Blocks.DEEPSLATE.defaultBlockState());
put(blocks, 0, 8, 0, Blocks.SCULK_VEIN.defaultBlockState());
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
assertEquals(7, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(7, NativeStructureFoundationBuilder.findStiltAnchorY(
world(blocks), 0, 0, 10, 2, -64, -64, position));
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY(
world(blocks), 0, 0, 10, 1, -64, -64, position));
assertEquals(Integer.MIN_VALUE, NativeStructurePostProcessor.findStiltAnchorY(
assertEquals(Integer.MIN_VALUE, NativeStructureFoundationBuilder.findStiltAnchorY(
world(new HashMap<>()), 0, 0, 10, 64, -64, -64, position));
}
@@ -635,7 +635,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
@Test
public void singlePoolTemplateFieldMatchesTheRuntimeContract() {
Field field = NativeStructurePostProcessor.resolveSinglePoolTemplateField();
Field field = NativeStructureReflection.resolveSinglePoolTemplateField();
assertEquals(SinglePoolElement.class, field.getDeclaringClass());
assertEquals(Either.class, field.getType());
@@ -649,12 +649,12 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void runtimeTemplatesAndLegacyAirUseTheExactContract() {
StructureTemplate runtimeTemplate = new StructureTemplate();
assertEquals(runtimeTemplate, NativeStructurePostProcessor.resolveTemplateReference(
assertEquals(runtimeTemplate, NativeStructureReflection.resolveTemplateReference(
Either.right(runtimeTemplate), null));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructurePostProcessor.shouldClearLegacyAir(96, 80, true));
assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(79, 80, false));
assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(80, 80, false));
assertTrue(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, false));
assertFalse(NativeStructureTerrainIntegrator.shouldClearLegacyAir(96, 80, true));
}
@Test
@@ -687,7 +687,7 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
blocks.put(belowFloorPosition, Blocks.DIRT.defaultBlockState());
blocks.put(outsidePosition, Blocks.DIRT.defaultBlockState());
NativeStructurePostProcessor.clearTemplateAir(
NativeStructureTerrainIntegrator.clearTemplateAir(
world(blocks), template, origin, 80, settings);
assertEquals(Blocks.AIR.defaultBlockState(), blocks.get(clearPosition));
@@ -699,11 +699,11 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
public void unrelatedPieceBoundsAreRejectedBeforeTemplateScanning() {
BoundingBox area = new BoundingBox(0, -64, 0, 15, 319, 15);
assertTrue(NativeStructurePostProcessor.intersects(
assertTrue(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(15, 60, 15, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
assertFalse(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(16, 60, 16, 30, 90, 30), area));
assertFalse(NativeStructurePostProcessor.intersects(
assertFalse(NativeStructureTerrainIntegrator.intersects(
new BoundingBox(0, 320, 0, 15, 350, 15), area));
}
@@ -717,23 +717,22 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
BlockState log = Blocks.OAK_LOG.defaultBlockState();
blocks.put(origin, log);
NativeStructurePostProcessor.clearTemplateAir(world(blocks), template, origin, 80, settings);
NativeStructureTerrainIntegrator.clearTemplateAir(world(blocks), template, origin, 80, settings);
assertEquals(log, blocks.get(origin));
}
private static NativeStructurePostProcessor.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructurePostProcessor.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
private static NativeStructureSurfaceFitter.SurfaceAnchor anchor(int meetY, int strength) {
return new NativeStructureSurfaceFitter.SurfaceAnchor(0, 4, 0, 4, meetY, strength);
}
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
int horizontalPadding) {
private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve(
StructureStart start, int horizontalPadding) {
return organicCarve(start, horizontalPadding, 0.85D);
}
private static NativeStructurePostProcessor.OrganicCarve organicCarve(StructureStart start,
int horizontalPadding,
double lobeStrength) {
private static NativeStructureTerrainIntegrator.OrganicCarve organicCarve(
StructureStart start, int horizontalPadding, double lobeStrength) {
IrisStructureTerrain terrain = new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
.setShape(IrisStructureCarveShape.ERODED)
@@ -743,8 +742,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
.setErosionStrength(1D)
.setErosionFrequency(0.05D)
.setLobeStrength(lobeStrength);
return NativeStructurePostProcessor.organicCarve(
NativeStructurePostProcessor.carveFootprint(start, horizontalPadding,
return NativeStructureTerrainIntegrator.organicCarve(
NativeStructureTerrainIntegrator.carveFootprint(start, horizontalPadding,
NativeStructurePostProcessorSurfaceTerrainTest::forbiddenTemplateManager),
terrain, IrisStructureCarveShape.ERODED, TEST_SEED);
}
@@ -778,8 +777,8 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
SLAB_WIDTH - 1 + SLAB_PADDING, transectY, SLAB_DEPTH - 1);
Map<BlockPos, BlockState> blocks = fill(area);
NativeStructurePostProcessor.carveOrganicColumns(world(blocks), area,
NativeStructurePostProcessor.organicCarve(
NativeStructureTerrainIntegrator.carveOrganicColumns(world(blocks), area,
NativeStructureTerrainIntegrator.organicCarve(
footprint, terrain, IrisStructureCarveShape.ERODED, TEST_SEED));
int[] depths = new int[SLAB_DEPTH];
@@ -11,53 +11,53 @@ import static org.junit.Assert.assertTrue;
public class NativeStructurePostProcessorVegetationTest {
@Test
public void surfaceStructuresClearTreeColumnsAutomatically() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(116, 100, false));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(100, 100, false));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(116, 100, false));
}
@Test
public void buriedStructuresPreserveUnrelatedSurfaceForest() {
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, false));
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(99, 100, false));
assertFalse(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, false));
}
@Test
public void explicitVegetationOptionForcesUnusualPlacementCleanup() {
assertTrue(NativeStructurePostProcessor.shouldClearVegetationColumn(20, 100, true));
assertTrue(NativeStructureVegetationClearer.shouldClearVegetationColumn(20, 100, true));
}
@Test
public void surfaceStructuresPreserveVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.SURFACE_STRUCTURES, true));
}
@Test
public void undergroundStructuresPreserveSurfaceVegetationUnlessConfigured() {
assertFalse(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertFalse(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, false));
assertTrue(NativeStructurePostProcessor.shouldClearEntireVegetationFootprint(
assertTrue(NativeStructureVegetationClearer.shouldClearEntireVegetationFootprint(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES, true));
}
@Test
public void allUndergroundGenerationStepsShareOneClassification() {
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_STRUCTURES));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.UNDERGROUND_DECORATION));
assertTrue(NativeStructurePostProcessor.isUndergroundStep(
assertTrue(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.STRONGHOLDS));
assertFalse(NativeStructurePostProcessor.isUndergroundStep(
assertFalse(NativeStructureVegetationClearer.isUndergroundStep(
GenerationStep.Decoration.SURFACE_STRUCTURES));
}
@Test
public void undergroundStructuresUseTheLowestTerrainColumn() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
int offset = NativeStructurePostProcessor.resolveBuriedOffset(
int offset = NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, -64, 320, (x, z) -> x == 1 ? 76 : 100);
assertEquals(-5, offset);
}
@@ -65,7 +65,7 @@ public class NativeStructurePostProcessorVegetationTest {
@Test
public void undergroundBurialClampsToTheWorldFloorInsteadOfFailing() {
BoundingBox bounds = new BoundingBox(0, 60, 0, 1, 80, 0);
assertEquals(-2, NativeStructurePostProcessor.resolveBuriedOffset(
assertEquals(-2, NativeStructureVerticalPlacer.resolveBuriedOffset(
bounds, 0, 58, 320, (x, z) -> x == 1 ? 76 : 100));
}
}
@@ -27,28 +27,23 @@ import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.BukkitWorldReconciler;
import art.arcane.iris.core.IrisWorldGeneratorResolver;
import art.arcane.iris.core.PendingWorldDeleteQueue;
import art.arcane.iris.core.SettingsHotloadWatch;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.BukkitEnginePlatformHooks;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.api.terrain.IrisTerrainService;
import art.arcane.iris.core.link.IrisPapiInstaller;
import art.arcane.iris.core.link.IrisPapiListener;
import art.arcane.iris.core.link.IrisPapiState;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.EditSVC;
@@ -61,19 +56,14 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IrisCompat;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -96,17 +86,13 @@ import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.plugin.chunk.ChunkTickets;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.scheduling.Queue;
import art.arcane.volmlib.util.scheduling.ShurikenQueue;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -121,23 +107,14 @@ import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -145,7 +122,6 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -165,8 +141,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public static ChunkTickets tickets;
private static VolmitSender sender;
private static Thread shutdownHook;
private static File settingsFile;
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
static {
try {
@@ -178,11 +152,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
private static final Object TEARDOWN_LOCK = new Object();
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
private KMap<Class<? extends IrisService>, IrisService> services;
private final IrisWorldGeneratorResolver generatorResolver = new IrisWorldGeneratorResolver(this);
private final BukkitWorldReconciler worldReconciler = new BukkitWorldReconciler(this);
private final PendingWorldDeleteQueue pendingWorldDeletes = new PendingWorldDeleteQueue(this);
private volatile SettingsHotloadWatch settingsHotloadWatch;
public static VolmitSender getSender() {
if (sender == null) {
@@ -292,69 +273,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public static File getCached(String name, String url) {
String h = IO.hash(name + "@" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
if (!f.exists()) {
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
Iris.verbose("Aquiring " + name);
}
} catch (IOException e) {
Iris.reportError(e);
}
}
return f.exists() ? f : null;
}
public static String getNonCached(String name, String url) {
String h = IO.hash(name + "*" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
Iris.reportError(e);
}
try {
return IO.readAll(f);
} catch (IOException e) {
Iris.reportError(e);
}
return "";
}
public static File getNonCachedFile(String name, String url) {
String h = IO.hash(name + "*" + url);
File f = Iris.instance.getDataFile("cache", h.substring(0, 2), h.substring(3, 5), h);
Iris.verbose("Download " + name + " -> " + url);
try (BufferedInputStream in = new BufferedInputStream(URI.create(url).toURL().openStream()); FileOutputStream fileOutputStream = new FileOutputStream(f)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
fileOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
}
return f;
}
public static void warn(String format, Object... objs) {
msg(C.YELLOW + safeFormat(format, objs));
}
@@ -616,6 +534,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void enable() {
alreadyDrained.set(false);
servicesDisabled.set(false);
sharedRuntimeClosed.set(false);
MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen();
IrisLanguage.initialize();
PaperLibBootstrap.install();
SimdSupport.install();
@@ -635,7 +557,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(IrisCompat.class, compat);
ServerConfigurator.configure();
IrisToolbelt.applyPregenPerformanceProfile();
validateAllPacks();
generatorResolver.validateAllPacks();
IrisSafeguard.execute();
getSender().setTag(getTag());
splash();
@@ -649,37 +571,38 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisServices.register(EnginePlatformHooks.class, new BukkitEnginePlatformHooks());
IrisServices.register(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) IrisWorldManager::new);
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) Iris::queueWorldDeletionOnStartup);
settingsFile = getDataFile("settings.json");
IrisServices.register(art.arcane.iris.core.runtime.WorldDeletionQueue.class, (art.arcane.iris.core.runtime.WorldDeletionQueue) pendingWorldDeletes::queueWorldDeletionOnStartup);
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("settings.json"));
settingsHotloadWatch = watch;
configHotloadEngine = new ConfigHotloadEngine(
Iris::isSettingsFile,
Iris::knownSettingsFiles,
Iris::readSettingsContent,
Iris::normalizeSettingsContent
watch::isSettingsFile,
watch::knownSettingsFiles,
watch::readSettingsContent,
watch::normalizeSettingsContent
);
configHotloadEngine.configure(3_000L, List.of(settingsFile), List.of());
configHotloadEngine.configure(3_000L, List.of(watch.settingsFile()), List.of());
services.values().forEach(IrisService::onEnable);
services.values().forEach(this::registerListener);
addShutdownHook();
processPendingStartupWorldDeletes();
pendingWorldDeletes.processPendingStartupWorldDeletes();
WorldLifecycleService.get();
WorldRuntimeControlService.get();
if (J.isFolia()) {
J.s(() -> checkForBukkitWorlds(s -> true), 1);
J.s(() -> worldReconciler.checkForBukkitWorlds(s -> true), 1);
}
J.s(() -> {
J.a(() -> IO.delete(getTemp()));
J.a(this::bstats);
J.ar(this::checkConfigHotload, 60);
J.ar(() -> settingsHotloadWatch.checkConfigHotload(configHotloadEngine), 60);
J.sr(this::tickQueue, 0);
J.s(this::setupPapi);
J.a(DatapackIngestService::autoIngestOnStartup, 60);
autoStartStudio();
if (!J.isFolia()) {
checkForBukkitWorlds(s -> true);
worldReconciler.checkForBukkitWorlds(s -> true);
}
IrisToolbelt.retainMantleDataForSlice(String.class.getCanonicalName());
IrisToolbelt.retainMantleDataForSlice(BlockData.class.getCanonicalName());
@@ -696,24 +619,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
}
shutdownHook = new Thread(() -> {
if (alreadyDrained.compareAndSet(false, true)) {
try {
Bukkit.getWorlds()
.stream()
.map(World::getGenerator)
.filter(PlatformChunkGenerator.class::isInstance)
.map(PlatformChunkGenerator.class::cast)
.forEach(PlatformChunkGenerator::close);
} catch (Throwable e) {
Iris.reportError("Failed to close Iris world generators from the JVM shutdown hook.", e);
}
}
MultiBurst.burst.close();
MultiBurst.ioBurst.close();
IrisServices.clear();
}, "Iris-ShutdownHook");
shutdownHook = new Thread(() -> teardownRuntime("shutdown-hook", 30L), "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
} catch (IllegalStateException ex) {
@@ -721,247 +627,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
public void checkForBukkitWorlds(Predicate<String> filter) {
try {
KList<String> deferredStartupWorlds = new KList<>();
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
try {
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
Iris.info("Loading World: %s | Generator: %s", s, generator);
ChunkGenerator gen = getDefaultWorldGenerator(s, generator);
IrisDimension dim = loadDimension(s, generator);
assert dim != null && gen != null;
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen)
.environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) {
c.seed(stagedSeed);
}
INMS.get().createWorld(c);
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
} catch (Throwable e) {
if (containsCreateWorldUnsupportedOperation(e)) {
if (J.isFolia()) {
if (!deferredStartupWorlds.contains(s)) {
deferredStartupWorlds.add(s);
}
return;
}
Iris.error("Failed to load world " + s + "!");
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
reportError("Failed to load staged startup world \"" + s + "\".", e);
return;
}
reportError("Failed to load startup world \"" + s + "\".", e);
}
});
if (!deferredStartupWorlds.isEmpty()) {
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
}
} catch (Throwable e) {
reportError("Failed while loading startup Iris worlds.", e);
}
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
public static synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
if (instance == null || worldNames == null || worldNames.isEmpty()) {
return 0;
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
int before = queue.size();
for (String worldName : worldNames) {
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
if (queue.size() != before) {
writePendingWorldDeleteMap(queue);
}
return queue.size() - before;
}
private void processPendingStartupWorldDeletes() {
try {
try {
int unregistered = art.arcane.iris.core.tools.IrisCreator.removeTransientStudioWorldsFromBukkitYml();
if (unregistered > 0) {
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
}
} catch (Throwable e) {
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
}
if (queue.isEmpty()) {
return;
}
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
for (String worldName : queue.values()) {
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
continue;
}
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
if (loaded != null) {
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
try {
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
if (generator != null) {
generator.close();
}
IrisToolbelt.evacuate(loaded);
Bukkit.unloadWorld(loaded, false);
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
} catch (Throwable e) {
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
}
if (WorldIdentity.resolve(worldKey).isPresent()) {
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
} else {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
boolean foundAny = false;
boolean deletedAll = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
if (!worldFolder.exists()) {
continue;
}
foundAny = true;
IO.delete(worldFolder);
if (worldFolder.exists()) {
deletedAll = false;
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
} else {
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
}
}
if (!foundAny) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
continue;
}
if (!deletedAll) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
writePendingWorldDeleteMap(remaining);
} catch (Throwable e) {
Iris.error("Failed to process queued startup world deletions.");
reportError(e);
e.printStackTrace();
}
}
private static LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
if (instance == null) {
return queue;
}
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
if (!queueFile.exists()) {
return queue;
}
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
String line;
while ((line = reader.readLine()) != null) {
String normalized = normalizeWorldName(line);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
}
return queue;
}
private static void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
if (instance == null) {
return;
}
File queueFile = instance.getDataFile(PENDING_WORLD_DELETE_FILE);
if (queue.isEmpty()) {
if (queueFile.exists()) {
IO.delete(queueFile);
}
return;
}
File parent = queueFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
}
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
for (String worldName : queue.values()) {
writer.println(worldName);
}
}
}
@Nullable
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String trimmed = worldName.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
public BukkitWorldReconciler worldReconciler() {
return worldReconciler;
}
private void autoStartStudio() {
@@ -1012,13 +679,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void onDisable() {
teardownPapi();
if (IrisSafeguard.isForceShutdown()) return;
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators("onDisable", 30L);
}
if (services != null) {
services.values().forEach(IrisService::onDisable);
}
IrisServices.clear();
teardownRuntime("onDisable", 30L);
if (BukkitPlatform.hasHud()) {
BukkitPlatform.hudSlots().shutdown();
BukkitPlatform.hudLanes().shutdown();
@@ -1039,12 +700,54 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Override
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
teardownPapi();
if (!alreadyDrained.compareAndSet(false, true)) {
if (alreadyDrained.get()) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
return;
}
Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
drainWorldGenerators("pre-unload:" + reason, 45L);
drainOnce("pre-unload:" + reason, 45L);
}
/**
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
* second caller cannot rip the pools or services out from under an in-flight drain.
*/
private void drainOnce(String reason, long timeoutSeconds) {
synchronized (TEARDOWN_LOCK) {
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators(reason, timeoutSeconds);
}
}
}
/**
* Full teardown: generators, then services, then the shared pools and the service map.
* Both onDisable and the JVM shutdown hook route through here; whichever runs second is a no-op.
*/
private void teardownRuntime(String reason, long timeoutSeconds) {
synchronized (TEARDOWN_LOCK) {
if (alreadyDrained.compareAndSet(false, true)) {
drainWorldGenerators(reason, timeoutSeconds);
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
for (IrisService service : services.values()) {
try {
service.onDisable();
} catch (Throwable e) {
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
}
}
}
if (!sharedRuntimeClosed.compareAndSet(false, true)) {
return;
}
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
IrisServices.clear();
}
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
@@ -1164,61 +867,6 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return IrisSafeguard.mode().tag(subTag);
}
private void checkConfigHotload() {
if (configHotloadEngine == null) {
return;
}
for (File file : configHotloadEngine.pollTouchedFiles()) {
configHotloadEngine.processFileChange(file, ignored -> {
IrisSettings.invalidate();
IrisSettings.get();
IrisLanguage.reload();
return true;
}, ignored -> Iris.info("Hotloaded settings.json "));
}
IrisLanguage.update();
}
private static boolean isSettingsFile(File file) {
if (file == null || settingsFile == null) {
return false;
}
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
}
private static List<File> knownSettingsFiles() {
if (settingsFile == null) {
return List.of();
}
return List.of(settingsFile);
}
private static String readSettingsContent(File file) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
try {
return IO.readAll(file);
} catch (Throwable ex) {
Iris.warn("Failed to read settings file %s: %s%s",
file.getAbsolutePath(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
return null;
}
}
private static String normalizeSettingsContent(String text) {
if (text == null) {
return null;
}
return text.replace("\r\n", "\n").trim();
}
private void tickQueue() {
synchronized (Iris.syncJobs) {
if (!Iris.syncJobs.hasNext()) {
@@ -1256,117 +904,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
@Nullable
@Override
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
}
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
return super.getDefaultBiomeProvider(worldName, id);
return generatorResolver.resolveDefaultBiomeProvider(worldName, id, () -> super.getDefaultBiomeProvider(worldName, id));
}
@Override
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
}
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
IrisWorld w = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.build();
Iris.debug("Generator Config: " + w.toString());
File ff = new File(w.worldFolder(), "iris/pack");
File[] files = ff.listFiles();
if (files == null || files.length == 0)
IO.delete(ff);
if (!ff.exists()) {
ff.mkdirs();
dim = service(StudioSVC.class).installIntoWorld(getSender(), dim, w.worldFolder());
if (dim == null) {
throw new IllegalStateException("Failed to install dimension pack for " + id);
}
}
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
}
public static void validateAllPacks() {
File packsRoot = Iris.instance.getDataFolder("packs");
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
}
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
Iris.info("Resolved missing dimension, proceeding.");
}
}
return dimension;
return generatorResolver.resolveDefaultWorldGenerator(worldName, id);
}
public void splash() {
@@ -0,0 +1,110 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator;
import java.util.function.Predicate;
/**
* Loads Iris worlds that are staged in bukkit.yml but not yet present on the server.
*/
public final class BukkitWorldReconciler {
private final Iris plugin;
public BukkitWorldReconciler(Iris plugin) {
this.plugin = plugin;
}
public void checkForBukkitWorlds(Predicate<String> filter) {
try {
KList<String> deferredStartupWorlds = new KList<>();
IrisWorlds.readBukkitWorlds().forEach((s, generator) -> {
try {
NamespacedKey worldKey = IrisWorldStorage.keyFromName(s);
if (WorldIdentity.resolve(worldKey).isPresent() || !filter.test(s)) return;
Iris.info("Loading World: %s | Generator: %s", s, generator);
ChunkGenerator gen = plugin.getDefaultWorldGenerator(s, generator);
IrisDimension dim = IrisWorldGeneratorResolver.loadDimension(s, generator);
assert dim != null && gen != null;
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + s + "' using Iris:" + generator + "...");
WorldCreator c = WorldCreator.ofKey(worldKey)
.generator(gen)
.environment(BukkitEnvironment.from(dim.getEnvironment()));
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
if (stagedSeed != null) {
c.seed(stagedSeed);
}
INMS.get().createWorld(c);
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
} catch (Throwable e) {
if (containsCreateWorldUnsupportedOperation(e)) {
if (J.isFolia()) {
if (!deferredStartupWorlds.contains(s)) {
deferredStartupWorlds.add(s);
}
return;
}
Iris.error("Failed to load world " + s + "!");
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
Iris.reportError("Failed to load staged startup world \"" + s + "\".", e);
return;
}
Iris.reportError("Failed to load startup world \"" + s + "\".", e);
}
});
if (!deferredStartupWorlds.isEmpty()) {
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
}
} catch (Throwable e) {
Iris.reportError("Failed while loading startup Iris worlds.", e);
}
}
private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) {
Throwable cursor = throwable;
while (cursor != null) {
if (cursor instanceof UnsupportedOperationException || cursor instanceof IllegalStateException) {
for (StackTraceElement element : cursor.getStackTrace()) {
if ("org.bukkit.craftbukkit.CraftServer".equals(element.getClassName())
&& "createWorld".equals(element.getMethodName())) {
return true;
}
}
}
cursor = cursor.getCause();
}
return false;
}
}
@@ -0,0 +1,173 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.io.IO;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.function.Supplier;
/**
* Pack validation, dimension lookup, and the world generator / biome provider resolution that the
* Bukkit plugin entry points delegate to.
*/
public final class IrisWorldGeneratorResolver {
private final VolmitPlugin plugin;
public IrisWorldGeneratorResolver(VolmitPlugin plugin) {
this.plugin = plugin;
}
public void validateAllPacks() {
File packsRoot = plugin.getDataFolder("packs");
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
}
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File pack = IrisWorldStorage.packRoot(IrisWorldStorage.keyFromName(worldName));
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
Iris.warn("Unable to find dimension type " + id + " Looking for online packs...");
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender()), id, false);
dimension = IrisData.loadAnyDimension(id, null);
if (dimension != null) {
Iris.info("Resolved missing dimension, proceeding.");
}
}
return dimension;
}
/**
* Resolves the biome provider for a world, falling back to the supplied Bukkit default when
* Iris has nothing staged.
*/
@Nullable
public BiomeProvider resolveDefaultBiomeProvider(String worldName, @Nullable String id, Supplier<BiomeProvider> fallback) {
BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
}
Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id);
return fallback.get();
}
public ChunkGenerator resolveDefaultWorldGenerator(String worldName, String id) {
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
}
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
IrisWorld w = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.build();
Iris.debug("Generator Config: " + w.toString());
File ff = new File(w.worldFolder(), "iris/pack");
File[] files = ff.listFiles();
if (files == null || files.length == 0)
IO.delete(ff);
if (!ff.exists()) {
ff.mkdirs();
dim = Iris.service(StudioSVC.class).installIntoWorld(Iris.getSender(), dim, w.worldFolder());
if (dim == null) {
throw new IllegalStateException("Failed to install dimension pack for " + id);
}
}
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
}
}
@@ -0,0 +1,228 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
/**
* Persistent queue of world folders that must be deleted on the next startup, plus the startup
* drain that actually removes them.
*/
public final class PendingWorldDeleteQueue {
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private final VolmitPlugin plugin;
public PendingWorldDeleteQueue(VolmitPlugin plugin) {
this.plugin = plugin;
}
public synchronized int queueWorldDeletionOnStartup(Collection<String> worldNames) throws IOException {
if (worldNames == null || worldNames.isEmpty()) {
return 0;
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
int before = queue.size();
for (String worldName : worldNames) {
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
if (queue.size() != before) {
writePendingWorldDeleteMap(queue);
}
return queue.size() - before;
}
public void processPendingStartupWorldDeletes() {
try {
try {
int unregistered = IrisCreator.removeTransientStudioWorldsFromBukkitYml();
if (unregistered > 0) {
Iris.info("Unregistered " + unregistered + " transient studio world(s) from bukkit.yml on startup.");
}
} catch (Throwable e) {
Iris.reportError("Failed to unregister transient studio worlds from bukkit.yml on startup.", e);
}
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(IrisWorldStorage.levelRoot())) {
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
}
if (queue.isEmpty()) {
return;
}
LinkedHashMap<String, String> remaining = new LinkedHashMap<>();
for (String worldName : queue.values()) {
if (worldName.equalsIgnoreCase(ServerProperties.LEVEL_NAME)) {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is configured as level-name.");
continue;
}
NamespacedKey worldKey = IrisWorldStorage.keyFromName(worldName);
World loaded = WorldIdentity.resolve(worldKey).orElse(null);
if (loaded != null) {
if (TransientWorldCleanupSupport.isTransientStudioWorldName(worldName)) {
try {
PlatformChunkGenerator generator = IrisToolbelt.access(loaded);
if (generator != null) {
generator.close();
}
IrisToolbelt.evacuate(loaded);
Bukkit.unloadWorld(loaded, false);
Iris.info("Unloaded leftover studio world \"" + worldName + "\" for deletion.");
} catch (Throwable e) {
Iris.reportError("Failed to unload leftover studio world \"" + worldName + "\".", e);
}
if (WorldIdentity.resolve(worldKey).isPresent()) {
Iris.warn("Studio world \"" + worldName + "\" is still loaded after unload; will retry next startup.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
} else {
Iris.warn("Skipping queued deletion for \"" + worldName + "\" because it is currently loaded.");
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
boolean foundAny = false;
boolean deletedAll = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File worldFolder = IrisWorldStorage.dimensionRoot(familyWorldName);
if (!worldFolder.exists()) {
continue;
}
foundAny = true;
IO.delete(worldFolder);
if (worldFolder.exists()) {
deletedAll = false;
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
} else {
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
}
}
if (!foundAny) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
continue;
}
if (!deletedAll) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
}
writePendingWorldDeleteMap(remaining);
} catch (Throwable e) {
Iris.error("Failed to process queued startup world deletions.");
Iris.reportError(e);
e.printStackTrace();
}
}
private LinkedHashMap<String, String> loadPendingWorldDeleteMap() throws IOException {
LinkedHashMap<String, String> queue = new LinkedHashMap<>();
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
if (!queueFile.exists()) {
return queue;
}
try (BufferedReader reader = new BufferedReader(new FileReader(queueFile))) {
String line;
while ((line = reader.readLine()) != null) {
String normalized = normalizeWorldName(line);
if (normalized == null) {
continue;
}
queue.putIfAbsent(normalized.toLowerCase(Locale.ROOT), normalized);
}
}
return queue;
}
private void writePendingWorldDeleteMap(Map<String, String> queue) throws IOException {
File queueFile = plugin.getDataFile(PENDING_WORLD_DELETE_FILE);
if (queue.isEmpty()) {
if (queueFile.exists()) {
IO.delete(queueFile);
}
return;
}
File parent = queueFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create queue directory: " + parent.getAbsolutePath());
}
try (PrintWriter writer = new PrintWriter(new FileWriter(queueFile))) {
for (String worldName : queue.values()) {
writer.println(worldName);
}
}
}
@Nullable
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String trimmed = worldName.trim();
if (trimmed.isEmpty()) {
return null;
}
return trimmed;
}
}
@@ -0,0 +1,98 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.io.IO;
import java.io.File;
import java.util.List;
/**
* Identity and hotload handling for settings.json. Supplies the predicates the
* {@link ConfigHotloadEngine} is built from and drains the touched-file queue.
*/
public final class SettingsHotloadWatch {
private final File settingsFile;
public SettingsHotloadWatch(File settingsFile) {
this.settingsFile = settingsFile;
}
public File settingsFile() {
return settingsFile;
}
public void checkConfigHotload(ConfigHotloadEngine engine) {
if (engine == null) {
return;
}
for (File file : engine.pollTouchedFiles()) {
engine.processFileChange(file, ignored -> {
IrisSettings.invalidate();
IrisSettings.get();
IrisLanguage.reload();
return true;
}, ignored -> Iris.info("Hotloaded settings.json "));
}
IrisLanguage.update();
}
public boolean isSettingsFile(File file) {
if (file == null || settingsFile == null) {
return false;
}
return settingsFile.getAbsoluteFile().equals(file.getAbsoluteFile());
}
public List<File> knownSettingsFiles() {
if (settingsFile == null) {
return List.of();
}
return List.of(settingsFile);
}
public String readSettingsContent(File file) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
try {
return IO.readAll(file);
} catch (Throwable ex) {
Iris.warn("Failed to read settings file %s: %s%s",
file.getAbsolutePath(),
ex.getClass().getSimpleName(),
ex.getMessage() == null ? "" : " - " + ex.getMessage());
Iris.reportError(ex);
return null;
}
}
public String normalizeSettingsContent(String text) {
if (text == null) {
return null;
}
return text.replace("\r\n", "\n").trim();
}
}
@@ -51,6 +51,7 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.BufferedInputStream;
import java.io.File;
@@ -283,7 +284,7 @@ public class CommandDeveloper implements DirectorExecutor {
}
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER, sync = true)
@Director(description = "Delete nearby chunk blocks for regen testing", descriptionKey = "iris.director.commanddeveloper.director.delete_nearby_chunk_blocks_regen_testing", name = "delete-chunk", aliases = {"dc"}, origin = DirectorOrigin.PLAYER)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", descriptionKey = "iris.director.commanddeveloper.param.radius_chunks_around_your_current_chunk", defaultValue = "0")
int radius
@@ -293,25 +294,34 @@ public class CommandDeveloper implements DirectorExecutor {
return;
}
World world = player().getWorld();
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_THIS_IS_NOT_IRIS_WORLD));
return;
}
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL));
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
Engine engine = access.getEngine();
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
// The player position must be read on the thread owning the player; ChunkClearer hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
new ChunkClearer(world, access.getEngine(), sender(), centerX, centerZ, radius).start();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_DELETE_STARTED_CHUNK_S_AROUND_CLEARING_BLOCKS_AIR, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
new ChunkClearer(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule delete-chunk on the thread owning " + player.getName() + ".");
}
}
@Director(description = "Test", descriptionKey = "iris.director.commanddeveloper.director.test_4", aliases = {"ip"})
@@ -332,7 +342,7 @@ public class CommandDeveloper implements DirectorExecutor {
// --- Regen ---
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER, sync = true)
@Director(name = "regen", aliases = {"rg"}, description = "Delete and regenerate nearby chunks in place using Iris generation", descriptionKey = "iris.director.commanddeveloper.director.delete_regenerate_nearby_chunks_place_using_iris_generation", origin = DirectorOrigin.PLAYER)
public void regen(
@Param(name = "radius", description = "The radius of nearby chunks", descriptionKey = "iris.director.commanddeveloper.param.radius_nearby_chunks", defaultValue = "5")
int radius
@@ -342,29 +352,37 @@ public class CommandDeveloper implements DirectorExecutor {
return;
}
World world = player().getWorld();
Player player = player();
VolmitSender commandSender = sender();
World world = player.getWorld();
if (!IrisToolbelt.isIrisWorld(world)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_YOU_MUST_BE_IRIS_WORLD_USE_REGEN));
return;
}
Engine engine = IrisToolbelt.access(world).getEngine();
if (engine == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_ENGINE_ACCESS_THIS_WORLD_IS_NULL_GENERATE_NEARBY_CHUNKS_FIRST));
return;
}
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int chunks = (radius * 2 + 1) * (radius * 2 + 1);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
// The player position must be read on the thread owning the player; the regenerator hops per chunk itself.
if (!J.runEntity(player, () -> {
int centerX = player.getLocation().getBlockX() >> 4;
int centerZ = player.getLocation().getBlockZ() >> 4;
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_DEVELOPER_REGEN_STARTED_CHUNK_S_AROUND_DELETING_REGENERATING_PLACE, MessageArgument.untrusted("chunks", chunks), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ)));
Iris.info("Regen run start: world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " chunks=" + chunks);
new InPlaceChunkRegenerator(world, engine, commandSender, centerX, centerZ, radius).start();
})) {
Iris.warn("Could not schedule regen on the thread owning " + player.getName() + ".");
}
}
@Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", descriptionKey = "iris.director.commanddeveloper.director.generate_chunks_into_buffers_no_world_writes_hash_blocks_biomes_captures_golden", origin = DirectorOrigin.BOTH)
@@ -316,19 +316,6 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_IRIS_V_BY_VOLMIT_SOFTWARE, MessageArgument.untrusted("value", Iris.instance.getDescription().getVersion())));
}
/*
/todo
@Director(description = "Benchmark a pack", descriptionKey = "iris.director.commandiris.director.benchmark_pack", origin = DirectorOrigin.CONSOLE)
public void packbenchmark(
@Param(description = "Dimension to benchmark", descriptionKey = "iris.director.commandiris.param.dimension_benchmark")
IrisDimension type
) throws InterruptedException {
BenchDimension = type.getLoadKey();
IrisPackBenchmarking.runBenchmark();
} */
@Director(description = "Print world height information", descriptionKey = "iris.director.commandiris.director.print_world_height_information", origin = DirectorOrigin.PLAYER)
public void height() {
if (sender().isPlayer()) {
@@ -588,7 +575,7 @@ public class CommandIris implements DirectorExecutor {
return;
}
Iris.instance.checkForBukkitWorlds(logicalWorldName::equals);
Iris.instance.worldReconciler().checkForBukkitWorlds(logicalWorldName::equals);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_LOADED_SUCCESSFULLY, MessageArgument.untrusted("logicalWorldName", logicalWorldName)));
}
@Director(description = "Evacuate an iris world", descriptionKey = "iris.director.commandiris.director.evacuate_iris_world", origin = DirectorOrigin.PLAYER, sync = true)
@@ -428,8 +428,13 @@ public class CommandObject implements DirectorExecutor {
@Director(description = "Get a powder that reveals objects", descriptionKey = "iris.director.commandobject.director.get_powder_that_reveals_objects", aliases = "d")
public void dust() {
player().getInventory().addItem(WandSVC.createDust());
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createDust());
commandSender.playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
});
}
@Director(description = "Contract a selection based on your looking direction", descriptionKey = "iris.director.commandobject.director.contract_selection_based_on_your_looking_direction", aliases = "-")
@@ -437,28 +442,33 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "The amount to inset by", descriptionKey = "iris.director.commandobject.param.amount_inset_by", defaultValue = "1")
int amount
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
return;
}
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND));
return;
}
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Cuboid cursor = new Cuboid(a1, a2);
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
assert d != null;
cursor = cursor.expand(d.f(), -amount);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Set point 1 to look", descriptionKey = "iris.director.commandobject.director.set_point_1_look", aliases = "p1")
@@ -466,25 +476,30 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look", defaultValue = "true")
boolean here
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
return;
}
VolmitSender commandSender = sender();
Player player = player();
if (WandSVC.isHoldingWand(player())) {
Location[] g = WandSVC.getCuboid(player());
if (g == null) {
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[1] = player().getTargetBlock(null, 256).getLocation().clone();
} else {
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
if (WandSVC.isHoldingWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[1] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[1] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Set point 2 to look", descriptionKey = "iris.director.commandobject.director.set_point_2_look", aliases = "p2")
@@ -492,26 +507,31 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "Whether to use your current position, or where you look", descriptionKey = "iris.director.commandobject.param.whether_use_your_current_position_where_you_look_2", defaultValue = "true")
boolean here
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
return;
}
VolmitSender commandSender = sender();
Player player = player();
if (WandSVC.isHoldingIrisWand(player())) {
Location[] g = WandSVC.getCuboid(player());
if (g == null) {
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
if (WandSVC.isHoldingIrisWand(player)) {
Location[] g = WandSVC.getCuboid(player);
if (g == null) {
return;
}
if (!here) {
// TODO: WARNING HEIGHT
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
} else {
g[0] = player.getLocation().getBlock().getLocation().clone().add(0, -1, 0);
}
player.getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
player().getInventory().setItemInMainHand(WandSVC.createWand(g[0], g[1]));
}
});
}
@Director(description = "Paste an object", descriptionKey = "iris.director.commandobject.director.paste_object", sync = true)
@@ -540,8 +560,10 @@ public class CommandObject implements DirectorExecutor {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(IrisObjectRotation.of(0, rotate, 0));
ItemStack wand = player().getInventory().getItemInMainHand();
Location block = player().getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
VolmitSender commandSender = sender();
Player player = player();
ItemStack wand = player.getInventory().getItemInMainHand();
Location block = player.getTargetBlock(skipBlocks, 256).getLocation().clone().add(0, 1, 0);
Map<Block, BlockData> futureChanges = new HashMap<>();
@@ -549,30 +571,50 @@ public class CommandObject implements DirectorExecutor {
o = o.scaled(scale, IrisObjectPlacementScaleInterpolator.TRICUBIC);
}
o.place(block.getBlockX(), block.getBlockY() + (int) o.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
// Block writes must run on the thread owning the target chunk; the undo log stays global.
final IrisObject placed = o;
if (!J.runAt(block, () -> {
placed.place(block.getBlockX(), block.getBlockY() + (int) placed.getCenter().getY(), block.getBlockZ(), createPlacer(block.getWorld(), futureChanges), placement, new RNG(), null);
J.runGlobal(() -> Iris.service(ObjectSVC.class).addChanges(futureChanges));
Iris.service(ObjectSVC.class).addChanges(futureChanges);
if (edit) {
Vector center = new Vector(o.getCenter().getX(), o.getCenter().getY(), o.getCenter().getZ());
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(o.getW() - 1,
o.getH() + center.getY() - 1, o.getD() - 1), block.clone().subtract(center.clone().setY(0)));
if (WandSVC.isWand(wand)) {
wand = newWand;
player().getInventory().setItemInMainHand(wand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
} else {
int slot = WandSVC.findWand(player().getInventory());
if (slot == -1) {
player().getInventory().addItem(newWand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
} else {
player().getInventory().setItem(slot, newWand);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", o.getLoadKey())));
}
if (!edit) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
return;
}
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
onPlayerThread(player, () -> {
Vector center = new Vector(placed.getCenter().getX(), placed.getCenter().getY(), placed.getCenter().getZ());
ItemStack newWand = WandSVC.createWand(block.clone().subtract(center).add(placed.getW() - 1,
placed.getH() + center.getY() - 1, placed.getD() - 1), block.clone().subtract(center.clone().setY(0)));
if (WandSVC.isWand(wand)) {
player.getInventory().setItemInMainHand(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
int slot = WandSVC.findWand(player.getInventory());
if (slot == -1) {
player.getInventory().addItem(newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_GIVEN_NEW_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", placed.getLoadKey())));
} else {
player.getInventory().setItem(slot, newWand);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
}
}
});
})) {
Iris.warn("Could not schedule the object paste at " + block.getBlockX() + ", " + block.getBlockY() + ", " + block.getBlockZ() + ".");
}
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris object on the thread owning " + player.getName() + ".");
}
}
@@ -616,30 +658,35 @@ public class CommandObject implements DirectorExecutor {
@Param(description = "The amount to shift by", descriptionKey = "iris.director.commandobject.param.amount_shift_by", defaultValue = "1")
int amount
) {
if (!WandSVC.isHoldingWand(player())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
return;
}
VolmitSender commandSender = sender();
Player player = player();
Location[] b = WandSVC.getCuboid(player());
if (b == null || b[0] == null || b[1] == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player().getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
a1.add(d.toVector().multiply(amount));
a2.add(d.toVector().multiply(amount));
Cuboid cursor = new Cuboid(a1, a2);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player().getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player().updateInventory();
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
onPlayerThread(player, () -> {
if (!WandSVC.isHoldingWand(player)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
return;
}
Location[] b = WandSVC.getCuboid(player);
if (b == null || b[0] == null || b[1] == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED_2));
return;
}
Location a1 = b[0].clone();
Location a2 = b[1].clone();
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
if (d == null) {
return; // HOW DID THIS HAPPEN
}
a1.add(d.toVector().multiply(amount));
a2.add(d.toVector().multiply(amount));
Cuboid cursor = new Cuboid(a1, a2);
b[0] = cursor.getLowerNE();
b[1] = cursor.getUpperSW();
player.getInventory().setItemInMainHand(WandSVC.createWand(b[0], b[1]));
player.updateInventory();
commandSender.playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
});
}
@Director(description = "Undo a number of pastes", descriptionKey = "iris.director.commandobject.director.undo_number_pastes", aliases = "u")
@@ -655,20 +702,25 @@ public class CommandObject implements DirectorExecutor {
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", descriptionKey = "iris.director.commandobject.director.gets_object_wand_grabs_current_worldedit_selection", aliases = "we", origin = DirectorOrigin.PLAYER)
public void we() {
VolmitSender commandSender = sender();
Player player = player();
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_CAN_T_GET_WORLDEDIT_SELECTION_WITHOUT_WORLDEDIT_YOU_KNOW));
return;
}
Cuboid locs = WorldEditLink.getSelection(sender().player());
Cuboid locs = WorldEditLink.getSelection(player);
if (locs == null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_YOU_DON_T_HAVE_WORLDEDIT_SELECTION_THIS_WORLD));
return;
}
sender().player().getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
onPlayerThread(player, () -> {
player.getInventory().addItem(WandSVC.createWand(locs.getLowerNE(), locs.getUpperSW()));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_FRESH_WAND_WITH_YOUR_CURRENT_WORLDEDIT_SELECTION_ON_IT));
});
}
@Director(description = "Get an object wand", descriptionKey = "iris.director.commandobject.director.get_object_wand", sync = true)
@@ -26,6 +26,7 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
@@ -104,6 +105,8 @@ import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
@@ -114,6 +117,7 @@ import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
public class CommandStudio implements DirectorExecutor {
private static final long CHUNK_SCAN_TIMEOUT_MS = 3_000;
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
@@ -662,10 +666,18 @@ public class CommandStudio implements DirectorExecutor {
@Param(description = "The location to spawn the entity at", descriptionKey = "iris.director.commandstudio.param.location_spawn_entity_at", contextual = true)
Vector location
) {
VolmitSender commandSender = sender();
Engine spawnEngine = engine();
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_YOU_HAVE_BE_IRIS_WORLD_SPAWN_ENTITIES_PROPERLY_TRYING_SPAWN));
}
// Entity creation must run on the thread owning the destination chunk.
Location at = new Location(world(), location.getX(), location.getY(), location.getZ());
if (!J.runAt(at, () -> entity.spawn(spawnEngine, at))) {
Iris.warn("Could not schedule the entity spawn at " + at.getBlockX() + ", " + at.getBlockY() + ", " + at.getBlockZ() + ".");
}
entity.spawn(engine(), new Location(world(), location.getX(), location.getY(), location.getZ()));
}
@Director(description = "Teleport to the active studio world", descriptionKey = "iris.director.commandstudio.director.teleport_active_studio_world", aliases = "stp", origin = DirectorOrigin.PLAYER, sync = true)
@@ -697,7 +709,7 @@ public class CommandStudio implements DirectorExecutor {
IrisDimension dimension
) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATING_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
if (new IrisProject(dimension.getLoader().getDataFolder()).updateWorkspace()) {
if (new IrisCodeWorkspace(new IrisProject(dimension.getLoader().getDataFolder())).updateWorkspace()) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_UPDATED_CODE_WORKSPACE, MessageArgument.untrusted("value", dimension.getName())));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_INVALID_PROJECT_TRY_DELETING_CODE_WORKSPACE_FILE_TRY_AGAIN, MessageArgument.untrusted("value", dimension.getName())));
@@ -718,20 +730,46 @@ public class CommandStudio implements DirectorExecutor {
return;
}
KList<Chunk> chunks = new KList<>();
int bx = player().getLocation().getChunk().getX();
int bz = player().getLocation().getChunk().getZ();
Player reporter = player();
CountDownLatch gathered = new CountDownLatch(1);
try {
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
// The raycast and the chunk loads need the thread owning the player; the report itself stays off it.
boolean scheduled = J.runEntity(reporter, () -> {
try {
int bx = reporter.getLocation().getChunk().getX();
int bz = reporter.getLocation().getChunk().getZ();
int cx = l.getChunk().getX();
int cz = l.getChunk().getZ();
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
} catch (Throwable e) {
Iris.reportError(e);
try {
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
int cx = l.getChunk().getX();
int cz = l.getChunk().getZ();
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + cx, z + cz))).drain();
} catch (Throwable e) {
Iris.reportError(e);
}
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
} finally {
gathered.countDown();
}
});
if (!scheduled) {
Iris.warn("Could not schedule the chunk report scan on the thread owning " + reporter.getName() + ".");
return;
}
try {
if (!gathered.await(CHUNK_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Iris.warn("Timed out waiting for the chunk report scan of " + reporter.getName() + ".");
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
new Spiraler(3, 3, (x, z) -> chunks.addIfMissing(world.getChunkAt(x + bx, z + bz))).drain();
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CAPTURING_IGENDATA_FROM_NEARBY_CHUNKS, MessageArgument.untrusted("value", chunks.size())));
try {
File ff = Iris.instance.getDataFile("reports/" + M.ms() + ".txt");
@@ -25,10 +25,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.edit.BlockSignal;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -38,8 +41,10 @@ import org.bukkit.Chunk;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicInteger;
@@ -97,83 +102,115 @@ public class CommandWhat implements DirectorExecutor {
@Director(description = "What region am i in?", descriptionKey = "iris.director.commandwhat.director.what_region_am_i", origin = DirectorOrigin.PLAYER)
public void region() {
try {
Chunk chunk = world().getChunkAt(player().getLocation().getBlockX() >> 4, player().getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine(), chunk);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
VolmitSender commandSender = sender();
Player player = player();
World world = world();
Engine engine = engine();
} catch (Throwable e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
}
// Chunk access must happen on the thread owning the player's chunk.
onPlayerThread(player, () -> {
try {
Chunk chunk = world.getChunkAt(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4);
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
} catch (Throwable e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
}
});
}
@Director(description = "What block am i looking at?", descriptionKey = "iris.director.commandwhat.director.what_block_am_i_looking_at", origin = DirectorOrigin.PLAYER)
public void block() {
BlockData bd;
try {
bd = player().getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
} catch (NullPointerException e) {
Iris.reportError(e);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
bd = null;
}
VolmitSender commandSender = sender();
Player player = player();
if (bd != null) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
if (BukkitBlockResolution.isStorage(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
// The raycast reads blocks, so it has to run on the thread owning the player.
onPlayerThread(player, () -> {
BlockData bd;
try {
bd = player.getTargetBlockExact(128, FluidCollisionMode.NEVER).getBlockData();
} catch (NullPointerException e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
bd = null;
}
if (BukkitBlockResolution.isLit(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
}
if (bd != null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_MATERIAL_3, MessageArgument.untrusted("value", bd.getMaterial().name())));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FULL_2, MessageArgument.untrusted("value", bd.getAsString(true))));
if (BukkitBlockResolution.isFoliage(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isStorage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
}
if (BukkitBlockResolution.isDecorant(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
}
if (BukkitBlockResolution.isLit(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
}
if (BukkitBlockResolution.isFluid(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
}
if (BukkitBlockResolution.isFoliage(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isDecorant(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
}
if (BukkitBlockResolution.isSolid(bd)) {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
if (BukkitBlockResolution.isFluid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
}
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
}
if (BukkitBlockResolution.isSolid(bd)) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
}
}
}
});
}
@Director(description = "Show markers in chunk", descriptionKey = "iris.director.commandwhat.director.show_markers_chunk", origin = DirectorOrigin.PLAYER)
public void markers(@Param(description = "Marker name such as cave_floor or cave_ceiling", descriptionKey = "iris.director.commandwhat.param.marker_name_such_as_cave_floor_cave_ceiling") String marker) {
Chunk c = player().getLocation().getChunk();
VolmitSender commandSender = sender();
Player player = player();
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
int m = 1;
AtomicInteger v = new AtomicInteger(0);
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
onPlayerThread(player, () -> {
Chunk c = player.getLocation().getChunk();
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
AtomicInteger v = new AtomicInteger(0);
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
for (int zzz = c.getZ() - 4; zzz <= c.getZ() + 4; zzz++) {
IrisToolbelt.access(c.getWorld()).getEngine().getMantle().findMarkers(xxx, zzz, new MatterMarker(marker))
.convert((i) -> BukkitPlatform.toLocation(i, c.getWorld())).forEach((i) -> {
BlockSignal.of(i.getWorld(), i.getBlockX(), i.getBlockY(), i.getBlockZ(), 100);
v.incrementAndGet();
});
}
}
}
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
} else {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
} else {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY_2));
}
});
}
/**
* Runs the body on the thread owning the player, reporting when the hop cannot be scheduled.
*/
private void onPlayerThread(Player player, Runnable body) {
if (player == null) {
return;
}
if (!J.runEntity(player, body)) {
Iris.warn("Could not schedule /iris what on the thread owning " + player.getName() + ".");
}
}
@@ -23,21 +23,24 @@ import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
@SuppressWarnings("ClassCanBeRecord")
public class BukkitBlockEditor implements BlockEditor {
private final World world;
private volatile long last;
public BukkitBlockEditor(World world) {
this.world = world;
this.last = M.ms();
}
@Override
public void set(int x, int y, int z, BlockData d) {
touch();
world.getBlockAt(x, y, z).setBlockData(d, false);
}
@Override
public BlockData get(int x, int y, int z) {
touch();
return world.getBlockAt(x, y, z).getBlockData();
}
@@ -48,11 +51,12 @@ public class BukkitBlockEditor implements BlockEditor {
@Override
public long last() {
return M.ms();
return last;
}
@Override
public void setBiome(int x, int z, Biome b) {
touch();
int minHeight = world.getMinHeight();
int maxHeight = world.getMaxHeight();
for (int y = minHeight; y < maxHeight; y++) {
@@ -62,16 +66,23 @@ public class BukkitBlockEditor implements BlockEditor {
@Override
public void setBiome(int x, int y, int z, Biome b) {
touch();
world.setBiome(x, y, z, b);
}
@Override
public Biome getBiome(int x, int y, int z) {
touch();
return world.getBiome(x, y, z);
}
@Override
public Biome getBiome(int x, int z) {
touch();
return world.getBiome(x, world.getMinHeight(), z);
}
private void touch() {
last = M.ms();
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
@@ -26,72 +27,141 @@ import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH;
public final class BukkitVisionOverlay implements GuiOverlay {
private final Engine engine;
private final AtomicBoolean playerRefreshQueued = new AtomicBoolean();
private volatile List<GuiMarker> playerMarkers = List.of();
public BukkitVisionOverlay(Engine engine) {
this.engine = engine;
}
/**
* Called from the AWT event thread, so it may only hand back the last snapshot
* built by a server thread.
*/
@Override
public List<GuiMarker> players() {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(world)) {
markers.add(GuiMarker.player(player.getName(), player.getLocation().getX(), player.getLocation().getZ()));
queuePlayerRefresh();
return playerMarkers;
}
private void queuePlayerRefresh() {
if (!playerRefreshQueued.compareAndSet(false, true)) {
return;
}
boolean scheduled = J.runGlobal(() -> {
try {
List<GuiMarker> markers = new ArrayList<>();
for (Player player : BukkitWorldBinding.players(engine.getWorld())) {
Location at = player.getLocation();
markers.add(GuiMarker.player(player.getName(), at.getX(), at.getZ()));
}
playerMarkers = List.copyOf(markers);
} finally {
playerRefreshQueued.set(false);
}
});
if (!scheduled) {
playerRefreshQueued.set(false);
}
return markers;
}
@Override
public void requestEntities(Consumer<List<GuiMarker>> sink) {
J.s(() -> {
IrisWorld world = engine.getWorld();
List<GuiMarker> markers = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(world, LivingEntity.class)) {
if (entity instanceof Player) {
continue;
}
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
entity.getHealth(), maxHealth));
J.runGlobal(() -> {
IrisWorld target = engine.getWorld();
World world = BukkitWorldBinding.world(target);
if (world == null) {
sink.accept(List.of());
return;
}
List<LivingEntity> living = new ArrayList<>();
for (LivingEntity entity : BukkitWorldBinding.entities(target, LivingEntity.class)) {
if (!(entity instanceof Player)) {
living.add(entity);
}
}
if (living.isEmpty()) {
sink.accept(List.of());
return;
}
List<GuiMarker> collected = Collections.synchronizedList(new ArrayList<>(living.size()));
AtomicInteger pending = new AtomicInteger(living.size());
Runnable complete = () -> {
if (pending.decrementAndGet() == 0) {
sink.accept(List.copyOf(collected));
}
};
for (LivingEntity entity : living) {
Location at = entity.getLocation();
Runnable read = () -> {
try {
collected.add(marker(entity, at));
} catch (Throwable ignored) {
} finally {
complete.run();
}
};
if (!J.runRegion(world, at.getBlockX() >> 4, at.getBlockZ() >> 4, read)) {
complete.run();
}
}
sink.accept(markers);
});
}
private GuiMarker marker(LivingEntity entity, Location at) {
String label = Form.capitalizeWords(entity.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "));
double maxHealth = 0;
try {
maxHealth = entity.getAttribute(MAX_HEALTH).getValue();
} catch (Throwable ignored) {
}
return GuiMarker.entity(label, at.getX(), at.getY(), at.getZ(), entity.getHealth(), maxHealth);
}
@Override
public void teleport(double worldX, double worldZ) {
IrisWorld world = engine.getWorld();
if (!world.hasPlatformWorld()) {
IrisWorld target = engine.getWorld();
if (!target.hasPlatformWorld()) {
return;
}
J.s(() -> {
List<Player> players = BukkitWorldBinding.players(world);
J.runGlobal(() -> {
List<Player> players = BukkitWorldBinding.players(target);
if (players.isEmpty()) {
return;
}
Player player = players.get(0);
World world = player.getWorld();
int xx = (int) worldX;
int zz = (int) worldZ;
int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1;
player.teleport(new Location(player.getWorld(), xx, yy, zz));
J.runRegion(world, xx >> 4, zz >> 4, () -> {
int yy = world.getHighestBlockYAt(xx, zz) + 1;
Location destination = new Location(world, xx, yy, zz);
J.runEntity(player, () -> BukkitPlatform.teleportAsync(player, destination));
});
});
}
@@ -24,6 +24,7 @@ import art.arcane.iris.core.datapack.DatapackIngestService;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisCodeWorkspace;
import art.arcane.iris.core.service.IrisApiEventSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.tools.WorldMaintenance;
@@ -41,7 +42,7 @@ import org.bukkit.World;
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
@Override
public void refreshWorkspace(Engine engine) {
new IrisProject(engine.getData().getDataFolder()).updateWorkspace();
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
}
@Override
@@ -0,0 +1,45 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
import org.bukkit.Location;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class FellingRun {
final TreeClaim claim;
final TreeCandidate candidate;
final int preservationChance;
final TreeFellerRunHooks runHooks;
final int heldSlot;
final TreeFellerPresentation presentation;
final AtomicBoolean finished = new AtomicBoolean();
final AtomicInteger cursor = new AtomicInteger();
final AtomicInteger processed = new AtomicInteger();
volatile int blocksPerPulse = 1;
volatile int effectStride = 1;
volatile ItemStack expectedTool;
volatile List<TreeMember> work = List.of();
FellingRun(
TreeClaim claim,
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks,
int heldSlot,
Location fallbackLocation
) {
this.claim = claim;
this.candidate = candidate;
this.preservationChance = preservationChance;
this.runHooks = runHooks;
this.heldSlot = heldSlot;
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
this.expectedTool = candidate.tool().clone();
}
}
@@ -279,6 +279,11 @@ public final class IrisEngineSVC implements IrisService {
}
closing.completion().complete(null);
} else {
// A failed close must still stop conflicting with future registrations,
// otherwise the world never regains its maintenance task after a reload.
synchronized (registrationLock) {
closingGenerators.remove(closing);
}
closing.completion().completeExceptionally(failure);
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
private final TreeFellerSVC service;
private final FellingRun run;
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
super(block, player);
this.run = run;
this.service = service;
}
@Override
public boolean routeDrop(Object drop) {
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.UUID;
final class TreeFellerModel {
private TreeFellerModel() {
}
record PendingFell(
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks
) {
PendingFell withAccess(TreeFellerAccess access) {
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
}
}
record TreeCandidate(
TreeContext context,
World world,
Player player,
ItemStack tool,
TreeFellerAccess access,
TreeMarkerTraversal.Position trigger
) {
TreeCandidate withAccess(TreeFellerAccess access) {
return new TreeCandidate(context, world, player, tool, access, trigger);
}
}
record TreeContext(
Engine engine,
String marker,
TreeBlockMaterial expectedMaterial,
int minimumY,
int maximumY
) {
}
record TreeClaim(UUID worldId, String marker) {
}
record ProvenanceSnapshot(
Engine engine,
World world,
int minimumY,
TreeMarkerTraversal.Position position,
String marker,
TreeBlockMaterial material
) {
}
record ChunkPosition(int x, int z) {
}
record TreeMember(
TreeMarkerTraversal.Position position,
boolean log,
TreeBlockMaterial expectedMaterial,
int erosionOrder
) {
}
record DamageReservation(
ItemStack toolForDrops,
boolean charged,
boolean broke,
boolean logCostReserved
) {
}
}
@@ -5,27 +5,22 @@ import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.api.tree.TreeFellerOptions;
import art.arcane.iris.api.tree.TreeFellerRunHooks;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.core.service.TreeFellerModel.PendingFell;
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeClaim;
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.ExperienceOrb;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -36,40 +31,31 @@ import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.ServicePriority;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
private static final String PERMISSION = "iris.treefeller";
private final AtomicBoolean serviceEnabled = new AtomicBoolean();
private final Map<BlockBreakEvent, PendingFell> pending = Collections.synchronizedMap(new IdentityHashMap<>());
private final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
final Set<BlockBreakEvent> managedEvents = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>())
);
private final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
private final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
final Set<TreeClaim> activeClaims = ConcurrentHashMap.newKeySet();
final Map<UUID, Set<FellingRun>> activeRuns = new ConcurrentHashMap<>();
private final Map<Engine, TreeDefinitionIndex> definitions = Collections.synchronizedMap(new WeakHashMap<>());
private final TreeProvenance provenance = new TreeProvenance(definitions);
private final TreeFellingRunner runner = new TreeFellingRunner(this, provenance);
@Override
public void onEnable() {
@@ -92,7 +78,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
managedEvents.clear();
for (Set<FellingRun> runs : activeRuns.values()) {
for (FellingRun run : runs) {
finish(run);
runner.finish(run);
}
}
activeRuns.clear();
@@ -105,8 +91,12 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
if (!serviceEnabled.get()
|| event == null
|| options == null
|| event.isCancelled()
|| isManagedBreak(event)) {
|| event.isCancelled()) {
return false;
}
// An event that already carries a pending fell may still be upgraded to
// INTEGRATION_OVERRIDE; only internal probes are refused outright.
if (!pending.containsKey(event) && isManagedBreak(event)) {
return false;
}
if (!canUse(event.getPlayer(), options.access())) {
@@ -115,7 +105,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
TreeCandidate candidate;
try {
candidate = resolveCandidate(event.getBlock(), event.getPlayer());
candidate = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
} catch (Throwable error) {
IrisLogging.reportError("Failed to resolve an Iris tree-feller request.", error);
return false;
@@ -153,7 +143,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
return false;
}
try {
return resolveTreeContext(block) != null;
return provenance.resolveTreeContext(block) != null;
} catch (Throwable error) {
IrisLogging.reportError("Failed to inspect Iris tree provenance.", error);
return false;
@@ -186,7 +176,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
TreeCandidate current;
try {
current = resolveCandidate(event.getBlock(), event.getPlayer());
current = provenance.resolveCandidate(event.getBlock(), event.getPlayer());
} catch (Throwable error) {
IrisLogging.reportError("Failed to finalize an Iris tree-feller request.", error);
deferSuccessfulBreakCleanup(event);
@@ -224,38 +214,38 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
activeRuns.computeIfAbsent(event.getPlayer().getUniqueId(), ignored -> ConcurrentHashMap.newKeySet()).add(run);
notifyActivationAccepted(run.runHooks);
run.presentation.activate(event.getBlock());
discover(run);
runner.discover(run);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenSneakingStops(PlayerToggleSneakEvent event) {
if (!event.isSneaking()) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenHeldSlotChanges(PlayerItemHeldEvent event) {
if (event.getNewSlot() != event.getPreviousSlot()) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void haltWhenHandsSwap(PlayerSwapHandItemsEvent event) {
finishRuns(event.getPlayer().getUniqueId());
runner.finishRuns(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void clearPlacedProvenance(BlockPlaceEvent event) {
ProvenanceSnapshot snapshot = captureProvenance(event.getBlockPlaced());
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlockPlaced());
if (snapshot == null) {
return;
}
Location location = event.getBlockPlaced().getLocation();
Runnable cleanup = () -> {
if (!event.isCancelled()) {
clearProvenanceIfMatching(snapshot);
provenance.clearProvenanceIfMatching(snapshot);
}
};
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
@@ -263,6 +253,10 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
boolean isServiceEnabled() {
return serviceEnabled.get();
}
private boolean canUse(Player player, TreeFellerAccess access) {
if (access == TreeFellerAccess.INTEGRATION_OVERRIDE) {
return true;
@@ -287,616 +281,19 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private TreeCandidate resolveCandidate(Block block, Player player) {
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
return null;
}
if (!Tag.LOGS.isTagged(block.getType())) {
return null;
}
ItemStack tool = player.getInventory().getItemInMainHand();
if (!isAxe(tool)) {
return null;
}
TreeContext context = resolveTreeContext(block);
if (context == null) {
return null;
}
return new TreeCandidate(
context,
block.getWorld(),
player,
tool.clone(),
TreeFellerAccess.STANDALONE,
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
);
}
private TreeContext resolveTreeContext(Block block) {
if (block.getType().isAir()) {
return null;
}
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
World world = block.getWorld();
int minimumY = world.getMinHeight();
int maximumY = world.getMaxHeight();
int relativeY = block.getY() - minimumY;
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return null;
}
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
block.getX(),
relativeY,
block.getZ(),
TreeBlockMaterial.class
);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
return null;
}
if (expected == null
&& !decoded.objectKey().startsWith("trees/")
&& !definitionIndex(engine).isTreeMarker(marker)) {
return null;
}
return new TreeContext(engine, marker, expected, minimumY, maximumY);
}
private TreeDefinitionIndex definitionIndex(Engine engine) {
synchronized (definitions) {
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
}
}
private void discover(FellingRun run) {
J.a(() -> {
if (run.candidate.context().engine().isClosed()) {
finish(run);
return;
}
try {
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
run.candidate.trigger(),
run.candidate.context().marker(),
run.candidate.context().minimumY(),
run.candidate.context().maximumY(),
(x, y, z) -> markerAt(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
x,
y,
z
)
);
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
preflight(run, positions, discovery.complete());
} catch (Throwable error) {
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
preflight(run, List.of(run.candidate.trigger()), false);
}
});
}
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
if (grouped.isEmpty()) {
finish(run);
return;
}
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
for (int index = 0; index < positions.size(); index++) {
erosionOrder.put(positions.get(index), index);
}
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
AtomicBoolean failed = new AtomicBoolean();
AtomicInteger remaining = new AtomicInteger(grouped.size());
AtomicBoolean completed = new AtomicBoolean();
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
ChunkPosition chunk = entry.getKey();
Runnable task = () -> {
try {
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
failed.set(true);
return;
}
for (TreeMarkerTraversal.Position position : entry.getValue()) {
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
if (member != null) {
members.add(member);
}
}
} catch (Throwable error) {
failed.set(true);
IrisLogging.reportError(
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
error
);
} finally {
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
};
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
failed.set(true);
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
}
}
static List<TreeMarkerTraversal.Position> positionsForFelling(
TreeMarkerTraversal.Discovery discovery,
TreeMarkerTraversal.Position trigger
) {
return discovery.complete() ? discovery.members() : List.of(trigger);
}
private void completePreflightGroup(
FellingRun run,
List<TreeMember> members,
AtomicBoolean failed,
AtomicInteger remaining,
AtomicBoolean completed,
boolean allowFallback
) {
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
return;
}
if (failed.get() && allowFallback) {
preflight(run, List.of(run.candidate.trigger()), false);
return;
}
if (failed.get()) {
finish(run);
return;
}
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
finish(run);
return;
}
run.work = ordered;
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
processNext(run);
}
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
World world = run.candidate.world();
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (block.getType().isAir()) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
}
private List<TreeMember> orderMembers(
TreeMarkerTraversal.Position trigger,
Collection<TreeMember> discovered
) {
Comparator<TreeMember> erosionOrder = Comparator
.comparingInt(TreeMember::erosionOrder)
.thenComparingInt(member -> member.position().y())
.thenComparingInt(member -> member.position().x())
.thenComparingInt(member -> member.position().z());
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
return List.of();
}
return ordered;
}
private void processNext(FellingRun run) {
if (run.finished.get()) {
return;
}
int index = run.cursor.getAndIncrement();
if (index >= run.work.size()) {
finish(run);
return;
}
TreeMember member = run.work.get(index);
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
Runnable task = () -> runTask(
run,
"Failed to prepare an Iris tree-feller block.",
() -> prepareBreak(run, member)
);
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
}
}
private void prepareBreak(FellingRun run, TreeMember member) {
Block block = liveMemberBlock(run, member);
if (block == null) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
if (!member.log()) {
runMutationTask(
run,
member,
new DamageReservation(run.expectedTool.clone(), false, false, false),
new AtomicBoolean()
);
return;
}
Runnable task = () -> runTask(
run,
"Failed to reserve Iris tree-feller tool durability.",
() -> reserveDamage(run, member)
);
if (!J.runEntity(run.candidate.player(), task)) {
finish(run);
}
}
private void reserveDamage(FellingRun run, TreeMember member) {
Player player = run.candidate.player();
if (!isRunControlActive(run, player)) {
finish(run);
return;
}
PlayerInventory inventory = player.getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
if (current == null
|| inventory.getHeldItemSlot() != run.heldSlot
|| !current.isSimilar(run.expectedTool)
|| !isAxe(current)) {
finish(run);
return;
}
if (!reserveLogCost(run)) {
finish(run);
return;
}
ItemStack before = current.clone();
ItemMeta meta = current.getItemMeta();
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
return;
}
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
refundAndFinish(run, new DamageReservation(before, false, false, true));
return;
}
int nextDamage = damageable.getDamage() + 1;
boolean broke = nextDamage >= current.getType().getMaxDurability();
if (broke) {
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
run.expectedTool = new ItemStack(Material.AIR);
} else {
damageable.setDamage(nextDamage);
current.setItemMeta(meta);
inventory.setItem(run.heldSlot, current);
run.expectedTool = current.clone();
}
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
}
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
TreeMarkerTraversal.Position position = member.position();
AtomicBoolean mutationSucceeded = new AtomicBoolean();
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
boolean scheduled;
try {
scheduled = J.runRegion(
run.candidate.world(),
position.x() >> 4,
position.z() >> 4,
task
);
} catch (Throwable error) {
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
refundAndFinish(run, reservation);
return;
}
if (!scheduled) {
refundAndFinish(run, reservation);
}
}
private void runMutationTask(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
if (run.finished.get()) {
refundAndFinish(run, reservation);
return;
}
try {
probeAndMutate(run, member, reservation, mutationSucceeded);
} catch (Throwable error) {
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
if (mutationSucceeded.get()) {
finish(run);
} else {
refundAndFinish(run, reservation);
}
}
}
private void probeAndMutate(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
Block block = liveMemberBlock(run, member);
if (block == null) {
refundAndFinish(run, reservation);
return;
}
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run);
managedEvents.add(probe);
try {
Bukkit.getPluginManager().callEvent(probe);
} catch (Throwable error) {
probe.setCancelled(true);
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
} finally {
managedEvents.remove(probe);
}
try {
if (probe.isCancelled()) {
if (reservation.charged() || reservation.logCostReserved()) {
refundAndFinish(run, reservation);
} else if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
block = liveMemberBlock(run, member);
if (run.finished.get() || block == null) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
BlockData visualData = block.getBlockData().clone();
List<ItemStack> vanillaDrops = probe.isDropItems()
? block.getDrops(reservation.toolForDrops()).stream()
.map(ItemStack::clone)
.toList()
: List.of();
block.setType(Material.AIR, false);
if (!block.getType().isAir()) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
mutationSucceeded.set(true);
run.presentation.erode(
source,
visualData,
member.erosionOrder(),
run.processed.get(),
run.blocksPerPulse,
run.effectStride,
run.work.size()
);
clearProvenance(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
member.position()
);
routeDrops(run, vanillaDrops, source);
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
dropExperience(source, probe.getExpToDrop());
}
if (reservation.logCostReserved()) {
completeLogCost(run, reservation);
return;
}
completeSuccessfulMutation(run, reservation);
} catch (RuntimeException | Error error) {
if (!mutationSucceeded.get()) {
probe.setCancelled(true);
}
throw error;
}
}
private Block liveMemberBlock(FellingRun run, TreeMember member) {
World world = run.candidate.world();
TreeMarkerTraversal.Position position = member.position();
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
return null;
}
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
TreeBlockMaterial expected = materialAt(context.engine(), context.minimumY(), position);
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (expected != null && !matchesExpectedMaterial(block, expected)) {
clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return block;
}
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
if (!reservation.charged() && !reservation.logCostReserved()) {
finish(run);
return;
}
if (!J.runEntity(run.candidate.player(), () -> {
if (reservation.charged()) {
PlayerInventory inventory = run.candidate.player().getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
boolean currentMatches = expectedAir
? current == null || current.getType() == Material.AIR
: current != null && current.isSimilar(run.expectedTool);
if (currentMatches) {
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
run.expectedTool = reservation.toolForDrops().clone();
}
}
if (reservation.logCostReserved()) {
refundLogCost(run);
}
finish(run);
})) {
finish(run);
}
}
private boolean isRunControlActive(FellingRun run, Player player) {
return player.isOnline()
&& player.getGameMode() == GameMode.SURVIVAL
&& player.isSneaking()
&& player.getWorld().equals(run.candidate.world());
}
private boolean reserveLogCost(FellingRun run) {
try {
return run.runHooks.reserveLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
return false;
}
}
private void completeLogCost(FellingRun run, DamageReservation reservation) {
if (!J.runEntity(run.candidate.player(), () -> {
try {
run.runHooks.commitLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
finish(run);
return;
}
completeSuccessfulMutation(run, reservation);
})) {
finish(run);
}
}
private void refundLogCost(FellingRun run) {
try {
run.runHooks.refundLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
}
}
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
if (reservation.broke()) {
finish(run);
return;
}
continueRun(run);
}
private void continueRun(FellingRun run) {
int processed = run.processed.incrementAndGet();
if (processed % run.blocksPerPulse == 0) {
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
} else {
processNext(run);
}
}
private void runTask(FellingRun run, String context, Runnable task) {
if (run.finished.get() || !serviceEnabled.get()) {
finish(run);
return;
}
try {
task.run();
} catch (Throwable error) {
IrisLogging.reportError(context, error);
finish(run);
}
}
private void finish(FellingRun run) {
if (run.finished.compareAndSet(false, true)) {
activeClaims.remove(run.claim);
Set<FellingRun> runs = activeRuns.get(run.candidate.player().getUniqueId());
if (runs != null) {
runs.remove(run);
if (runs.isEmpty()) {
activeRuns.remove(run.candidate.player().getUniqueId(), runs);
}
}
run.presentation.finish();
}
}
private void finishRuns(UUID playerId) {
Set<FellingRun> runs = activeRuns.get(playerId);
if (runs == null) {
return;
}
for (FellingRun run : List.copyOf(runs)) {
finish(run);
}
}
private void releaseManagedLater(BlockBreakEvent event) {
J.s(() -> managedEvents.remove(event), 1);
}
private void deferSuccessfulBreakCleanup(BlockBreakEvent event) {
ProvenanceSnapshot snapshot = captureProvenance(event.getBlock());
ProvenanceSnapshot snapshot = provenance.captureProvenance(event.getBlock());
if (snapshot == null) {
return;
}
Location location = event.getBlock().getLocation();
Runnable cleanup = () -> {
if (!event.isCancelled()) {
clearProvenanceIfMatching(snapshot);
provenance.clearProvenanceIfMatching(snapshot);
}
};
if (!J.runAt(location, cleanup, 1) && !J.isFolia()) {
@@ -904,77 +301,11 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
List<TreeMarkerTraversal.Position> positions
) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
for (TreeMarkerTraversal.Position position : positions) {
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
}
return grouped;
}
private String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
}
private String markerAt(Engine engine, int minimumY, int x, int y, int z) {
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
}
private TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return engine.getMantle().getMantle().get(
position.x(),
position.y() - minimumY,
position.z(),
TreeBlockMaterial.class
);
}
private ProvenanceSnapshot captureProvenance(Block block) {
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
block.getX(),
block.getY(),
block.getZ()
);
int minimumY = block.getWorld().getMinHeight();
String marker = markerAt(engine, minimumY, position);
TreeBlockMaterial material = materialAt(engine, minimumY, position);
if (marker == null && material == null) {
return null;
}
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
}
private void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
}
}
private void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
int relativeY = position.y() - minimumY;
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
}
private boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
return expected.matches(block.getBlockData().getAsString());
}
private boolean isAxe(ItemStack item) {
static boolean isAxe(ItemStack item) {
return item != null && item.getType() != Material.AIR && item.getType().name().endsWith("_AXE");
}
private void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
void routeDrops(FellingRun run, List<ItemStack> drops, Location source) {
World world = source.getWorld();
if (world == null) {
return;
@@ -986,7 +317,7 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
}
}
private void dropExperience(Location location, int experience) {
void dropExperience(Location location, int experience) {
if (experience <= 0 || location.getWorld() == null) {
return;
}
@@ -996,115 +327,4 @@ public class TreeFellerSVC implements IrisService, IrisTreeFellerService {
);
orb.setExperience(experience);
}
private record PendingFell(
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks
) {
private PendingFell withAccess(TreeFellerAccess access) {
return new PendingFell(candidate.withAccess(access), preservationChance, runHooks);
}
}
private record TreeCandidate(
TreeContext context,
World world,
Player player,
ItemStack tool,
TreeFellerAccess access,
TreeMarkerTraversal.Position trigger
) {
private TreeCandidate withAccess(TreeFellerAccess access) {
return new TreeCandidate(context, world, player, tool, access, trigger);
}
}
private record TreeContext(
Engine engine,
String marker,
TreeBlockMaterial expectedMaterial,
int minimumY,
int maximumY
) {
}
private record TreeClaim(UUID worldId, String marker) {
}
private record ProvenanceSnapshot(
Engine engine,
World world,
int minimumY,
TreeMarkerTraversal.Position position,
String marker,
TreeBlockMaterial material
) {
}
private record ChunkPosition(int x, int z) {
}
private record TreeMember(
TreeMarkerTraversal.Position position,
boolean log,
TreeBlockMaterial expectedMaterial,
int erosionOrder
) {
}
private record DamageReservation(
ItemStack toolForDrops,
boolean charged,
boolean broke,
boolean logCostReserved
) {
}
private final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
private final FellingRun run;
private RoutedBlockBreakEvent(Block block, Player player, FellingRun run) {
super(block, player);
this.run = run;
}
@Override
public boolean routeDrop(Object drop) {
return serviceEnabled.get() && run.presentation.routeDrop(drop);
}
}
private static final class FellingRun {
private final TreeClaim claim;
private final TreeCandidate candidate;
private final int preservationChance;
private final TreeFellerRunHooks runHooks;
private final int heldSlot;
private final TreeFellerPresentation presentation;
private final AtomicBoolean finished = new AtomicBoolean();
private final AtomicInteger cursor = new AtomicInteger();
private final AtomicInteger processed = new AtomicInteger();
private volatile int blocksPerPulse = 1;
private volatile int effectStride = 1;
private volatile ItemStack expectedTool;
private volatile List<TreeMember> work = List.of();
private FellingRun(
TreeClaim claim,
TreeCandidate candidate,
int preservationChance,
TreeFellerRunHooks runHooks,
int heldSlot,
Location fallbackLocation
) {
this.claim = claim;
this.candidate = candidate;
this.preservationChance = preservationChance;
this.runHooks = runHooks;
this.heldSlot = heldSlot;
this.presentation = new TreeFellerPresentation(candidate.player(), candidate.world(), fallbackLocation);
this.expectedTool = candidate.tool().clone();
}
}
}
@@ -0,0 +1,590 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.service.TreeFellerModel.ChunkPosition;
import art.arcane.iris.core.service.TreeFellerModel.DamageReservation;
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
import art.arcane.iris.core.service.TreeFellerModel.TreeMember;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
final class TreeFellingRunner {
private final TreeFellerSVC service;
private final TreeProvenance provenance;
TreeFellingRunner(TreeFellerSVC service, TreeProvenance provenance) {
this.service = service;
this.provenance = provenance;
}
void discover(FellingRun run) {
J.a(() -> {
if (run.candidate.context().engine().isClosed()) {
finish(run);
return;
}
try {
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
run.candidate.trigger(),
run.candidate.context().marker(),
run.candidate.context().minimumY(),
run.candidate.context().maximumY(),
(x, y, z) -> provenance.markerAt(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
x,
y,
z
)
);
List<TreeMarkerTraversal.Position> positions = positionsForFelling(discovery, run.candidate.trigger());
preflight(run, positions, discovery.complete());
} catch (Throwable error) {
IrisLogging.reportError("Failed to discover an Iris tree for felling.", error);
preflight(run, List.of(run.candidate.trigger()), false);
}
});
}
private void preflight(FellingRun run, List<TreeMarkerTraversal.Position> positions, boolean allowFallback) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = groupByChunk(positions);
if (grouped.isEmpty()) {
finish(run);
return;
}
Map<TreeMarkerTraversal.Position, Integer> erosionOrder = new HashMap<>(positions.size());
for (int index = 0; index < positions.size(); index++) {
erosionOrder.put(positions.get(index), index);
}
List<TreeMember> members = Collections.synchronizedList(new ArrayList<>());
AtomicBoolean failed = new AtomicBoolean();
AtomicInteger remaining = new AtomicInteger(grouped.size());
AtomicBoolean completed = new AtomicBoolean();
for (Map.Entry<ChunkPosition, List<TreeMarkerTraversal.Position>> entry : grouped.entrySet()) {
ChunkPosition chunk = entry.getKey();
Runnable task = () -> {
try {
if (!run.candidate.world().isChunkLoaded(chunk.x(), chunk.z())) {
failed.set(true);
return;
}
for (TreeMarkerTraversal.Position position : entry.getValue()) {
TreeMember member = inspectMember(run, position, erosionOrder.getOrDefault(position, Integer.MAX_VALUE));
if (member != null) {
members.add(member);
}
}
} catch (Throwable error) {
failed.set(true);
IrisLogging.reportError(
"Failed to preflight an Iris tree-feller chunk at " + chunk.x() + "," + chunk.z() + ".",
error
);
} finally {
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
};
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
failed.set(true);
completePreflightGroup(run, members, failed, remaining, completed, allowFallback);
}
}
}
static List<TreeMarkerTraversal.Position> positionsForFelling(
TreeMarkerTraversal.Discovery discovery,
TreeMarkerTraversal.Position trigger
) {
return discovery.complete() ? discovery.members() : List.of(trigger);
}
private void completePreflightGroup(
FellingRun run,
List<TreeMember> members,
AtomicBoolean failed,
AtomicInteger remaining,
AtomicBoolean completed,
boolean allowFallback
) {
if (remaining.decrementAndGet() != 0 || !completed.compareAndSet(false, true)) {
return;
}
if (failed.get() && allowFallback) {
preflight(run, List.of(run.candidate.trigger()), false);
return;
}
if (failed.get()) {
finish(run);
return;
}
List<TreeMember> ordered = orderMembers(run.candidate.trigger(), members);
if (ordered.isEmpty() || !ordered.getFirst().position().equals(run.candidate.trigger())) {
finish(run);
return;
}
run.work = ordered;
run.blocksPerPulse = TreeFellerPresentation.blocksPerPulse(ordered.size());
run.effectStride = TreeFellerPresentation.effectStride(run.blocksPerPulse);
processNext(run);
}
private TreeMember inspectMember(FellingRun run, TreeMarkerTraversal.Position position, int erosionOrder) {
World world = run.candidate.world();
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (block.getType().isAir()) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return new TreeMember(position, Tag.LOGS.isTagged(block.getType()), expected, erosionOrder);
}
private List<TreeMember> orderMembers(
TreeMarkerTraversal.Position trigger,
Collection<TreeMember> discovered
) {
Comparator<TreeMember> erosionOrder = Comparator
.comparingInt(TreeMember::erosionOrder)
.thenComparingInt(member -> member.position().y())
.thenComparingInt(member -> member.position().x())
.thenComparingInt(member -> member.position().z());
List<TreeMember> ordered = discovered.stream().sorted(erosionOrder).toList();
if (ordered.isEmpty() || !ordered.getFirst().position().equals(trigger)) {
return List.of();
}
return ordered;
}
private void processNext(FellingRun run) {
if (run.finished.get()) {
return;
}
int index = run.cursor.getAndIncrement();
if (index >= run.work.size()) {
finish(run);
return;
}
TreeMember member = run.work.get(index);
ChunkPosition chunk = new ChunkPosition(member.position().x() >> 4, member.position().z() >> 4);
Runnable task = () -> runTask(
run,
"Failed to prepare an Iris tree-feller block.",
() -> prepareBreak(run, member)
);
if (!J.runRegion(run.candidate.world(), chunk.x(), chunk.z(), task)) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
}
}
private void prepareBreak(FellingRun run, TreeMember member) {
Block block = liveMemberBlock(run, member);
if (block == null) {
if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
if (!member.log()) {
runMutationTask(
run,
member,
new DamageReservation(run.expectedTool.clone(), false, false, false),
new AtomicBoolean()
);
return;
}
Runnable task = () -> runTask(
run,
"Failed to reserve Iris tree-feller tool durability.",
() -> reserveDamage(run, member)
);
if (!J.runEntity(run.candidate.player(), task)) {
finish(run);
}
}
private void reserveDamage(FellingRun run, TreeMember member) {
Player player = run.candidate.player();
if (!isRunControlActive(run, player)) {
finish(run);
return;
}
PlayerInventory inventory = player.getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
if (current == null
|| inventory.getHeldItemSlot() != run.heldSlot
|| !current.isSimilar(run.expectedTool)
|| !TreeFellerSVC.isAxe(current)) {
finish(run);
return;
}
if (!reserveLogCost(run)) {
finish(run);
return;
}
ItemStack before = current.clone();
ItemMeta meta = current.getItemMeta();
if (meta.isUnbreakable() || ThreadLocalRandom.current().nextInt(100) < run.preservationChance) {
scheduleMutation(run, member, new DamageReservation(before, false, false, true));
return;
}
if (!(meta instanceof Damageable damageable) || current.getType().getMaxDurability() <= 0) {
refundAndFinish(run, new DamageReservation(before, false, false, true));
return;
}
int nextDamage = damageable.getDamage() + 1;
boolean broke = nextDamage >= current.getType().getMaxDurability();
if (broke) {
inventory.setItem(run.heldSlot, new ItemStack(Material.AIR));
run.expectedTool = new ItemStack(Material.AIR);
} else {
damageable.setDamage(nextDamage);
current.setItemMeta(meta);
inventory.setItem(run.heldSlot, current);
run.expectedTool = current.clone();
}
scheduleMutation(run, member, new DamageReservation(before, true, broke, true));
}
private void scheduleMutation(FellingRun run, TreeMember member, DamageReservation reservation) {
TreeMarkerTraversal.Position position = member.position();
AtomicBoolean mutationSucceeded = new AtomicBoolean();
Runnable task = () -> runMutationTask(run, member, reservation, mutationSucceeded);
boolean scheduled;
try {
scheduled = J.runRegion(
run.candidate.world(),
position.x() >> 4,
position.z() >> 4,
task
);
} catch (Throwable error) {
IrisLogging.reportError("Failed to schedule an Iris tree-feller block removal.", error);
refundAndFinish(run, reservation);
return;
}
if (!scheduled) {
refundAndFinish(run, reservation);
}
}
private void runMutationTask(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
if (run.finished.get()) {
refundAndFinish(run, reservation);
return;
}
try {
probeAndMutate(run, member, reservation, mutationSucceeded);
} catch (Throwable error) {
IrisLogging.reportError("Failed to remove an Iris tree-feller block.", error);
if (mutationSucceeded.get()) {
finish(run);
} else {
refundAndFinish(run, reservation);
}
}
}
private void probeAndMutate(
FellingRun run,
TreeMember member,
DamageReservation reservation,
AtomicBoolean mutationSucceeded
) {
Block block = liveMemberBlock(run, member);
if (block == null) {
refundAndFinish(run, reservation);
return;
}
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
service.managedEvents.add(probe);
try {
Bukkit.getPluginManager().callEvent(probe);
} catch (Throwable error) {
probe.setCancelled(true);
IrisLogging.reportError("Failed to dispatch an Iris tree-feller block probe.", error);
} finally {
service.managedEvents.remove(probe);
}
try {
if (probe.isCancelled()) {
if (reservation.charged() || reservation.logCostReserved()) {
refundAndFinish(run, reservation);
} else if (member.log()) {
finish(run);
} else {
continueRun(run);
}
return;
}
block = liveMemberBlock(run, member);
if (run.finished.get() || block == null) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
Location source = block.getLocation().clone().add(0.5D, 0.5D, 0.5D);
BlockData visualData = block.getBlockData().clone();
List<ItemStack> vanillaDrops = probe.isDropItems()
? block.getDrops(reservation.toolForDrops()).stream()
.map(ItemStack::clone)
.toList()
: List.of();
block.setType(Material.AIR, false);
if (!block.getType().isAir()) {
probe.setCancelled(true);
refundAndFinish(run, reservation);
return;
}
mutationSucceeded.set(true);
run.presentation.erode(
source,
visualData,
member.erosionOrder(),
run.processed.get(),
run.blocksPerPulse,
run.effectStride,
run.work.size()
);
provenance.clearProvenance(
run.candidate.context().engine(),
run.candidate.context().minimumY(),
member.position()
);
service.routeDrops(run, vanillaDrops, source);
if (!run.presentation.routeExperience(probe.getExpToDrop())) {
service.dropExperience(source, probe.getExpToDrop());
}
if (reservation.logCostReserved()) {
completeLogCost(run, reservation);
return;
}
completeSuccessfulMutation(run, reservation);
} catch (RuntimeException | Error error) {
if (!mutationSucceeded.get()) {
probe.setCancelled(true);
}
throw error;
}
}
private Block liveMemberBlock(FellingRun run, TreeMember member) {
World world = run.candidate.world();
TreeMarkerTraversal.Position position = member.position();
if (!world.isChunkLoaded(position.x() >> 4, position.z() >> 4)) {
return null;
}
Block block = world.getBlockAt(position.x(), position.y(), position.z());
TreeContext context = run.candidate.context();
if (!context.marker().equals(provenance.markerAt(context.engine(), context.minimumY(), position))) {
return null;
}
if (block.getType().isAir() || member.log() != Tag.LOGS.isTagged(block.getType())) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
TreeBlockMaterial expected = provenance.materialAt(context.engine(), context.minimumY(), position);
if (member.expectedMaterial() != null && !member.expectedMaterial().equals(expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
if (expected != null && !provenance.matchesExpectedMaterial(block, expected)) {
provenance.clearProvenance(context.engine(), context.minimumY(), position);
return null;
}
return block;
}
private void refundAndFinish(FellingRun run, DamageReservation reservation) {
if (!reservation.charged() && !reservation.logCostReserved()) {
finish(run);
return;
}
if (!J.runEntity(run.candidate.player(), () -> {
if (reservation.charged()) {
PlayerInventory inventory = run.candidate.player().getInventory();
ItemStack current = inventory.getItem(run.heldSlot);
boolean expectedAir = run.expectedTool.getType() == Material.AIR;
boolean currentMatches = expectedAir
? current == null || current.getType() == Material.AIR
: current != null && current.isSimilar(run.expectedTool);
if (currentMatches) {
inventory.setItem(run.heldSlot, reservation.toolForDrops().clone());
run.expectedTool = reservation.toolForDrops().clone();
}
}
if (reservation.logCostReserved()) {
refundLogCost(run);
}
finish(run);
})) {
finish(run);
}
}
private boolean isRunControlActive(FellingRun run, Player player) {
return player.isOnline()
&& player.getGameMode() == GameMode.SURVIVAL
&& player.isSneaking()
&& player.getWorld().equals(run.candidate.world());
}
private boolean reserveLogCost(FellingRun run) {
try {
return run.runHooks.reserveLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost reservation failed.", error);
return false;
}
}
private void completeLogCost(FellingRun run, DamageReservation reservation) {
if (!J.runEntity(run.candidate.player(), () -> {
try {
run.runHooks.commitLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost commit failed.", error);
finish(run);
return;
}
completeSuccessfulMutation(run, reservation);
})) {
finish(run);
}
}
private void refundLogCost(FellingRun run) {
try {
run.runHooks.refundLogCost();
} catch (Throwable error) {
IrisLogging.reportError("An Iris tree-feller integration log-cost refund failed.", error);
}
}
private void completeSuccessfulMutation(FellingRun run, DamageReservation reservation) {
if (reservation.broke()) {
finish(run);
return;
}
continueRun(run);
}
private void continueRun(FellingRun run) {
int processed = run.processed.incrementAndGet();
if (processed % run.blocksPerPulse == 0) {
J.s(() -> runTask(run, "Failed to continue an Iris tree-feller run.", () -> processNext(run)), 1);
} else {
processNext(run);
}
}
private void runTask(FellingRun run, String context, Runnable task) {
if (run.finished.get() || !service.isServiceEnabled()) {
finish(run);
return;
}
try {
task.run();
} catch (Throwable error) {
IrisLogging.reportError(context, error);
finish(run);
}
}
void finish(FellingRun run) {
if (run.finished.compareAndSet(false, true)) {
service.activeClaims.remove(run.claim);
Set<FellingRun> runs = service.activeRuns.get(run.candidate.player().getUniqueId());
if (runs != null) {
runs.remove(run);
if (runs.isEmpty()) {
service.activeRuns.remove(run.candidate.player().getUniqueId(), runs);
}
}
run.presentation.finish();
}
}
void finishRuns(UUID playerId) {
Set<FellingRun> runs = service.activeRuns.get(playerId);
if (runs == null) {
return;
}
for (FellingRun run : List.copyOf(runs)) {
finish(run);
}
}
private Map<ChunkPosition, List<TreeMarkerTraversal.Position>> groupByChunk(
List<TreeMarkerTraversal.Position> positions
) {
Map<ChunkPosition, List<TreeMarkerTraversal.Position>> grouped = new LinkedHashMap<>();
for (TreeMarkerTraversal.Position position : positions) {
ChunkPosition chunk = new ChunkPosition(position.x() >> 4, position.z() >> 4);
grouped.computeIfAbsent(chunk, ignored -> new ArrayList<>()).add(position);
}
return grouped;
}
}
@@ -0,0 +1,151 @@
package art.arcane.iris.core.service;
import art.arcane.iris.api.tree.TreeFellerAccess;
import art.arcane.iris.core.service.TreeFellerModel.ProvenanceSnapshot;
import art.arcane.iris.core.service.TreeFellerModel.TreeCandidate;
import art.arcane.iris.core.service.TreeFellerModel.TreeContext;
import art.arcane.iris.core.service.tree.TreeDefinitionIndex;
import art.arcane.iris.core.service.tree.TreeMarkerTraversal;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.GameMode;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Map;
import java.util.Objects;
final class TreeProvenance {
private final Map<Engine, TreeDefinitionIndex> definitions;
TreeProvenance(Map<Engine, TreeDefinitionIndex> definitions) {
this.definitions = definitions;
}
TreeCandidate resolveCandidate(Block block, Player player) {
if (player.getGameMode() != GameMode.SURVIVAL || !player.isSneaking()) {
return null;
}
if (!Tag.LOGS.isTagged(block.getType())) {
return null;
}
ItemStack tool = player.getInventory().getItemInMainHand();
if (!TreeFellerSVC.isAxe(tool)) {
return null;
}
TreeContext context = resolveTreeContext(block);
if (context == null) {
return null;
}
return new TreeCandidate(
context,
block.getWorld(),
player,
tool.clone(),
TreeFellerAccess.STANDALONE,
new TreeMarkerTraversal.Position(block.getX(), block.getY(), block.getZ())
);
}
TreeContext resolveTreeContext(Block block) {
if (block.getType().isAir()) {
return null;
}
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
World world = block.getWorld();
int minimumY = world.getMinHeight();
int maximumY = world.getMaxHeight();
int relativeY = block.getY() - minimumY;
String marker = markerAt(engine, minimumY, block.getX(), block.getY(), block.getZ());
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return null;
}
TreeBlockMaterial expected = engine.getMantle().getMantle().get(
block.getX(),
relativeY,
block.getZ(),
TreeBlockMaterial.class
);
if (expected != null && !matchesExpectedMaterial(block, expected)) {
return null;
}
if (expected == null
&& !decoded.objectKey().startsWith("trees/")
&& !definitionIndex(engine).isTreeMarker(marker)) {
return null;
}
return new TreeContext(engine, marker, expected, minimumY, maximumY);
}
TreeDefinitionIndex definitionIndex(Engine engine) {
synchronized (definitions) {
return definitions.computeIfAbsent(engine, TreeDefinitionIndex::build);
}
}
String markerAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return markerAt(engine, minimumY, position.x(), position.y(), position.z());
}
String markerAt(Engine engine, int minimumY, int x, int y, int z) {
return engine.getMantle().getMantle().get(x, y - minimumY, z, String.class);
}
TreeBlockMaterial materialAt(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
return engine.getMantle().getMantle().get(
position.x(),
position.y() - minimumY,
position.z(),
TreeBlockMaterial.class
);
}
ProvenanceSnapshot captureProvenance(Block block) {
PlatformChunkGenerator access = IrisToolbelt.access(block.getWorld());
if (access == null || access.getEngine() == null) {
return null;
}
Engine engine = access.getEngine();
TreeMarkerTraversal.Position position = new TreeMarkerTraversal.Position(
block.getX(),
block.getY(),
block.getZ()
);
int minimumY = block.getWorld().getMinHeight();
String marker = markerAt(engine, minimumY, position);
TreeBlockMaterial material = materialAt(engine, minimumY, position);
if (marker == null && material == null) {
return null;
}
return new ProvenanceSnapshot(engine, block.getWorld(), minimumY, position, marker, material);
}
void clearProvenanceIfMatching(ProvenanceSnapshot snapshot) {
String marker = markerAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
TreeBlockMaterial material = materialAt(snapshot.engine(), snapshot.minimumY(), snapshot.position());
if (Objects.equals(snapshot.marker(), marker) && Objects.equals(snapshot.material(), material)) {
clearProvenance(snapshot.engine(), snapshot.minimumY(), snapshot.position());
}
}
void clearProvenance(Engine engine, int minimumY, TreeMarkerTraversal.Position position) {
int relativeY = position.y() - minimumY;
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), String.class);
engine.getMantle().getMantle().remove(position.x(), relativeY, position.z(), TreeBlockMaterial.class);
}
boolean matchesExpectedMaterial(Block block, TreeBlockMaterial expected) {
return expected.matches(block.getBlockData().getAsString());
}
}
@@ -361,11 +361,7 @@ public class WandSVC implements IrisService {
wand = createWand();
dust = createDust();
J.ar(() -> {
for (Player i : Bukkit.getOnlinePlayers()) {
tick(i);
}
}, 0);
J.ar(this::tickAll, 0);
}
@Override
@@ -373,6 +369,22 @@ public class WandSVC implements IrisService {
}
/**
* Async driver tick. The online player list is only read from the thread that owns it,
* and every wand draw is dispatched to the thread owning that player.
*/
private void tickAll() {
try {
J.runGlobal(() -> {
for (Player p : Bukkit.getOnlinePlayers()) {
J.runEntity(p, () -> tick(p));
}
});
} catch (Throwable e) {
Iris.reportError(e);
}
}
public void tick(Player p) {
try {
try {
@@ -22,9 +22,9 @@ public final class IrisColumnWalk {
int chunkMinBlockX = Math.max(query.minBlockX(), chunkX << 4);
int chunkMaxBlockX = Math.min(query.maxBlockX(), (chunkX << 4) + 15);
for (int blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (int blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit(blockX, blockZ)) {
for (long blockZ = align(query.minBlockZ(), chunkMinBlockZ, stride); blockZ <= chunkMaxBlockZ; blockZ += stride) {
for (long blockX = align(query.minBlockX(), chunkMinBlockX, stride); blockX <= chunkMaxBlockX; blockX += stride) {
if (!visitor.visit((int) blockX, (int) blockZ)) {
return visited;
}
visited++;
@@ -36,10 +36,10 @@ public final class IrisColumnWalk {
return visited;
}
private static int align(int origin, int lowerBound, int stride) {
private static long align(int origin, int lowerBound, int stride) {
long offset = (long) lowerBound - (long) origin;
long steps = (offset + stride - 1L) / stride;
return (int) (origin + steps * stride);
return origin + steps * stride;
}
@FunctionalInterface
@@ -41,6 +41,10 @@ public class WandSelection {
public void draw() {
Location playerLoc = p.getLocation();
if (c.getWorld() == null || !c.getWorld().equals(playerLoc.getWorld())) {
return;
}
double maxDistanceSquared = 256 * 256;
int particleCount = 0;
@@ -37,7 +37,7 @@ public class TreeFellerEventOrderTest {
false
);
assertEquals(List.of(trigger), TreeFellerSVC.positionsForFelling(incomplete, trigger));
assertEquals(List.of(trigger), TreeFellingRunner.positionsForFelling(incomplete, trigger));
}
@Test
@@ -1,21 +1,32 @@
package art.arcane.iris.client;
import art.arcane.iris.spi.protocol.IrisMessage;
import art.arcane.iris.spi.protocol.IrisProtocol;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
final class IrisTileAssembler {
private static final int MAX_CHUNK_COUNT =
(IrisTileCodec.MAX_DECODED_BYTES + IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES - 1)
/ IrisProtocol.VISION_TILE_MAX_CHUNK_BYTES + 1;
private static final int MAX_PENDING_TILES = 64;
private final Map<IrisTileKey, Partial> partials;
IrisTileAssembler() {
this.partials = new HashMap<>();
this.partials = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<IrisTileKey, Partial> eldest) {
return size() > MAX_PENDING_TILES;
}
};
}
IrisTileImage add(IrisMessage.VisionTile tile) {
int chunkCount = tile.chunkCount();
int chunkIndex = tile.chunkIndex();
if (chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
if (chunkCount <= 0 || chunkCount > MAX_CHUNK_COUNT || chunkIndex < 0 || chunkIndex >= chunkCount || tile.data() == null) {
return null;
}
IrisTileKey key = new IrisTileKey(tile.tileX(), tile.tileZ(), tile.zoomLevel());
@@ -12,6 +12,7 @@ public final class IrisTileCodec {
public static final int MODE_PALETTE = 1;
private static final int MAX_DIMENSION = 512;
private static final int OPAQUE = 0xFF000000;
static final int MAX_DECODED_BYTES = 9 + 4 + 3 * MAX_DIMENSION * MAX_DIMENSION;
private IrisTileCodec() {
}
@@ -87,6 +88,9 @@ public final class IrisTileCodec {
break;
}
}
if (out.size() + produced > MAX_DECODED_BYTES) {
return null;
}
out.write(buffer, 0, produced);
}
} catch (DataFormatException malformed) {
+4 -1
View File
@@ -103,8 +103,11 @@ configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
configurations.testCompileClasspath.extendsFrom(configurations.devBundle)
configurations.testRuntimeClasspath.extendsFrom(configurations.devBundle)
// Jar-in-jar payload: exactly the Fabric API modules declared below, nothing else. Kept
// non-transitive so the bundled set matches the `jars` list in fabric.mod.json one-for-one a
// transitive pull (fabric-transitive-access-wideners-v1) used to land in META-INF/jars undeclared.
configurations.create('jij') {
transitive = true
transitive = false
}
dependencies {
+2 -45
View File
@@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
maven {
@@ -43,49 +41,8 @@ dependencyResolutionManagement {
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle.
apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile
includeBuild('../..') {
dependencySubstitution {
@@ -26,6 +26,9 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
@Mixin(PackRepository.class)
public class PackRepositoryMixin {
@@ -37,5 +40,10 @@ public class PackRepositoryMixin {
return;
}
}
// Client resource-pack repositories legitimately have no ServerPacksSource; a missing server-data
// repository is reported once at boot by ModdedForcedDatapack.verifyInjected().
LoggerFactory.getLogger("Iris").debug(
"Iris forced datapack source not attached: no ServerPacksSource among {} source(s) {}",
sources.length, Arrays.toString(sources));
}
}
@@ -28,12 +28,12 @@
{ "file": "META-INF/jars/fabric-registry-sync-v0.jar" },
{ "file": "META-INF/jars/fabric-resource-loader-v1.jar" },
{ "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" },
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
{ "file": "META-INF/jars/fabric-command-api-v2.jar" },
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" },
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
{ "file": "META-INF/jars/fabric-rendering-v1.jar" },
{ "file": "META-INF/jars/fabric-transitive-access-wideners-v1.jar" },
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" }
{ "file": "META-INF/jars/fabric-key-mapping-api-v1.jar" },
{ "file": "META-INF/jars/fabric-permission-api-v1.jar" }
],
"depends": {
"fabricloader": ">=0.19.3",
+2 -45
View File
@@ -16,8 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import java.io.File
pluginManagement {
repositories {
gradlePluginPortal()
@@ -43,49 +41,8 @@ dependencyResolutionManagement {
}
}
boolean hasVolmLibSettings(File directory) {
new File(directory, 'settings.gradle.kts').exists() || new File(directory, 'settings.gradle').exists()
}
File resolveLocalVolmLibDirectory() {
String configuredPath = providers.gradleProperty('localVolmLibDirectory')
.orElse(providers.environmentVariable('VOLMLIB_DIR'))
.orNull
if (configuredPath != null && !configuredPath.isBlank()) {
File configuredDirectory = file(configuredPath)
if (hasVolmLibSettings(configuredDirectory)) {
return configuredDirectory
}
}
File currentDirectory = settingsDir
while (currentDirectory != null) {
File candidate = new File(currentDirectory, 'VolmLib')
if (hasVolmLibSettings(candidate)) {
return candidate
}
currentDirectory = currentDirectory.parentFile
}
null
}
boolean useLocalVolmLib = providers.gradleProperty('useLocalVolmLib')
.orElse('true')
.map { String value -> value.equalsIgnoreCase('true') }
.get()
File localVolmLibDirectory = resolveLocalVolmLibDirectory()
if (useLocalVolmLib && localVolmLibDirectory != null) {
includeBuild(localVolmLibDirectory) {
dependencySubstitution {
substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared'))
substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared'))
}
}
}
// Shared VolmLib source resolution; see gradle/volmlib-resolution.settings.gradle.
apply from: new File(settingsDir, '../../gradle/volmlib-resolution.settings.gradle').canonicalFile
includeBuild('../..') {
dependencySubstitution {
@@ -63,10 +63,10 @@ public final class NativeStructureFactory {
}
StructureStart positioned;
if (plan.placement().isUnderground()) {
positioned = NativeStructurePostProcessor.relocateToMinY(
positioned = NativeStructureVerticalPlacer.relocateToMinY(
generated, source, plan.baseY(), context.heightAccessor());
} else {
NativeStructurePostProcessor.applyVerticalPlacement(
NativeStructureVerticalPlacer.applyVerticalPlacement(
generated,
plan.source().getStructure(),
0,
@@ -0,0 +1,215 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisStructureStiltSettings;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.SupportType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructureFoundationBuilder {
private static final int FOUNDATION_VERTICAL_TOLERANCE = 1;
private NativeStructureFoundationBuilder() {
}
private static List<FoundationColumn> foundationEnvelope(BoundingBox area, StructureStart start) {
BoundingBox structure = NativeStructureReferenceEnvelope.contentBounds(start);
int minX = Math.max(area.minX(), structure.minX());
int minZ = Math.max(area.minZ(), structure.minZ());
int maxX = Math.min(area.maxX(), structure.maxX());
int maxZ = Math.min(area.maxZ(), structure.maxZ());
if (minX > maxX || minZ > maxZ) {
return List.of();
}
List<StructurePiece> pieces = start.getPieces();
List<FoundationColumn> columns = new ArrayList<>((maxX - minX + 1) * (maxZ - minZ + 1));
BitSet envelope = new BitSet(area.getYSpan());
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
envelope.clear();
markFoundationEnvelope(envelope, pieces, area, x, z);
int cellCount = envelope.cardinality();
if (cellCount == 0) {
continue;
}
int[] ys = new int[cellCount];
int cell = 0;
for (int bit = envelope.nextSetBit(0); bit >= 0; bit = envelope.nextSetBit(bit + 1)) {
ys[cell++] = area.minY() + bit;
}
columns.add(new FoundationColumn(x, z, ys));
}
}
return List.copyOf(columns);
}
private static void markFoundationEnvelope(BitSet envelope, List<StructurePiece> pieces, BoundingBox area,
int x, int z) {
for (StructurePiece piece : pieces) {
if (NativeStructureReferenceEnvelope.isMarker(piece)) {
continue;
}
BoundingBox bounds = piece.getBoundingBox();
if (x < bounds.minX() || x > bounds.maxX() || z < bounds.minZ() || z > bounds.maxZ()) {
continue;
}
int groundY = bounds.minY();
if (piece instanceof PoolElementStructurePiece poolPiece) {
groundY += poolPiece.getGroundLevelDelta();
if (groundY < bounds.minY()) {
continue;
}
}
int minY = Math.max(area.minY(), bounds.minY());
int maxY = Math.min(area.maxY(), Math.min(bounds.maxY(), groundY + FOUNDATION_VERTICAL_TOLERANCE));
if (minY <= maxY) {
envelope.set(minY - area.minY(), maxY - area.minY() + 1);
}
}
}
static void placeStilts(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisStructureStiltSettings settings,
NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver,
IntBinaryOperator surfaceHeight,
boolean surfaceStructure) {
Objects.requireNonNull(surfaceHeight, "Structure stilts require an Iris terrain height resolver");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int structureHash = structureId == null ? 0 : structureId.hashCode();
RNG rng = new RNG(world.getSeed() ^ structureHash);
for (FoundationColumn column : foundationEnvelope(area, start)) {
if (!isStiltColumn(column.x(), column.z(), settings.getSpacing())) {
continue;
}
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
int terrainY = surfaceStructure
? Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())))
: area.minY() - 1;
int anchorY = findStiltAnchorY(
world, column.x(), column.z(), foundationY,
Math.max(1, settings.getMaxDepth()), terrainY, area.minY(), position);
if (anchorY == Integer.MIN_VALUE) {
continue;
}
for (int y = foundationY - 1; y > anchorY; y--) {
position.set(column.x(), y, column.z());
BlockState stilt = settings.getPalette() == null
? Blocks.COBBLESTONE.defaultBlockState()
: Objects.requireNonNull(
paletteBlockResolver.resolve(
settings.getPalette(), rng, column.x(), y, column.z()),
"Stilt palette returned no block for " + structureId + " at "
+ column.x() + "," + y + "," + column.z());
world.setBlock(position, stilt, 2);
}
}
}
static boolean isStiltColumn(int x, int z, int spacing) {
int resolvedSpacing = Math.max(1, spacing);
return resolvedSpacing == 1
|| Math.floorMod(x, resolvedSpacing) == 0
&& Math.floorMod(z, resolvedSpacing) == 0;
}
static int findStiltAnchorY(
WorldGenLevel world, int x, int z, int foundationY, int maxDepth,
int terrainY, int areaMinY, BlockPos.MutableBlockPos position) {
int minimumAnchorY = Math.max(
areaMinY, Math.max(terrainY, foundationY - maxDepth - 1));
for (int y = foundationY - 1; y >= minimumAnchorY; y--) {
BlockState state = world.getBlockState(position.set(x, y, z));
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
if (!vegetation && state.isFaceSturdy(
world, position, Direction.UP, SupportType.FULL)) {
return y;
}
}
return Integer.MIN_VALUE;
}
public static StiltSupportAudit auditStiltSupport(WorldGenLevel world, BoundingBox area,
StructureStart start, BlockState expectedStilt,
IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(expectedStilt, "Expected stilt state must not be null");
Objects.requireNonNull(surfaceHeight, "Structure stilt audit requires an Iris terrain height resolver");
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
int baseColumns = 0;
int stiltBlocks = 0;
int stiltColumns = 0;
int unsupportedColumns = 0;
for (FoundationColumn column : foundationEnvelope(area, start)) {
int foundationY = findFoundationY(world, column, position);
if (foundationY == Integer.MIN_VALUE) {
continue;
}
baseColumns++;
int terrainY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(column.x(), column.z())));
boolean grounded = foundationY <= terrainY + 1;
boolean stiltColumn = false;
for (int y = foundationY - 1; y >= area.minY(); y--) {
if (y <= terrainY) {
grounded = true;
break;
}
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.is(expectedStilt.getBlock())) {
stiltBlocks++;
stiltColumn = true;
if (y == area.minY()) {
grounded = true;
}
continue;
}
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
grounded = state.isSolid() && !vegetation;
break;
}
if (stiltColumn) {
stiltColumns++;
}
if (!grounded) {
unsupportedColumns++;
}
}
return new StiltSupportAudit(baseColumns, stiltBlocks, stiltColumns, unsupportedColumns);
}
private static int findFoundationY(WorldGenLevel world, FoundationColumn column,
BlockPos.MutableBlockPos position) {
for (int cell = 0; cell < column.ys().length; cell++) {
int y = column.ys()[cell];
BlockState state = world.getBlockState(position.set(column.x(), y, column.z()));
if (state.isSolid()) {
return y;
}
}
return Integer.MIN_VALUE;
}
private record FoundationColumn(int x, int z, int[] ys) {
}
public record StiltSupportAudit(int baseColumns, int stiltBlocks, int stiltColumns,
int unsupportedColumns) {
}
}
@@ -0,0 +1,148 @@
package art.arcane.iris.nativegen;
import com.mojang.datafixers.util.Either;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
final class NativeStructureReflection {
private NativeStructureReflection() {
}
static Field resolveScatteredHeightPositionField() {
Field resolved = null;
for (Field field : ScatteredFeaturePiece.class.getDeclaredFields()) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || field.getType() != int.class
|| !Modifier.isProtected(modifiers) || Modifier.isFinal(modifiers)) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("ScatteredFeaturePiece has multiple mutable protected int fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("ScatteredFeaturePiece height-position field is missing");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("ScatteredFeaturePiece height-position field is inaccessible");
}
return resolved;
}
private static Field resolveMonumentChildPiecesField() {
Field resolved = null;
for (Field field : OceanMonumentPieces.MonumentBuilding.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != List.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("Ocean Monument has multiple instance List fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("Ocean Monument child-pieces List field is missing");
}
if (!Modifier.isPrivate(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("Ocean Monument child-pieces field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("Ocean Monument child-pieces field is inaccessible");
}
return resolved;
}
static Field resolveSinglePoolTemplateField() {
Field resolved = null;
for (Field field : SinglePoolElement.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != Either.class) {
continue;
}
if (resolved != null) {
throw new IllegalStateException("SinglePoolElement has multiple instance Either fields");
}
resolved = field;
}
if (resolved == null) {
throw new IllegalStateException("SinglePoolElement template Either field is missing");
}
if (!Modifier.isProtected(resolved.getModifiers()) || !Modifier.isFinal(resolved.getModifiers())) {
throw new IllegalStateException("SinglePoolElement template field has an unexpected access contract");
}
if (!resolved.trySetAccessible()) {
throw new IllegalStateException("SinglePoolElement template field is inaccessible");
}
return resolved;
}
static StructureTemplate resolveTemplate(SinglePoolElement element,
Supplier<StructureTemplateManager> templates) {
Object value;
try {
value = SinglePoolTemplateAccess.FIELD.get(element);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read native structure pool template", error);
}
if (!(value instanceof Either<?, ?> reference)) {
throw new IllegalStateException("Native structure pool template field is not an Either");
}
return resolveTemplateReference(reference, templates);
}
static StructureTemplate resolveTemplateReference(Either<?, ?> reference,
Supplier<StructureTemplateManager> templates) {
return reference.map(
location -> resolveNamedTemplate(location, templates),
NativeStructureReflection::requireRuntimeTemplate);
}
private static StructureTemplate resolveNamedTemplate(Object value,
Supplier<StructureTemplateManager> templates) {
if (!(value instanceof Identifier identifier)) {
throw new IllegalStateException("Native structure pool template identifier is "
+ (value == null ? "null" : value.getClass().getName()));
}
return Objects.requireNonNull(templates == null ? null : templates.get(),
"Native structure template manager is unavailable").getOrCreate(identifier);
}
private static StructureTemplate requireRuntimeTemplate(Object value) {
if (!(value instanceof StructureTemplate template)) {
throw new IllegalStateException("Native runtime structure pool template is "
+ (value == null ? "null" : value.getClass().getName()));
}
return template;
}
static final class MonumentChildPiecesAccess {
static final Field FIELD = resolveMonumentChildPiecesField();
private MonumentChildPiecesAccess() {
}
}
static final class ScatteredHeightPositionAccess {
static final Field FIELD = resolveScatteredHeightPositionField();
private ScatteredHeightPositionAccess() {
}
}
private static final class SinglePoolTemplateAccess {
private static final Field FIELD = resolveSinglePoolTemplateField();
private SinglePoolTemplateAccess() {
}
}
}
@@ -0,0 +1,279 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
import java.util.function.Supplier;
public final class NativeStructureSurfaceFitter {
private static final double SURFACE_TERRAIN_FALLOFF = 2.0;
private static final long SURFACE_TERRAIN_INFLUENCE_SCALE = 1_000_000L;
private static final int SURFACE_TERRAIN_RADIUS = 12;
private NativeStructureSurfaceFitter() {
}
public static void prepareSurfaceStructures(WorldGenLevel world, BoundingBox area,
List<StructureStart> starts,
IntBinaryOperator surfaceHeight) {
if (starts == null || starts.isEmpty()) {
return;
}
Objects.requireNonNull(surfaceHeight, "Surface structure terrain fitting requires an Iris height resolver");
List<SurfaceAnchor> anchors = collectSurfaceAnchors(starts);
if (!anchors.isEmpty()) {
fitSurfaceTerrain(world, area, anchors, surfaceHeight);
}
Supplier<StructureTemplateManager> templates = () -> world.getLevel().getStructureManager();
for (StructureStart start : starts) {
if (requiresSurfaceTerrain(start)) {
NativeStructureTerrainIntegrator.clearLegacyTemplateAir(world, area, start, templates);
}
}
}
static boolean shouldPrepareSurfaceTerrain(TerrainAdjustment adjustment,
GenerationStep.Decoration step) {
return adjustment == TerrainAdjustment.BEARD_THIN
&& step == GenerationStep.Decoration.SURFACE_STRUCTURES;
}
static int resolveSurfaceTarget(List<SurfaceAnchor> anchors, int worldX, int worldZ,
int originalY) {
int localTargetY = originalY;
SurfaceAnchor selectedLocal = null;
long totalInfluence = 0L;
long weightedMeetY = 0L;
long maximumInfluence = 0L;
for (SurfaceAnchor anchor : anchors) {
int outX = IrisObjectVacuum.outset(worldX, anchor.minX(), anchor.maxX());
int outZ = IrisObjectVacuum.outset(worldZ, anchor.minZ(), anchor.maxZ());
long distanceSquared = (long) outX * outX + (long) outZ * outZ;
if (distanceSquared > (long) SURFACE_TERRAIN_RADIUS * SURFACE_TERRAIN_RADIUS) {
continue;
}
boolean containsColumn = outX == 0 && outZ == 0;
if (containsColumn) {
if (precedes(anchor, selectedLocal)) {
localTargetY = anchor.meetY();
selectedLocal = anchor;
}
continue;
}
double factor = IrisObjectVacuum.columnInfluence(
worldX, worldZ,
anchor.minX(), anchor.maxX(), anchor.minZ(), anchor.maxZ(),
SURFACE_TERRAIN_RADIUS, SURFACE_TERRAIN_FALLOFF);
long influence = Math.round(factor * SURFACE_TERRAIN_INFLUENCE_SCALE);
if (influence <= 0L) {
continue;
}
long weightedInfluence = influence * Math.max(1, anchor.strength());
totalInfluence += weightedInfluence;
weightedMeetY += weightedInfluence * anchor.meetY();
maximumInfluence = Math.max(maximumInfluence, influence);
}
if (selectedLocal != null) {
return localTargetY;
}
if (totalInfluence == 0L) {
return originalY;
}
double blendedMeetY = weightedMeetY / (double) totalInfluence;
double factor = maximumInfluence / (double) SURFACE_TERRAIN_INFLUENCE_SCALE;
return (int) Math.round(originalY + ((blendedMeetY - originalY) * factor));
}
private static boolean precedes(SurfaceAnchor candidate, SurfaceAnchor selected) {
if (selected == null) {
return true;
}
if (candidate.strength() != selected.strength()) {
return candidate.strength() > selected.strength();
}
if (candidate.meetY() != selected.meetY()) {
return candidate.meetY() < selected.meetY();
}
if (candidate.minX() != selected.minX()) {
return candidate.minX() < selected.minX();
}
if (candidate.minZ() != selected.minZ()) {
return candidate.minZ() < selected.minZ();
}
if (candidate.maxX() != selected.maxX()) {
return candidate.maxX() < selected.maxX();
}
if (candidate.maxZ() != selected.maxZ()) {
return candidate.maxZ() < selected.maxZ();
}
return false;
}
private static List<SurfaceAnchor> collectSurfaceAnchors(List<StructureStart> starts) {
List<SurfaceAnchor> anchors = new ArrayList<>();
for (StructureStart start : starts) {
if (!requiresSurfaceTerrain(start)) {
continue;
}
for (StructurePiece piece : start.getPieces()) {
if (NativeStructureReferenceEnvelope.isMarker(piece)) {
continue;
}
if (piece instanceof PoolElementStructurePiece poolPiece) {
if (poolPiece.getElement().getProjection() == StructureTemplatePool.Projection.RIGID) {
BoundingBox bounds = poolPiece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() + poolPiece.getGroundLevelDelta() - 1, 2));
}
for (JigsawJunction junction : poolPiece.getJunctions()) {
anchors.add(new SurfaceAnchor(
junction.getSourceX(), junction.getSourceX(),
junction.getSourceZ(), junction.getSourceZ(),
junction.getSourceGroundY() - 1, 1));
}
continue;
}
BoundingBox bounds = piece.getBoundingBox();
anchors.add(new SurfaceAnchor(
bounds.minX(), bounds.maxX(), bounds.minZ(), bounds.maxZ(),
bounds.minY() - 1, 2));
}
}
return List.copyOf(anchors);
}
static boolean requiresSurfaceTerrain(StructureStart start) {
return start != null
&& start.isValid()
&& shouldPrepareSurfaceTerrain(
start.getStructure().terrainAdaptation(), start.getStructure().step());
}
private static void fitSurfaceTerrain(WorldGenLevel world, BoundingBox area,
List<SurfaceAnchor> anchors,
IntBinaryOperator surfaceHeight) {
int width = area.getXSpan();
int depth = area.getZSpan();
int[] originalHeights = new int[width * depth];
int[] targetHeights = new int[width * depth];
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
int originalY = Math.max(area.minY(), Math.min(
area.maxY(), surfaceHeight.applyAsInt(x, z)));
originalHeights[column] = originalY;
targetHeights[column] = Math.max(area.minY(), Math.min(
area.maxY(), resolveSurfaceTarget(anchors, x, z, originalY)));
}
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
applySurfaceColumn(world, position, x, z,
originalHeights[column], targetHeights[column], area.minY(), area.maxY());
}
}
}
static void applySurfaceColumn(WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int targetY,
int worldMinY, int worldMaxY) {
if (targetY == originalY) {
return;
}
SurfaceMaterials materials = resolveSurfaceMaterials(world, position, x, z, originalY, worldMinY);
if (targetY < originalY) {
BlockState clearedState = clearSurfaceDecorationAndResolveFill(
world, position, x, z, originalY, worldMaxY);
for (int y = originalY; y > targetY; y--) {
world.setBlock(position.set(x, y, z), clearedState, 2);
}
world.setBlock(position.set(x, targetY, z), materials.surface(), 2);
return;
}
for (int y = originalY + 1; y < targetY; y++) {
position.set(x, y, z);
if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) {
world.setBlock(position, materials.subsurface(), 2);
}
}
position.set(x, targetY, z);
if (!NativeStructureVegetationClearer.isTreeBlock(world.getBlockState(position))) {
world.setBlock(position, materials.surface(), 2);
}
}
private static BlockState clearSurfaceDecorationAndResolveFill(
WorldGenLevel world, BlockPos.MutableBlockPos position,
int x, int z, int originalY, int worldMaxY) {
BlockState air = Blocks.AIR.defaultBlockState();
for (int y = originalY + 1; y <= worldMaxY; y++) {
BlockState state = world.getBlockState(position.set(x, y, z));
if (state.isAir()) {
return air;
}
if (!state.getFluidState().isEmpty()) {
BlockState fluid = state.getFluidState().createLegacyBlock();
if (state != fluid) {
world.setBlock(position, fluid, 2);
}
return fluid;
}
if (state.isSolid() || NativeStructureVegetationClearer.isTreeBlock(state)) {
return air;
}
world.setBlock(position, air, 2);
}
return air;
}
private static SurfaceMaterials resolveSurfaceMaterials(WorldGenLevel world,
BlockPos.MutableBlockPos position,
int x, int z, int originalY,
int worldMinY) {
BlockState surface = world.getBlockState(position.set(x, originalY, z));
BlockState subsurface = null;
for (int y = originalY - 1; y >= worldMinY; y--) {
BlockState candidate = world.getBlockState(position.set(x, y, z));
if (isTerrainBlock(candidate)) {
subsurface = candidate;
break;
}
}
if (subsurface == null) {
subsurface = isTerrainBlock(surface) ? surface : Blocks.STONE.defaultBlockState();
}
if (!isTerrainBlock(surface)) {
surface = subsurface;
}
return new SurfaceMaterials(surface, subsurface);
}
private static boolean isTerrainBlock(BlockState state) {
return state.isSolid() && !NativeStructureVegetationClearer.isTreeBlock(state);
}
record SurfaceAnchor(int minX, int maxX, int minZ, int maxZ, int meetY, int strength) {
}
private record SurfaceMaterials(BlockState surface, BlockState subsurface) {
}
}
@@ -0,0 +1,508 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.mantle.components.StructureCarveEnvelope;
import art.arcane.iris.engine.mantle.components.StructureCarvingFootprint;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.IrisStructureTerrainMode;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
public final class NativeStructureTerrainIntegrator {
private static final int AUTO_ENCASE_PADDING = 3;
private static final long CARVE_CEILING_ROLL_SIGNATURE = 0x2A17L;
private static final long CARVE_FLOOR_ROLL_SIGNATURE = 0x5B3DL;
private static final long CARVE_LOBE_SIGNATURE = 0x7C41L;
private static final int MAX_CACHED_CARVE_FOOTPRINTS = 4;
private static final int MAX_CARVE_COLUMNS = 2_000_000;
private static final int MAX_TEMPLATE_OCCUPANCY_CELLS = 4_194_304;
private static final List<Block> TEMPLATE_VOID_BLOCKS = List.of(Blocks.AIR, Blocks.STRUCTURE_VOID);
private static final Map<CarveFootprintKey, StructureCarvingFootprint> CARVE_FOOTPRINTS =
Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<CarveFootprintKey, StructureCarvingFootprint> eldest) {
return size() > MAX_CACHED_CARVE_FOOTPRINTS;
}
});
private NativeStructureTerrainIntegrator() {
}
public static IrisStructureTerrain resolveNativeTerrain(StructureStart start,
IrisStructureTerrain configuredTerrain) {
if (configuredTerrain != null) {
return configuredTerrain;
}
if (start == null || !start.isValid()
|| !encasesTerrain(start.getStructure().terrainAdaptation())) {
return null;
}
return new IrisStructureTerrain()
.setMode(IrisStructureTerrainMode.ENCASE)
.setHorizontalPadding(AUTO_ENCASE_PADDING)
.setCeilingPadding(AUTO_ENCASE_PADDING)
.setFloorPadding(AUTO_ENCASE_PADDING);
}
static boolean encasesTerrain(TerrainAdjustment adjustment) {
return adjustment == TerrainAdjustment.BURY || adjustment == TerrainAdjustment.ENCAPSULATE;
}
static void integrateTerrain(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisStructureTerrain configuredTerrain,
NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) {
IrisStructureTerrain terrain = configuredTerrain == null
? new IrisStructureTerrain().setMode(IrisStructureTerrainMode.SOURCE)
: configuredTerrain;
IrisStructureTerrainMode mode = terrain.resolvedMode();
if (mode == IrisStructureTerrainMode.SOURCE || mode == IrisStructureTerrainMode.PRESERVE) {
return;
}
if (mode == IrisStructureTerrainMode.VACUUM) {
carvePieceBoxes(world, area, start, terrain);
return;
}
if (mode == IrisStructureTerrainMode.ENCASE) {
encasePieces(world, area, structureId, start, terrain, paletteBlockResolver);
return;
}
if (mode != IrisStructureTerrainMode.BORE && mode != IrisStructureTerrainMode.FORCE_CARVE) {
throw new IllegalStateException("Native structure terrain mode " + mode
+ " is not implemented for '" + structureId + "'");
}
IrisStructureCarveShape shape = mode == IrisStructureTerrainMode.BORE
? IrisStructureCarveShape.BOX : terrain.resolvedShape();
if (shape == IrisStructureCarveShape.BOX) {
carvePieceBoxes(world, area, start, terrain);
return;
}
carveOrganicColumns(world, area, organicCarve(
carveFootprint(start, Math.max(0, terrain.getHorizontalPadding()),
() -> world.getLevel().getStructureManager()),
terrain, shape, carveNoiseIdentity(world, structureId, start)));
}
static List<BoundingBox> contentPieceBounds(StructureStart start) {
List<BoundingBox> bounds = new ArrayList<>(start.getPieces().size());
for (StructurePiece piece : start.getPieces()) {
if (!NativeStructureReferenceEnvelope.isMarker(piece)) {
bounds.add(piece.getBoundingBox());
}
}
return List.copyOf(bounds);
}
/**
* Per-column occupancy of the whole start, cached because a single structure spans many chunks and
* every one of them carves against the same shrinkwrapped footprint.
*/
static StructureCarvingFootprint carveFootprint(StructureStart start, int horizontalPadding,
Supplier<StructureTemplateManager> templates) {
CarveFootprintKey key = new CarveFootprintKey(start, horizontalPadding);
StructureCarvingFootprint cached = CARVE_FOOTPRINTS.get(key);
if (cached != null) {
return cached;
}
StructureCarvingFootprint footprint = StructureCarvingFootprint.fromColumns(
sink -> emitCarveColumns(start, templates, sink), horizontalPadding, MAX_CARVE_COLUMNS);
if (footprint == null) {
throw new IllegalStateException("Native structure carve footprint is empty or exceeds "
+ MAX_CARVE_COLUMNS + " columns");
}
CARVE_FOOTPRINTS.put(key, footprint);
return footprint;
}
static OrganicCarve organicCarve(StructureCarvingFootprint footprint, IrisStructureTerrain terrain,
IrisStructureCarveShape shape, long identity) {
double strength = terrain.resolvedErosionStrength();
double lobeStrength = terrain.resolvedLobeStrength();
RNG noiseRng = new RNG(identity);
CNG blob = null;
CNG ceilingRoll = null;
CNG floorRoll = null;
CNG lobe = null;
if (shape == IrisStructureCarveShape.ERODED && strength > 0D) {
blob = CNG.signature(noiseRng);
ceilingRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_CEILING_ROLL_SIGNATURE));
floorRoll = CNG.signature(noiseRng.nextParallelRNG(CARVE_FLOOR_ROLL_SIGNATURE));
}
if (shape == IrisStructureCarveShape.ERODED && lobeStrength > 0D) {
// A plain single octave channel: the fractured signature noise has no usable low frequency band.
lobe = new CNG(noiseRng.nextParallelRNG(CARVE_LOBE_SIGNATURE), 1D, 1);
}
return new OrganicCarve(footprint, shape,
Math.max(0, terrain.getHorizontalPadding()),
Math.max(0, terrain.getCeilingPadding()),
Math.max(0, terrain.getFloorPadding()),
strength, terrain.resolvedErosionFrequency(), blob, ceilingRoll, floorRoll,
lobe, terrain.resolvedLobeFrequency(), lobeStrength);
}
static void carveOrganicColumns(WorldGenLevel world, BoundingBox area, OrganicCarve carve) {
StructureCarvingFootprint footprint = carve.footprint();
int minX = Math.max(area.minX(), footprint.minX());
int maxX = Math.min(area.maxX(), footprint.maxX());
int minZ = Math.max(area.minZ(), footprint.minZ());
int maxZ = Math.min(area.maxZ(), footprint.maxZ());
if (minX > maxX || minZ > maxZ) {
return;
}
boolean eroded = carve.blob() != null;
BlockState air = Blocks.AIR.defaultBlockState();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
int index = footprint.indexAt(x, z);
long horizontalDistanceSquared = footprint.distanceSquaredAt(index);
double sideReach = StructureCarveEnvelope.lobedSideReach(carve.lobe(),
carve.lobeFrequency(), carve.lobeStrength(), x, z, carve.horizontalPadding());
double sideReachSquared = sideReach * sideReach;
if (horizontalDistanceSquared > sideReachSquared) {
continue;
}
double normalizedHorizontal = sideReachSquared == 0D
? 0D : horizontalDistanceSquared / sideReachSquared;
int sourceMinY = footprint.sourceMinYAt(index);
int sourceMaxY = footprint.sourceMaxYAt(index);
double upReach = eroded
? StructureCarveEnvelope.lobedUpReach(carve.lobe(), carve.lobeFrequency(),
carve.lobeStrength(), x, z,
StructureCarveEnvelope.erodedUpReach(carve.ceilingRoll(),
carve.frequency(), carve.strength(), x, z,
carve.ceilingPadding()))
: Math.max(1D, carve.ceilingPadding());
double floorReach = eroded
? StructureCarveEnvelope.erodedDownReach(carve.floorRoll(), carve.frequency(),
carve.strength(), x, z, carve.floorPadding())
: carve.floorPadding();
int columnMinY = Math.max(area.minY(), sourceMinY - (int) Math.floor(floorReach));
int columnMaxY = Math.min(area.maxY(),
sourceMaxY + (eroded ? (int) Math.ceil(upReach) : carve.ceilingPadding()));
double downReach = Math.max(1D, floorReach);
for (int y = columnMinY; y <= columnMaxY; y++) {
double normalizedVertical = StructureCarveEnvelope.normalizedVerticalDistance(
y, sourceMinY, sourceMaxY, upReach, downReach);
double distanceSquared = normalizedHorizontal
+ normalizedVertical * normalizedVertical;
if (distanceSquared > 1D) {
continue;
}
if (distanceSquared > 0D && eroded) {
double noise = carve.blob().fitDouble(0D, 1D,
x * carve.frequency(), y * carve.frequency(), z * carve.frequency());
if (!StructureCarveEnvelope.shouldCarveOverboreCell(
carve.shape(), distanceSquared, noise, carve.strength())) {
continue;
}
}
world.setBlock(position.set(x, y, z), air, 2);
}
}
}
}
private static boolean emitCarveColumns(StructureStart start,
Supplier<StructureTemplateManager> templates,
StructureCarvingFootprint.ColumnSink sink) {
for (StructurePiece piece : start.getPieces()) {
if (NativeStructureReferenceEnvelope.isMarker(piece)) {
continue;
}
BoundingBox bounds = piece.getBoundingBox();
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|| !emitTemplateColumns(pieceTemplates(poolPiece, templates),
poolPiece.getPosition(), poolPiece.getRotation(), bounds, sink)) {
emitBoxColumns(bounds, sink);
}
}
return true;
}
/**
* Derives per-column vertical extents from the complement of the template's air and structure-void
* cells, so a carve tracks the actual silhouette instead of the piece's rectangular bounding box.
*/
static boolean emitTemplateColumns(List<StructureTemplate> templates, BlockPos position,
Rotation rotation, BoundingBox bounds,
StructureCarvingFootprint.ColumnSink sink) {
if (templates.isEmpty()) {
return false;
}
int width = bounds.getXSpan();
int depth = bounds.getZSpan();
long cells = (long) width * depth * bounds.getYSpan();
if (cells < 1L || cells > MAX_TEMPLATE_OCCUPANCY_CELLS) {
return false;
}
StructurePlaceSettings settings = new StructurePlaceSettings().setRotation(rotation);
boolean[] voidCells = null;
for (StructureTemplate template : templates) {
boolean[] templateVoid = new boolean[(int) cells];
for (Block ignored : TEMPLATE_VOID_BLOCKS) {
for (StructureTemplate.StructureBlockInfo info
: template.filterBlocks(position, settings, ignored)) {
BlockPos voidPosition = info.pos();
if (bounds.isInside(voidPosition)) {
templateVoid[templateCellIndex(bounds, width, depth,
voidPosition.getX(), voidPosition.getY(), voidPosition.getZ())] = true;
}
}
}
if (voidCells == null) {
voidCells = templateVoid;
continue;
}
for (int cell = 0; cell < voidCells.length; cell++) {
voidCells[cell] &= templateVoid[cell];
}
}
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (int y = bounds.minY(); y <= bounds.maxY(); y++) {
if (voidCells[templateCellIndex(bounds, width, depth, x, y, z)]) {
continue;
}
if (minY == Integer.MAX_VALUE) {
minY = y;
}
maxY = y;
}
if (minY != Integer.MAX_VALUE) {
sink.column(x, z, minY, maxY);
}
}
}
return true;
}
private static void emitBoxColumns(BoundingBox bounds, StructureCarvingFootprint.ColumnSink sink) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
sink.column(x, z, bounds.minY(), bounds.maxY());
}
}
}
private static int templateCellIndex(BoundingBox bounds, int width, int depth,
int x, int y, int z) {
return ((y - bounds.minY()) * depth + z - bounds.minZ()) * width + x - bounds.minX();
}
private static List<StructureTemplate> pieceTemplates(PoolElementStructurePiece poolPiece,
Supplier<StructureTemplateManager> templates) {
List<StructureTemplate> resolved = new ArrayList<>(1);
collectElementTemplates(poolPiece.getElement(), templates, resolved);
return resolved;
}
private static void collectElementTemplates(StructurePoolElement element,
Supplier<StructureTemplateManager> templates,
List<StructureTemplate> resolved) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
collectElementTemplates(child, templates, resolved);
}
return;
}
if (element instanceof SinglePoolElement singleElement) {
resolved.add(NativeStructureReflection.resolveTemplate(singleElement, templates));
}
}
private static long carveNoiseIdentity(WorldGenLevel world, String structureId,
StructureStart start) {
return world.getSeed()
^ ((long) start.getChunkPos().x() * 341873128712L)
^ ((long) start.getChunkPos().z() * 132897987541L)
^ (structureId == null ? 0 : structureId.hashCode());
}
private static void encasePieces(WorldGenLevel world, BoundingBox area, String structureId,
StructureStart start, IrisStructureTerrain terrain,
NativeStructurePostProcessor.PaletteBlockResolver paletteBlockResolver) {
IrisMaterialPalette palette = terrain.getEncasePalette();
RNG rng = null;
if (palette != null) {
Objects.requireNonNull(paletteBlockResolver,
"Native structure encase palette requires a platform block resolver");
rng = new RNG(world.getSeed() ^ (structureId == null ? 0 : structureId.hashCode()));
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (BoundingBox bounds : contentPieceBounds(start)) {
BoundingBox shell = paddedPieceArea(area, bounds, terrain);
if (shell == null) {
continue;
}
for (int x = shell.minX(); x <= shell.maxX(); x++) {
for (int z = shell.minZ(); z <= shell.maxZ(); z++) {
for (int y = shell.minY(); y <= shell.maxY(); y++) {
BlockState existing = world.getBlockState(position.set(x, y, z));
if (!isEncaseable(existing)) {
continue;
}
BlockState fill = palette == null
? defaultEncaseBlock(y)
: Objects.requireNonNull(
paletteBlockResolver.resolve(palette, rng, x, y, z),
"Encase palette returned no block for " + structureId + " at "
+ x + "," + y + "," + z);
world.setBlock(position, fill, 2);
}
}
}
}
}
static boolean isEncaseable(BlockState state) {
return state.isAir() || !state.getFluidState().isEmpty();
}
static BlockState defaultEncaseBlock(int y) {
return y < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState();
}
private static void carvePieceBoxes(WorldGenLevel world, BoundingBox area, StructureStart start,
IrisStructureTerrain terrain) {
BlockState air = Blocks.AIR.defaultBlockState();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (BoundingBox bounds : contentPieceBounds(start)) {
BoundingBox carve = paddedPieceArea(area, bounds, terrain);
if (carve == null) {
continue;
}
for (int x = carve.minX(); x <= carve.maxX(); x++) {
for (int z = carve.minZ(); z <= carve.maxZ(); z++) {
for (int y = carve.minY(); y <= carve.maxY(); y++) {
world.setBlock(position.set(x, y, z), air, 2);
}
}
}
}
}
private static BoundingBox paddedPieceArea(BoundingBox area, BoundingBox bounds,
IrisStructureTerrain terrain) {
int horizontalPadding = Math.max(0, terrain.getHorizontalPadding());
int minX = Math.max(area.minX(), bounds.minX() - horizontalPadding);
int minY = Math.max(area.minY(), bounds.minY() - Math.max(0, terrain.getFloorPadding()));
int minZ = Math.max(area.minZ(), bounds.minZ() - horizontalPadding);
int maxX = Math.min(area.maxX(), bounds.maxX() + horizontalPadding);
int maxY = Math.min(area.maxY(), bounds.maxY() + Math.max(0, terrain.getCeilingPadding()));
int maxZ = Math.min(area.maxZ(), bounds.maxZ() + horizontalPadding);
if (minX > maxX || minY > maxY || minZ > maxZ) {
return null;
}
return new BoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
}
static void clearLegacyTemplateAir(WorldGenLevel world, BoundingBox area,
StructureStart start,
Supplier<StructureTemplateManager> templates) {
for (StructurePiece piece : start.getPieces()) {
if (NativeStructureReferenceEnvelope.isMarker(piece)) {
continue;
}
if (!(piece instanceof PoolElementStructurePiece poolPiece)
|| poolPiece.getElement().getProjection() != StructureTemplatePool.Projection.RIGID
|| !intersects(poolPiece.getBoundingBox(), area)) {
continue;
}
int groundY = poolPiece.getBoundingBox().minY() + poolPiece.getGroundLevelDelta();
StructurePlaceSettings settings = new StructurePlaceSettings()
.setRotation(poolPiece.getRotation())
.setBoundingBox(area);
clearLegacyTemplateAir(world, poolPiece.getElement(), poolPiece.getPosition(),
groundY, settings, templates);
}
}
private static void clearLegacyTemplateAir(WorldGenLevel world, StructurePoolElement element,
BlockPos position, int groundY,
StructurePlaceSettings settings,
Supplier<StructureTemplateManager> templates) {
if (element instanceof ListPoolElement listElement) {
for (StructurePoolElement child : listElement.getElements()) {
clearLegacyTemplateAir(world, child, position, groundY, settings, templates);
}
return;
}
if (!(element instanceof LegacySinglePoolElement legacyElement)) {
return;
}
StructureTemplate template = NativeStructureReflection.resolveTemplate(legacyElement, templates);
clearTemplateAir(world, template, position, groundY, settings);
}
static void clearTemplateAir(WorldGenLevel world, StructureTemplate template,
BlockPos position, int groundY,
StructurePlaceSettings settings) {
List<StructureTemplate.StructureBlockInfo> airBlocks = template.filterBlocks(
position, settings, Blocks.AIR);
BlockState air = Blocks.AIR.defaultBlockState();
for (StructureTemplate.StructureBlockInfo airBlock : airBlocks) {
BlockPos airPosition = airBlock.pos();
BlockState existingState = world.getBlockState(airPosition);
if (shouldClearLegacyAir(
airPosition.getY(), groundY, existingState.isAir())
&& !NativeStructureVegetationClearer.isTreeBlock(existingState)) {
world.setBlock(airPosition, air, 2);
}
}
}
static boolean shouldClearLegacyAir(int airY, int groundY, boolean existingAir) {
return airY >= groundY && !existingAir;
}
static boolean intersects(BoundingBox first, BoundingBox second) {
return first.maxX() >= second.minX() && first.minX() <= second.maxX()
&& first.maxY() >= second.minY() && first.minY() <= second.maxY()
&& first.maxZ() >= second.minZ() && first.minZ() <= second.maxZ();
}
// StructureStart has no value equality, so the key pins the exact start instance a chunk carves against.
private record CarveFootprintKey(StructureStart start, int padding) {
}
record OrganicCarve(StructureCarvingFootprint footprint, IrisStructureCarveShape shape,
int horizontalPadding, int ceilingPadding, int floorPadding,
double strength, double frequency, CNG blob, CNG ceilingRoll, CNG floorRoll,
CNG lobe, double lobeFrequency, double lobeStrength) {
}
public record TerrainTarget(String structureId, StructureStart start,
IrisStructureTerrain terrain) {
}
}
@@ -0,0 +1,174 @@
package art.arcane.iris.nativegen;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
public final class NativeStructureVegetationClearer {
private NativeStructureVegetationClearer() {
}
public static boolean isUndergroundStep(GenerationStep.Decoration step) {
return step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES
|| step == GenerationStep.Decoration.UNDERGROUND_DECORATION
|| step == GenerationStep.Decoration.STRONGHOLDS;
}
public static boolean shouldClearEntireVegetationFootprint(GenerationStep.Decoration step,
boolean configured) {
return configured;
}
public static void clearIntersectingVegetation(WorldGenLevel world, ChunkAccess chunk, BoundingBox area,
List<VegetationTarget> targets) {
if (targets == null || targets.isEmpty()) {
return;
}
VegetationSnapshot snapshot = captureVegetation(chunk, area);
if (snapshot.treeBlockCount() == 0) {
return;
}
boolean[] clearColumns = new boolean[area.getXSpan() * area.getZSpan()];
for (VegetationTarget target : targets) {
if (target != null && target.force() && target.start() != null && target.start().isValid()) {
markVegetationColumns(area, snapshot, target, clearColumns);
}
}
clearVegetationColumns(world, area, snapshot, clearColumns);
}
private static VegetationSnapshot captureVegetation(ChunkAccess chunk, BoundingBox area) {
int width = area.getXSpan();
int depth = area.getZSpan();
BitSet[] columns = new BitSet[width * depth];
int[] lowestY = new int[columns.length];
Arrays.fill(lowestY, Integer.MAX_VALUE);
int treeBlockCount = 0;
LevelChunkSection[] sections = chunk.getSections();
int chunkMinX = chunk.getPos().getMinBlockX();
int chunkMinZ = chunk.getPos().getMinBlockZ();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
int minY = Math.max(area.minY(), sectionMinY);
int maxY = Math.min(area.maxY(), sectionMinY + 15);
if (minY > maxY || section.hasOnlyAir()
|| !section.maybeHas(NativeStructureVegetationClearer::isTreeBlock)) {
continue;
}
for (int y = minY; y <= maxY; y++) {
int localY = y - sectionMinY;
for (int z = area.minZ(); z <= area.maxZ(); z++) {
int localZ = z - chunkMinZ;
for (int x = area.minX(); x <= area.maxX(); x++) {
int localX = x - chunkMinX;
if (!isTreeBlock(section.getBlockState(localX, localY, localZ))) {
continue;
}
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = columns[column];
if (treeBlocks == null) {
treeBlocks = new BitSet(area.getYSpan());
columns[column] = treeBlocks;
}
treeBlocks.set(y - area.minY());
lowestY[column] = Math.min(lowestY[column], y);
treeBlockCount++;
}
}
}
}
return new VegetationSnapshot(columns, lowestY, treeBlockCount);
}
private static void markVegetationColumns(BoundingBox area, VegetationSnapshot snapshot,
VegetationTarget target, boolean[] clearColumns) {
int width = area.getXSpan();
int[] pieceTops = new int[clearColumns.length];
Arrays.fill(pieceTops, Integer.MIN_VALUE);
for (StructurePiece piece : target.start().getPieces()) {
if (NativeStructureReferenceEnvelope.isMarker(piece)) {
continue;
}
BoundingBox bounds = piece.getBoundingBox();
int minX = Math.max(area.minX(), bounds.minX());
int maxX = Math.min(area.maxX(), bounds.maxX());
int minZ = Math.max(area.minZ(), bounds.minZ());
int maxZ = Math.min(area.maxZ(), bounds.maxZ());
if (minX > maxX || minZ > maxZ) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
int column = (z - area.minZ()) * width + x - area.minX();
pieceTops[column] = Math.max(pieceTops[column], bounds.maxY());
}
}
}
for (int column = 0; column < pieceTops.length; column++) {
if (snapshot.columns()[column] == null || pieceTops[column] == Integer.MIN_VALUE) {
continue;
}
if (shouldClearVegetationColumn(pieceTops[column], snapshot.lowestY()[column], target.force())) {
clearColumns[column] = true;
}
}
}
static boolean shouldClearVegetationColumn(int pieceTopY, int lowestTreeY, boolean force) {
return force || pieceTopY >= lowestTreeY;
}
private static void clearVegetationColumns(WorldGenLevel world, BoundingBox area,
VegetationSnapshot snapshot, boolean[] clearColumns) {
int width = area.getXSpan();
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
BlockState air = Blocks.AIR.defaultBlockState();
for (int z = area.minZ(); z <= area.maxZ(); z++) {
for (int x = area.minX(); x <= area.maxX(); x++) {
int column = (z - area.minZ()) * width + x - area.minX();
BitSet treeBlocks = snapshot.columns()[column];
if (!clearColumns[column] || treeBlocks == null) {
continue;
}
for (int bit = treeBlocks.nextSetBit(0); bit >= 0; bit = treeBlocks.nextSetBit(bit + 1)) {
int y = area.minY() + bit;
position.set(x, y, z);
BlockState state = world.getBlockState(position);
if (isTreeBlock(state)) {
world.setBlock(position, air, 2);
}
}
}
}
}
static boolean isTreeBlock(BlockState state) {
if (state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES)) {
return true;
}
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
return path.endsWith("_log") || path.endsWith("_wood")
|| path.endsWith("_stem") || path.endsWith("_hyphae")
|| path.endsWith("_leaves");
}
public record VegetationTarget(StructureStart start, boolean force) {
}
private record VegetationSnapshot(BitSet[] columns, int[] lowestY, int treeBlockCount) {
}
}
@@ -0,0 +1,365 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.engine.object.IrisStructureYBand;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
import net.minecraft.world.level.levelgen.structure.ScatteredFeaturePiece;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pools.JigsawJunction;
import net.minecraft.world.level.levelgen.structure.structures.DesertPyramidPiece;
import net.minecraft.world.level.levelgen.structure.structures.JungleTemplePiece;
import net.minecraft.world.level.levelgen.structure.structures.OceanMonumentPieces;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class NativeStructureVerticalPlacer {
private static final String DESERT_PYRAMID_ID = "minecraft:desert_pyramid";
private static final String JUNGLE_PYRAMID_ID = "minecraft:jungle_pyramid";
private static final int MAX_BURIAL_COLUMNS = 2_000_000;
private static final int MONUMENT_BASE_BELOW_SEA_LEVEL = 24;
private static final String OCEAN_MONUMENT_ID = "minecraft:monument";
private static final int UNDERGROUND_SURFACE_CLEARANCE = 1;
private NativeStructureVerticalPlacer() {
}
public static int applyVerticalPlacement(StructureStart start, String structureId, int requestedOffset,
int seaLevel, int worldMinY, int worldMaxYExclusive,
boolean underground, boolean preserveSourceY,
IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
if (isOceanMonument(structureId)) {
return alignOceanMonumentToSeaLevel(
start, requestedOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
if (isAdjustedScatteredStructure(structureId)) {
return alignScatteredStructureToSurface(
start, structureId, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
}
return applyVerticalShift(start, requestedOffset, worldMinY, worldMaxYExclusive,
underground, preserveSourceY, yBand, surfaceHeight);
}
public static StructureStart relocateToMinY(StructureStart start, Structure source, int targetMinY,
LevelHeightAccessor heightAccessor) {
Objects.requireNonNull(start, "Native structure start must not be null");
Objects.requireNonNull(source, "Native structure source must not be null");
Objects.requireNonNull(heightAccessor, "Native structure height accessor must not be null");
if (!start.isValid()) {
return StructureStart.INVALID_START;
}
List<StructurePiece> pieces = start.getPieces();
int minY = Integer.MAX_VALUE;
for (StructurePiece piece : pieces) {
minY = Math.min(minY, piece.getBoundingBox().minY());
}
if (minY == Integer.MAX_VALUE) {
return StructureStart.INVALID_START;
}
int offsetY = Math.subtractExact(targetMinY, minY);
if (offsetY != 0) {
for (StructurePiece piece : pieces) {
moveStructurePiece(piece, offsetY);
}
}
int worldMinY = heightAccessor.getMinY() + 1;
int worldMaxYExclusive = Math.addExact(
heightAccessor.getMinY(), heightAccessor.getHeight());
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
if (bounds.minY() < worldMinY || bounds.maxY() >= worldMaxYExclusive) {
throw new IllegalStateException("Native structure cannot fit target minimum Y "
+ targetMinY + " inside world bounds [" + worldMinY + ","
+ worldMaxYExclusive + ")");
}
}
return new StructureStart(
source,
start.getChunkPos(),
start.getReferences(),
new PiecesContainer(List.copyOf(pieces))
);
}
static int alignScatteredStructureToSurface(StructureStart start, String structureId,
int configuredOffset, int worldMinY,
int worldMaxYExclusive,
IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(surfaceHeight, "Scattered native structure requires a terrain height resolver");
ScatteredFeaturePiece piece = requireAdjustedScatteredPiece(start, structureId);
BoundingBox bounds = start.getBoundingBox();
BoundingBox pieceBounds = piece.getBoundingBox();
int surfaceY = representativeScatteredSurfaceY(structureId, pieceBounds, surfaceHeight);
int targetMinY = Math.addExact(Math.addExact(surfaceY, 1), configuredOffset);
int requestedMove = Math.subtractExact(targetMinY, pieceBounds.minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedMove, worldMinY, worldMaxYExclusive);
if (offsetY != 0) {
moveStructureStart(start, bounds, offsetY);
}
setScatteredHeightPosition(piece, Math.max(0, piece.getBoundingBox().minY()));
return offsetY;
}
public static int applyVerticalShift(StructureStart start, int requestedOffset, int worldMinY,
int worldMaxYExclusive, boolean underground,
boolean preserveSourceY, IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
BoundingBox bounds = start.getBoundingBox();
int resolvedOffset = resolveShiftOffset(start, bounds, requestedOffset, worldMinY,
worldMaxYExclusive, underground, preserveSourceY, yBand, surfaceHeight);
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), resolvedOffset, worldMinY, worldMaxYExclusive);
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
private static int resolveShiftOffset(StructureStart start, BoundingBox bounds, int requestedOffset,
int worldMinY, int worldMaxYExclusive, boolean underground,
boolean preserveSourceY, IrisStructureYBand yBand,
IntBinaryOperator surfaceHeight) {
if (preserveSourceY) {
return requestedOffset;
}
if (yBand != null) {
return resolveYBandOffset(bounds, yBand, start.getChunkPos());
}
if (underground) {
return resolveBuriedOffset(
bounds, requestedOffset, worldMinY, worldMaxYExclusive, surfaceHeight);
}
return requestedOffset;
}
static int alignOceanMonumentToSeaLevel(StructureStart start, int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
OceanMonumentPieces.MonumentBuilding building = requireOceanMonumentBuilding(start);
BoundingBox bounds = start.getBoundingBox();
int targetMinY = Math.addExact(
Math.subtractExact(seaLevel, MONUMENT_BASE_BELOW_SEA_LEVEL), configuredOffset);
int requestedOffset = Math.subtractExact(targetMinY, building.getBoundingBox().minY());
int offsetY = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), requestedOffset, worldMinY, worldMaxYExclusive);
if (offsetY != requestedOffset) {
throw new IllegalStateException("Ocean monument cannot align to sea level " + seaLevel
+ " with configured offset " + configuredOffset + " inside world bounds ["
+ worldMinY + "," + worldMaxYExclusive + ")");
}
if (offsetY == 0) {
return 0;
}
moveStructureStart(start, bounds, offsetY);
return offsetY;
}
static void ensureMonumentSeaLevelAlignment(StructureStart start, String structureId,
int configuredOffset, int seaLevel,
int worldMinY, int worldMaxYExclusive) {
if (isOceanMonument(structureId)) {
alignOceanMonumentToSeaLevel(
start, configuredOffset, seaLevel, worldMinY, worldMaxYExclusive);
}
}
private static boolean isOceanMonument(String structureId) {
return OCEAN_MONUMENT_ID.equals(structureId);
}
private static boolean isAdjustedScatteredStructure(String structureId) {
return DESERT_PYRAMID_ID.equals(structureId) || JUNGLE_PYRAMID_ID.equals(structureId);
}
private static ScatteredFeaturePiece requireAdjustedScatteredPiece(StructureStart start,
String structureId) {
Objects.requireNonNull(start, "Scattered native structure start must not be null");
List<StructurePiece> pieces = start.getPieces();
if (pieces.size() != 1) {
throw new IllegalStateException(structureId + " must contain exactly one scattered piece, found "
+ pieces.size());
}
StructurePiece piece = pieces.get(0);
if (DESERT_PYRAMID_ID.equals(structureId) && piece instanceof DesertPyramidPiece desertPyramid) {
return desertPyramid;
}
if (JUNGLE_PYRAMID_ID.equals(structureId) && piece instanceof JungleTemplePiece jungleTemple) {
return jungleTemple;
}
throw new IllegalStateException(structureId + " contains unexpected piece "
+ piece.getClass().getName());
}
private static int representativeScatteredSurfaceY(String structureId, BoundingBox bounds,
IntBinaryOperator surfaceHeight) {
if (DESERT_PYRAMID_ID.equals(structureId)) {
return lowestSurfaceY(bounds, surfaceHeight);
}
if (JUNGLE_PYRAMID_ID.equals(structureId)) {
return averageSurfaceY(bounds, surfaceHeight);
}
throw new IllegalStateException("Unsupported scattered native structure " + structureId);
}
private static int lowestSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
int lowestY = Integer.MAX_VALUE;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
lowestY = Math.min(lowestY, surfaceHeight.applyAsInt(x, z));
}
}
if (lowestY == Integer.MAX_VALUE) {
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
}
return lowestY;
}
private static int averageSurfaceY(BoundingBox bounds, IntBinaryOperator surfaceHeight) {
long totalY = 0L;
long columns = 0L;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
totalY += surfaceHeight.applyAsInt(x, z);
columns++;
}
}
if (columns == 0L) {
throw new IllegalStateException("Scattered native structure has an empty terrain footprint");
}
return Math.toIntExact(totalY / columns);
}
private static void setScatteredHeightPosition(ScatteredFeaturePiece piece, int heightPosition) {
try {
NativeStructureReflection.ScatteredHeightPositionAccess.FIELD.setInt(piece, heightPosition);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot lock scattered native structure height", error);
}
}
private static OceanMonumentPieces.MonumentBuilding requireOceanMonumentBuilding(StructureStart start) {
Objects.requireNonNull(start, "Ocean monument start must not be null");
List<StructurePiece> pieces = start.getPieces();
if (pieces.size() != 1 || !(pieces.get(0) instanceof OceanMonumentPieces.MonumentBuilding building)) {
throw new IllegalStateException("minecraft:monument must contain exactly one MonumentBuilding, found "
+ pieces.size() + " top-level pieces");
}
return building;
}
private static void moveStructureStart(StructureStart start, BoundingBox cachedBounds, int offsetY) {
for (StructurePiece piece : start.getPieces()) {
moveStructurePiece(piece, offsetY);
}
cachedBounds.move(0, offsetY, 0);
}
private static void moveStructurePiece(StructurePiece piece, int offsetY) {
piece.move(0, offsetY, 0);
if (piece instanceof OceanMonumentPieces.MonumentBuilding building) {
for (StructurePiece child : monumentChildPieces(building)) {
child.move(0, offsetY, 0);
}
}
if (piece instanceof PoolElementStructurePiece poolPiece) {
List<JigsawJunction> junctions = poolPiece.getJunctions();
for (int i = 0; i < junctions.size(); i++) {
JigsawJunction junction = junctions.get(i);
junctions.set(i, new JigsawJunction(
junction.getSourceX(),
junction.getSourceGroundY() + offsetY,
junction.getSourceZ(),
junction.getDeltaY(),
junction.getDestProjection()));
}
}
}
static List<StructurePiece> monumentChildPieces(OceanMonumentPieces.MonumentBuilding building) {
Object value;
try {
value = NativeStructureReflection.MonumentChildPiecesAccess.FIELD.get(building);
} catch (IllegalAccessException error) {
throw new IllegalStateException("Cannot read Ocean Monument child pieces", error);
}
if (!(value instanceof List<?> children)) {
throw new IllegalStateException("Ocean Monument child-pieces field is not a list");
}
List<StructurePiece> pieces = new ArrayList<>(children.size());
for (Object child : children) {
if (!(child instanceof StructurePiece monumentPiece)
|| child.getClass().getEnclosingClass() != OceanMonumentPieces.class) {
throw new IllegalStateException("Ocean Monument child-pieces list contains "
+ (child == null ? "null" : child.getClass().getName()));
}
pieces.add(monumentPiece);
}
return List.copyOf(pieces);
}
static int resolveYBandOffset(BoundingBox bounds, IrisStructureYBand yBand, ChunkPos startChunk) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(startChunk, "Native structure Y band requires a start chunk");
int target = yBandTargetMidpointY(
bounds.minY(), bounds.maxY(), yBand.resolvedMin(), yBand.resolvedMax(), startChunk);
return Math.subtractExact(target, midpointY(bounds.minY(), bounds.maxY()));
}
static int yBandTargetMidpointY(int minY, int maxY, int bandMin, int bandMax, ChunkPos startChunk) {
int height = Math.subtractExact(maxY, minY);
int lowest = Math.addExact(bandMin, height / 2);
int highest = Math.subtractExact(bandMax, height - height / 2);
if (lowest > highest) {
// The band is shorter than the structure, so only its midpoint can be honoured.
return Math.floorDiv(Math.addExact(bandMin, bandMax), 2);
}
int span = highest - lowest + 1;
if (span == 1) {
return lowest;
}
long identity = (long) startChunk.x() * 341873128712L ^ (long) startChunk.z() * 132897987541L;
return lowest + new RNG(identity).nextInt(span);
}
private static int midpointY(int minY, int maxY) {
return minY + (maxY - minY) / 2;
}
static int resolveBuriedOffset(BoundingBox bounds, int requestedOffset, int worldMinY,
int worldMaxYExclusive, IntBinaryOperator surfaceHeight) {
Objects.requireNonNull(bounds, "Native structure bounds must not be null");
Objects.requireNonNull(surfaceHeight, "Underground native structure requires a terrain height resolver");
long width = (long) bounds.maxX() - bounds.minX() + 1L;
long depth = (long) bounds.maxZ() - bounds.minZ() + 1L;
if (width <= 0L || depth <= 0L || width > MAX_BURIAL_COLUMNS / depth) {
throw new IllegalStateException("Underground native structure burial footprint is invalid or exceeds "
+ MAX_BURIAL_COLUMNS + " columns");
}
int maximumOffset = requestedOffset;
for (int x = bounds.minX(); x <= bounds.maxX(); x++) {
for (int z = bounds.minZ(); z <= bounds.maxZ(); z++) {
int allowedTopY = surfaceHeight.applyAsInt(x, z) - UNDERGROUND_SURFACE_CLEARANCE;
maximumOffset = Math.min(maximumOffset, allowedTopY - bounds.maxY());
}
}
int clampedOffset = StructureVerticalBounds.clampOffset(
bounds.minY(), bounds.maxY(), maximumOffset, worldMinY, worldMaxYExclusive);
if (clampedOffset > maximumOffset) {
IrisLogging.warn("Native structure burial at " + bounds.minX() + "," + bounds.minZ()
+ " clamped to world floor: wanted " + maximumOffset + ", used " + clampedOffset);
}
return clampedOffset;
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.core.nms.v26_2_R1;
package art.arcane.iris.nativegen;
import net.minecraft.core.SectionPos;
import net.minecraft.util.Mth;
@@ -29,15 +29,15 @@ import java.util.function.IntBinaryOperator;
* Heightmap's internal "first available" semantics: WORLD_SURFACE_WG counts fluid, OCEAN_FLOOR_WG
* does not.
*/
final class WorldgenTerrainHeightmaps {
public final class WorldgenTerrainHeightmaps {
private static final int COLUMNS = 256;
private static final int PLACEMENT_CHUNK_MARGIN = 1;
private WorldgenTerrainHeightmaps() {
}
static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
Objects.requireNonNull(surfaceFirstFreeY,
"Iris worldgen heightmap priming requires a surface height resolver");
@@ -47,9 +47,9 @@ final class WorldgenTerrainHeightmaps {
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
}
static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
public static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
Objects.requireNonNull(world,
"Iris worldgen heightmap priming requires a generation level");
if (starts == null || starts.isEmpty()) {
@@ -49,8 +49,10 @@ import java.util.stream.Stream;
final class IrisModdedBiomeSource extends BiomeSource {
private static final int BIOME_CACHE_MAX = 262144;
private static final int UNRESOLVED_WARN_KEYS_MAX = 256;
private final BiomeSource serializedSource;
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
private final ConcurrentHashMap<Long, Holder<Biome>> visibleBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
@@ -70,6 +72,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
visibleBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
warnedUnresolvedBiomeKeys.clear();
possibleStructureBiomeKeys = null;
for (StructureStateBiomeSource source : structureStateSources) {
source.clearCache();
@@ -332,7 +335,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
if (customBiome == null) {
return fallbackBiome(registry, quartX, quartY, quartZ, sampler);
return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
}
biomeKey = ModdedWorldgenIds.biomeRef(engine, customBiome.getId());
} else if (resolution.underground()) {
@@ -344,7 +348,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
return resolved == null
? fallbackBiome(registry, quartX, quartY, quartZ, sampler)
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler)
: resolved;
}
@@ -567,12 +571,29 @@ final class IrisModdedBiomeSource extends BiomeSource {
return identifier == null ? null : identifier.toString().toLowerCase(Locale.ROOT);
}
private Holder<Biome> fallbackBiome(Registry<Biome> registry, int quartX, int quartY, int quartZ,
private Holder<Biome> fallbackBiome(Registry<Biome> registry, String unresolvedKey,
int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
Holder<Biome> plains = resolveHolder(registry, "minecraft:plains");
warnUnresolvedBiome(unresolvedKey, plains == null ? "the serialized biome source" : "minecraft:plains",
quartX, quartY, quartZ);
return plains == null ? serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler) : plains;
}
private void warnUnresolvedBiome(String unresolvedKey, String fallback,
int quartX, int quartY, int quartZ) {
String key = unresolvedKey == null || unresolvedKey.isBlank() ? "<blank>" : unresolvedKey;
if (!warnedUnresolvedBiomeKeys.add(key)) {
return;
}
if (warnedUnresolvedBiomeKeys.size() > UNRESOLVED_WARN_KEYS_MAX) {
warnedUnresolvedBiomeKeys.clear();
}
ModdedIrisLog.warn("Iris biome " + key + " is not registered; using " + fallback
+ " at quart " + quartX + "," + quartY + "," + quartZ
+ " (wrong biome generates; regenerate the forced datapack and restart)");
}
private static long packNoiseKey(int x, int y, int z) {
return (((long) x & 67108863L) << 38)
| (((long) z & 67108863L) << 12)
@@ -23,27 +23,15 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureLocateResults;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
@@ -54,9 +42,7 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -83,15 +69,12 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
@@ -100,98 +83,41 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;
public final class IrisModdedChunkGenerator extends ChunkGenerator {
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
public static final MapCodec<IrisModdedChunkGenerator> CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance<IrisModdedChunkGenerator> instance) -> instance.group(
BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.serializedBiomeSource),
Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey)
).apply(instance, IrisModdedChunkGenerator::new));
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static volatile ExecutorService genPool = createGenPool();
public static void startGenPool() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
ModdedGenPool.start();
}
public static void shutdownGenPool() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
"com.ishland.c2me.base.ModProperties",
"com.ishland.c2me.base.common.config.C2MEConfig",
"com.ishland.c2me.opts.chunkio.ModProperties",
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
};
for (String marker : markers) {
try {
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
return true;
} catch (Throwable ignored) {
}
}
return false;
}
private static ExecutorService createGenPool() {
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
threads, threads, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
thread.setDaemon(true);
return thread;
});
pool.allowCoreThreadTimeOut(true);
return pool;
ModdedGenPool.shutdown();
}
private final String dimensionKey;
private final String defaultPack;
private final String defaultDimensionKey;
private final BiomeSource serializedBiomeSource;
private final IrisModdedBiomeSource structureBiomeSource;
private final EngineBinding<Engine> engineBinding = new EngineBinding<>(60L, TimeUnit.SECONDS);
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
final IrisModdedBiomeSource structureBiomeSource;
private final ModdedEngineBinding<Engine> engineBinding = new ModdedEngineBinding<>(60L, TimeUnit.SECONDS);
private final ModdedNativeStructureStage nativeStructures = new ModdedNativeStructureStage(this);
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
private final AtomicBoolean announced = new AtomicBoolean(false);
private volatile boolean vanillaSpawnBiomesInitialized;
private volatile boolean unloading;
private volatile Engine engine;
private volatile String activePack;
@@ -199,8 +125,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L;
private volatile Set<String> configuredStructureBiomeKeys;
private volatile ConfiguredPack configuredPack;
private volatile StructureStepCache structureStepCache;
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
@@ -262,8 +187,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.complete(replacement);
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
public synchronized void unbindEngine() {
@@ -287,8 +212,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.reset();
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
@@ -301,8 +226,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.engineBinding.reset();
this.announced.set(false);
this.structureBiomeSource.clearCaches();
this.worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
this.nativeStructures.clearWorldCheckStructureShifts();
this.spawnTables.resetVanillaSpawnBiomes();
}
public synchronized void resetToDefault() {
@@ -335,9 +260,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Pair<BlockPos, Holder<Structure>> irisPlaced = findNearestIrisStructure(
Pair<BlockPos, Holder<Structure>> irisPlaced = nativeStructures.findNearestIrisStructure(
level, holders, pos, Math.max(1, radius), findUnexplored, current);
HolderSet<Structure> reachable = filterReachableNativeStructures(level, holders, current);
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
level, holders, current);
Pair<BlockPos, Holder<Structure>> nativeLocated = reachable.size() == 0
? null
: super.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
@@ -349,67 +275,6 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return structure != null && structureBiomeSource.isStructureReachable(structure);
}
private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDistance = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
if (!IrisStructureLocator.isPlaced(current, structureId)) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long distance = dx * dx + dz * dz;
if (distance < bestDistance) {
bestDistance = distance;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
return best == null ? null : Pair.of(best, bestHolder);
}
private HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructurePostProcessor.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
private ServerLevel boundLevel() {
MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server == null) {
@@ -423,7 +288,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return null;
}
private Engine engine() {
Engine engine() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
@@ -516,7 +381,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
+ "and deny individual structures through importedStructures.disabled");
}
private Engine engineOrNull() {
Engine engineOrNull() {
requireBindingAllowed();
Engine cached = engine;
requireCompletedShutdown(cached);
@@ -607,7 +472,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (current != null && !current.isClosed() && !current.isClosing()) {
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Set<String> resolved = collectConfiguredBiomeKeys(
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
current.getAllBiomes(), current.getDimension().getLoadKey());
configuredStructureBiomeKeys = resolved;
return resolved;
@@ -617,15 +482,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
}
ConfiguredPack configured = configuredPack();
Set<String> resolved = collectConfiguredBiomeKeys(configured.dimension(), configured.data());
ModdedDimensionMetadata.ConfiguredPack configured = configuredPack();
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
configured.dimension(), configured.data());
configuredStructureBiomeKeys = resolved;
return resolved;
}
}
private ConfiguredPack configuredPack() {
ConfiguredPack cached = configuredPack;
private ModdedDimensionMetadata.ConfiguredPack configuredPack() {
ModdedDimensionMetadata.ConfiguredPack cached = configuredPack;
if (cached != null) {
return cached;
}
@@ -641,60 +507,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
throw new IllegalStateException("Iris dimension '" + activeDimensionKey
+ "' missing from pack " + packDirectory.getAbsolutePath());
}
ConfiguredPack resolved = new ConfiguredPack(data, dimension, dimensionMetadata(dimension));
ModdedDimensionMetadata.ConfiguredPack resolved = new ModdedDimensionMetadata.ConfiguredPack(
data, dimension, ModdedDimensionMetadata.dimensionMetadata(dimension));
configuredPack = resolved;
return resolved;
}
}
static DimensionMetadata dimensionMetadata(IrisDimension dimension) {
int minY = dimension.getMinHeight();
int maxY = dimension.getMaxHeight();
if (maxY <= minY) {
throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey()
+ "' has invalid height range " + minY + ".." + maxY);
}
return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight());
}
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>(
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) {
continue;
}
if (!region.getSeaBiomes().isEmpty()) {
keys.add("minecraft:the_void");
}
if (!region.getShoreBiomes().isEmpty()) {
keys.add("minecraft:beach");
}
}
return Set.copyOf(keys);
}
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
continue;
}
Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
keys.add(derivative.toString().toLowerCase(Locale.ROOT));
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
}
public String dimensionKey() {
return dimensionKey;
}
@@ -715,8 +534,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public void onHotload() {
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
worldCheckStructureShifts.clear();
resetVanillaSpawnBiomes();
nativeStructures.clearWorldCheckStructureShifts();
spawnTables.resetVanillaSpawnBiomes();
}
@Override
@@ -730,8 +549,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = vanillaSpawnBiomes.get(biome.value());
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
@@ -744,57 +563,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return explicitSpawns;
}
SpawnTableKey key = new SpawnTableKey(biome.value(), category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private synchronized void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
if (vanillaSpawnBiomesInitialized) {
return;
}
Engine current = engineOrNull();
if (current == null) {
return;
}
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
}
private synchronized void resetVanillaSpawnBiomes() {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
}
@Override
@@ -817,13 +586,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
PlatformBlockState air = IrisPlatforms.get().registries().air();
if (PARALLEL_CHUNK_SYSTEM) {
if (ModdedGenPool.parallelChunkSystem()) {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, air));
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, air),
genPool);
ModdedGenPool.pool());
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
@@ -944,7 +713,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
placeVanillaStructures(level, chunk, structureManager);
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
}
}
@@ -967,7 +736,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this,
structureBiomeSource
));
adjustGeneratedStructures(
nativeStructures.adjustGeneratedStructures(
registryAccess, chunk, previousStarts, configuredStarts, current, templateManager);
}
}
@@ -981,269 +750,17 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts,
Map<Structure, NativeStructureStartPlan> configuredStarts,
Engine current,
StructureTemplateManager templateManager) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
ChunkPos chunkPos = chunk.getPos();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructurePostProcessor.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructurePostProcessor.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructurePostProcessor.resolveNativeTerrain(start, decision.terrain()));
chunk.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
}
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<List<Structure>> byStep = structuresByStep(registry);
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
Engine current = engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructurePostProcessor.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructurePostProcessor.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructurePostProcessor.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
IrisNativeStructureDecision decision = plan == null
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
if (!decision.generate()) {
continue;
}
resolvedPlacements.add(new NativePlacement(start, decision));
terrainTargets.add(new NativeStructurePostProcessor.TerrainTarget(
structureId, start,
NativeStructurePostProcessor.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructurePostProcessor
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructurePostProcessor.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
structureId, index, step, List.copyOf(resolvedPlacements)));
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
NativeStructurePostProcessor.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain carving", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, this, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> engine().getHeight(x, z, true) + engine().getMinHeight());
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
synchronized (this) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
List<List<Structure>> grouped = new ArrayList<>(steps);
for (int step = 0; step < steps; step++) {
grouped.add(new ArrayList<>());
}
for (Structure structure : registry) {
grouped.get(structure.step().ordinal()).add(structure);
}
for (int step = 0; step < steps; step++) {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
return resolved;
}
}
private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) {
if (!WORLD_CHECK_ENABLED || structureId == null) {
return;
}
if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) {
worldCheckStructureShifts.clear();
}
worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY);
}
Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) {
if (structureId == null || startChunk == null) {
return null;
}
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
}
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
int x, int y, int z) {
PlatformBlockState platformState = palette.get(rng, x, y, z, engine().getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
int minY = chunk.getMinY();
int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
return nativeStructures.worldCheckStructureShift(structureId, startChunk);
}
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
initializeVanillaSpawnBiomes(registry);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Holder<Biome> vanillaBiome = vanillaSpawnBiomes.get(visibleBiome.value());
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
@@ -1276,13 +793,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public int getSpawnHeight(LevelHeightAccessor heightAccessor) {
return clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
}
static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1;
int maximum = minY + height - 2;
return Math.max(minimum, Math.min(maximum, 96));
return ModdedDimensionMetadata.clampSpawnHeight(heightAccessor.getMinY(), heightAccessor.getHeight());
}
@Override
@@ -1320,7 +831,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private GenerationSessionLease requireGenerationLease(Engine current, String operation) {
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
try {
return current.acquireGenerationLease(operation);
} catch (GenerationSessionException exception) {
@@ -1333,80 +844,4 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
info.add("Iris dimension: " + dimensionKey);
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
}
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
}
private record NativePlacementGroup(String structureId, int featureIndex, int step,
List<NativePlacement> placements) {
}
record DimensionMetadata(int minY, int maxY, int seaLevel) {
int depth() {
return maxY - minY;
}
}
private record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
}
static final class EngineBinding<T> {
private final long timeout;
private final TimeUnit timeoutUnit;
private volatile CompletableFuture<T> future = new CompletableFuture<>();
EngineBinding(long timeout, TimeUnit timeoutUnit) {
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
T await(String dimensionKey) {
try {
return future.get(timeout, timeoutUnit);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
} catch (ExecutionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
} catch (TimeoutException error) {
throw new IllegalStateException("Timed out waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
}
}
void complete(T value) {
future.complete(value);
}
void fail(Throwable error) {
future.completeExceptionally(error);
}
void throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
}
}
@@ -45,6 +45,7 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class ModdedBlockResolution {
@@ -86,6 +87,8 @@ public final class ModdedBlockResolution {
"acacia_leaves", "birch_leaves", "dark_oak_leaves", "jungle_leaves", "oak_leaves", "spruce_leaves");
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private static final UnresolvedKeyLog UNRESOLVED = new UnresolvedKeyLog("Iris modded block resolution", 30_000L);
private static final int REPORTED_FAILURE_KEYS_MAX = 256;
private static final Set<String> REPORTED_FAILURE_KEYS = ConcurrentHashMap.newKeySet();
private ModdedBlockResolution() {
}
@@ -152,6 +155,17 @@ public final class ModdedBlockResolution {
return map;
}
private static void reportResolveFailure(String key, Throwable error) {
String failureKey = key == null ? "<null>" : key;
if (!REPORTED_FAILURE_KEYS.add(failureKey)) {
return;
}
if (REPORTED_FAILURE_KEYS.size() > REPORTED_FAILURE_KEYS_MAX) {
REPORTED_FAILURE_KEYS.clear();
}
IrisLogging.reportError("Iris block data '" + failureKey + "' failed to resolve", error);
}
private static void warnUnresolved(String key, String message) {
if (UNRESOLVED.firstOccurrence(key)) {
IrisLogging.warn(message);
@@ -239,7 +253,7 @@ public final class ModdedBlockResolution {
return bdx;
} catch (Throwable e) {
e.printStackTrace();
reportResolveFailure(bdxf, e);
if (warn) {
warnUnresolved(bdxf, "Unknown Block Data '" + bdxf + "'");
}
@@ -38,6 +38,7 @@ public final class ModdedBlockState implements PlatformBlockState {
private final String key;
private final String namespace;
private final String deferredPlacementKey;
private volatile String materialKey;
private volatile Boolean air;
private volatile Boolean solid;
private volatile Boolean occluding;
@@ -148,6 +149,17 @@ public final class ModdedBlockState implements PlatformBlockState {
return namespace;
}
@Override
public String materialKey() {
String cached = materialKey;
if (cached == null) {
int bracket = key.indexOf('[');
cached = bracket < 0 ? key : key.substring(0, bracket);
materialKey = cached;
}
return cached;
}
@Override
public boolean isAir() {
Boolean cached = air;
@@ -0,0 +1,98 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import net.minecraft.resources.Identifier;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
final class ModdedDimensionMetadata {
private ModdedDimensionMetadata() {
}
static DimensionMetadata dimensionMetadata(IrisDimension dimension) {
int minY = dimension.getMinHeight();
int maxY = dimension.getMaxHeight();
if (maxY <= minY) {
throw new IllegalStateException("Iris dimension '" + dimension.getLoadKey()
+ "' has invalid height range " + minY + ".." + maxY);
}
return new DimensionMetadata(minY, maxY, minY + dimension.getFluidHeight());
}
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>(
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) {
continue;
}
if (!region.getSeaBiomes().isEmpty()) {
keys.add("minecraft:the_void");
}
if (!region.getShoreBiomes().isEmpty()) {
keys.add("minecraft:beach");
}
}
return Set.copyOf(keys);
}
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
continue;
}
Identifier derivative = Identifier.tryParse(irisBiome.getStructureDerivativeKey());
if (derivative != null) {
keys.add(derivative.toString().toLowerCase(Locale.ROOT));
}
if (!irisBiome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
}
static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1;
int maximum = minY + height - 2;
return Math.max(minimum, Math.min(maximum, 96));
}
record DimensionMetadata(int minY, int maxY, int seaLevel) {
int depth() {
return maxY - minY;
}
}
record ConfiguredPack(IrisData data, IrisDimension dimension, DimensionMetadata metadata) {
}
}
@@ -50,8 +50,12 @@ public final class ModdedDimensionRegistryStore {
}
static List<PersistentDimension> load(Path file) {
return contents(file).dimensions();
}
private static Contents contents(Path file) {
if (!Files.isRegularFile(file)) {
return new ArrayList<>();
return new Contents(new ArrayList<>(), new ArrayList<>());
}
try {
JSONObject root = new JSONObject(Files.readString(file, StandardCharsets.UTF_8));
@@ -60,7 +64,9 @@ public final class ModdedDimensionRegistryStore {
throw new IllegalArgumentException("registry root has no dimensions array");
}
Map<String, PersistentDimension> deduplicated = new LinkedHashMap<>();
List<Object> unparsed = new ArrayList<>();
for (int index = 0; index < entries.length(); index++) {
Object raw = entries.opt(index);
try {
JSONObject entry = entries.getJSONObject(index);
String id = required(entry, "id", index, file);
@@ -72,14 +78,18 @@ public final class ModdedDimensionRegistryStore {
PersistentDimension previous = deduplicated.putIfAbsent(
id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
if (previous != null) {
throw new IllegalArgumentException("duplicate id '" + id + "'");
LOGGER.warn("Iris persistent dimension registry entry {} in {} duplicates id '{}'; keeping the first",
index, file, id);
}
} catch (RuntimeException invalidEntry) {
LOGGER.error("Iris persistent dimension registry entry {} in {} is invalid; skipping only that entry",
index, file, invalidEntry);
if (raw != null) {
unparsed.add(raw);
}
LOGGER.warn("Iris persistent dimension registry entry {} in {} is invalid ({}); kept verbatim: {}",
index, file, invalidEntry.getMessage(), raw);
}
}
return new ArrayList<>(deduplicated.values());
return new Contents(new ArrayList<>(deduplicated.values()), unparsed);
} catch (RuntimeException | IOException e) {
throw new IllegalStateException("Iris persistent dimension registry at " + file
+ " could not be read; refusing to discard persistent worlds", e);
@@ -91,15 +101,19 @@ public final class ModdedDimensionRegistryStore {
}
public static synchronized void put(MinecraftServer server, PersistentDimension dimension) {
Map<String, PersistentDimension> current = index(load(server));
Path file = storeFile(server);
Contents contents = contents(file);
Map<String, PersistentDimension> current = index(contents.dimensions());
current.put(dimension.id(), dimension);
write(server, new ArrayList<>(current.values()));
write(file, new ArrayList<>(current.values()), contents.unparsed());
}
public static synchronized void remove(MinecraftServer server, String id) {
Map<String, PersistentDimension> current = index(load(server));
Path file = storeFile(server);
Contents contents = contents(file);
Map<String, PersistentDimension> current = index(contents.dimensions());
if (current.remove(id) != null) {
write(server, new ArrayList<>(current.values()));
write(file, new ArrayList<>(current.values()), contents.unparsed());
}
}
@@ -119,11 +133,11 @@ public final class ModdedDimensionRegistryStore {
return value;
}
private static void write(MinecraftServer server, List<PersistentDimension> dimensions) {
write(storeFile(server), dimensions);
static void write(Path file, List<PersistentDimension> dimensions) {
write(file, dimensions, List.of());
}
static void write(Path file, List<PersistentDimension> dimensions) {
private static void write(Path file, List<PersistentDimension> dimensions, List<Object> unparsed) {
JSONArray entries = new JSONArray();
for (PersistentDimension dimension : dimensions) {
JSONObject entry = new JSONObject();
@@ -133,6 +147,9 @@ public final class ModdedDimensionRegistryStore {
entry.put("seed", dimension.seed());
entries.put(entry);
}
for (Object entry : unparsed) {
entries.put(entry);
}
JSONObject root = new JSONObject();
root.put("dimensions", entries);
Path temp = file.resolveSibling(FILE_NAME + ".tmp");
@@ -167,4 +184,7 @@ public final class ModdedDimensionRegistryStore {
public record PersistentDimension(String id, String pack, String dimension, long seed) {
}
private record Contents(List<PersistentDimension> dimensions, List<Object> unparsed) {
}
}
@@ -0,0 +1,77 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class ModdedEngineBinding<T> {
private final long timeout;
private final TimeUnit timeoutUnit;
private volatile CompletableFuture<T> future = new CompletableFuture<>();
ModdedEngineBinding(long timeout, TimeUnit timeoutUnit) {
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
T await(String dimensionKey) {
try {
return future.get(timeout, timeoutUnit);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
} catch (ExecutionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
} catch (TimeoutException error) {
throw new IllegalStateException("Timed out waiting for Iris generator '"
+ dimensionKey + "' to bind", error);
}
}
void complete(T value) {
future.complete(value);
}
void fail(Throwable error) {
future.completeExceptionally(error);
}
void throwIfFailed(String dimensionKey) {
CompletableFuture<T> current = future;
if (!current.isCompletedExceptionally()) {
return;
}
try {
current.join();
} catch (CompletionException error) {
throw new IllegalStateException("Iris generator '" + dimensionKey + "' failed to bind",
error.getCause());
}
}
void reset() {
future = new CompletableFuture<>();
}
}
@@ -342,7 +342,7 @@ public final class ModdedEntitySpawner {
double addition = RNG.r.d();
double subtraction = RNG.r.d();
double particleX = entity.getX() + addition - subtraction + RNG.r.d();
double particleY = entity.getY() + 0.25 + addition - subtraction + level.getMinY() + RNG.r.i(effect.getParticleOffset());
double particleY = entity.getY() + 0.25 + addition - subtraction + RNG.r.i(effect.getParticleOffset());
double particleZ = entity.getZ() + addition - subtraction + RNG.r.d();
double altX = effect.isRandomAltX() ? RNG.r.d(-effect.getParticleAltX(), effect.getParticleAltX()) : effect.getParticleAltX();
double altY = effect.isRandomAltY() ? RNG.r.d(-effect.getParticleAltY(), effect.getParticleAltY()) : effect.getParticleAltY();
@@ -55,6 +55,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Stream;
@@ -63,6 +64,7 @@ public final class ModdedForcedDatapack {
private static final String PACK_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris";
private static final Object LOCK = new Object();
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
private ModdedForcedDatapack() {
}
@@ -71,9 +73,26 @@ public final class ModdedForcedDatapack {
return (Consumer<Pack> consumer) -> {
Pack pack = buildPack();
consumer.accept(pack);
LOADED.set(true);
};
}
public static void verifyInjected() {
if (LOADED.get()) {
return;
}
Path packsRoot = packsRoot();
File[] packs = packsRoot.toFile().listFiles(File::isDirectory);
if (packs == null || packs.length == 0) {
return;
}
LOGGER.error("===============================================================");
LOGGER.error("Iris forced datapack '{}' was never loaded by this server.", PACK_ID);
LOGGER.error("{} installed pack(s) at {} contributed no dimension types or custom biomes.", packs.length, packsRoot);
LOGGER.error("Datapack source injection failed for this loader (mixin/event not applied), so world creation will fail and restarting will not fix it.");
LOGGER.error("===============================================================");
}
public static Path datapackRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("generated").resolve("datapack");
}
@@ -310,7 +329,9 @@ public final class ModdedForcedDatapack {
String pack, String packDimensionKey) {
return registeredType.orElseThrow(() -> new IllegalStateException(
"Iris dimension type '" + typeRef + "' for pack '" + pack + "' dimension '"
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."));
+ packDimensionKey + "' is not loaded. Restart the server so the forced Iris datapack registers it before creating the world."
+ (LOADED.get() ? "" : " The forced Iris datapack has not been loaded by this server at all"
+ " (datapack source injection failed; see the Iris boot ERROR), so a restart alone will not register it.")));
}
private static void writeWorldPreset(KList<File> folders, String packName, String dimensionKey,
@@ -0,0 +1,87 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
final class ModdedGenPool {
private static final AtomicInteger GEN_THREAD_SEQ = new AtomicInteger();
private static final boolean PARALLEL_CHUNK_SYSTEM = detectParallelChunkSystem();
private static volatile ExecutorService genPool = createGenPool();
private ModdedGenPool() {
}
static boolean parallelChunkSystem() {
return PARALLEL_CHUNK_SYSTEM;
}
static ExecutorService pool() {
return genPool;
}
static void start() {
ExecutorService pool = genPool;
if (pool == null || pool.isShutdown()) {
genPool = createGenPool();
}
}
static void shutdown() {
ExecutorService pool = genPool;
if (pool != null) {
pool.shutdownNow();
}
}
private static boolean detectParallelChunkSystem() {
String[] markers = {
"com.ishland.c2me.base.ModProperties",
"com.ishland.c2me.base.common.config.C2MEConfig",
"com.ishland.c2me.opts.chunkio.ModProperties",
"ca.spottedleaf.moonrise.common.util.MoonriseCommon"
};
for (String marker : markers) {
try {
Class.forName(marker, false, IrisModdedChunkGenerator.class.getClassLoader());
return true;
} catch (Throwable ignored) {
}
}
return false;
}
private static ExecutorService createGenPool() {
int threads = Math.max(2, Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor pool = new ThreadPoolExecutor(
threads, threads, 30L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
runnable -> {
Thread thread = new Thread(runnable, "Iris ModGen-" + GEN_THREAD_SEQ.incrementAndGet());
thread.setDaemon(true);
return thread;
});
pool.allowCoreThreadTimeOut(true);
return pool;
}
}
@@ -43,10 +43,11 @@ public final class ModdedIrisLog {
public static void debug(String message) {
if (!debugEnabled()) {
LOGGER.debug(clean(message));
return;
}
LOGGER.debug(clean(message));
LOGGER.info("[Iris/DEBUG] " + clean(message));
}
public static void info(String message) {
@@ -267,15 +267,21 @@ public final class ModdedLootApplier {
for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) {
ItemStack existing = container.getItem(i);
if (existing.isEmpty()) {
container.setItem(i, stack);
return;
container.setItem(i, stack.split(container.getMaxStackSize(stack)));
continue;
}
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount());
existing.grow(move);
stack.shrink(move);
if (ItemStack.isSameItemSameComponents(existing, stack)) {
int limit = container.getMaxStackSize(existing);
if (existing.getCount() < limit) {
int move = Math.min(stack.getCount(), limit - existing.getCount());
existing.grow(move);
stack.shrink(move);
}
}
}
if (!stack.isEmpty()) {
IrisLogging.debug("Iris loot: container full, dropped " + stack.getCount() + "x " + stack.getItem());
}
}
private static void scramble(Container container, RNG rng) {
@@ -0,0 +1,439 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import art.arcane.iris.nativegen.NativeStructurePostProcessor;
import art.arcane.iris.nativegen.NativeStructureReferenceEnvelope;
import art.arcane.iris.nativegen.NativeStructureSurfaceFitter;
import art.arcane.iris.nativegen.NativeStructureTerrainIntegrator;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import art.arcane.iris.nativegen.NativeStructureVerticalPlacer;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.GenerationStep;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntBinaryOperator;
/**
* Native (vanilla registry) structure stage for {@link IrisModdedChunkGenerator}. The generator keeps the
* {@link net.minecraft.world.level.chunk.ChunkGenerator} overrides because they issue {@code super} calls;
* everything they do beyond that lives here.
*/
final class ModdedNativeStructureStage {
private static final int WORLD_CHECK_SHIFT_RECORD_LIMIT = 4096;
private static final boolean WORLD_CHECK_ENABLED = Boolean.getBoolean("iris.worldcheck");
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<NativeStructureStartKey, Integer> worldCheckStructureShifts = new ConcurrentHashMap<>();
private volatile StructureStepCache structureStepCache;
ModdedNativeStructureStage(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(ServerLevel level,
HolderSet<Structure> holders,
BlockPos pos, int radius, boolean findUnexplored,
Engine current) {
if (findUnexplored) {
return null;
}
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
BlockPos best = null;
Holder<Structure> bestHolder = null;
long bestDistance = Long.MAX_VALUE;
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure locate received an unregistered structure holder");
}
String structureId = id.toString();
if (!IrisStructureLocator.isPlaced(current, structureId)) {
continue;
}
IrisStructureLocator.LocateResult result = IrisStructureLocator.locate(
current, structureId, pos.getX(), pos.getZ(), radius);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
throw new IllegalStateException("Iris structure locate reached its safety limit for "
+ structureId + " within " + radius + " chunks");
}
if (!result.found()) {
continue;
}
long dx = (long) result.originX() - pos.getX();
long dz = (long) result.originZ() - pos.getZ();
long distance = dx * dx + dz * dz;
if (distance < bestDistance) {
bestDistance = distance;
best = new BlockPos(result.originX(), result.baseY(), result.originZ());
bestHolder = holder;
}
}
return best == null ? null : Pair.of(best, bestHolder);
}
HolderSet<Structure> filterReachableNativeStructures(ServerLevel level, HolderSet<Structure> holders,
Engine current) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> kept = new ArrayList<>(holders.size());
for (Holder<Structure> holder : holders) {
Identifier id = registry.getKey(holder.value());
if (id == null) {
throw new IllegalStateException("Native structure filtering received an unregistered structure holder");
}
String key = id.toString();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(current,
key, NativeStructureVegetationClearer.isUndergroundStep(holder.value().step()));
if (!decision.generate() || !generator.structureBiomeSource.isStructureReachable(holder)) {
continue;
}
kept.add(holder);
}
return kept.size() == holders.size() ? holders : HolderSet.direct(kept);
}
void adjustGeneratedStructures(RegistryAccess registryAccess, ChunkAccess chunk,
Map<Structure, StructureStart> previousStarts,
Map<Structure, NativeStructureStartPlan> configuredStarts,
Engine current,
StructureTemplateManager templateManager) {
Registry<Structure> registry = registryAccess.lookupOrThrow(Registries.STRUCTURE);
ChunkPos chunkPos = chunk.getPos();
for (Map.Entry<Structure, StructureStart> entry : chunk.getAllStarts().entrySet()) {
Structure structure = entry.getKey();
StructureStart start = entry.getValue();
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
decision = NativeStructureGenerationPolicy.resolve(current,
structureId, undergroundStep);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"policy resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
if (!decision.generate()) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
continue;
}
int offsetY;
try {
offsetY = NativeStructureVerticalPlacer.applyVerticalPlacement(
start,
structureId,
decision.yShift(),
generator.getSeaLevel(),
chunk.getMinY(),
chunk.getMinY() + chunk.getHeight(),
undergroundStep,
decision.preserveSourceY(),
decision.yBand(),
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
StructureStart wrapped = NativeStructureReferenceEnvelope.wrap(
start, structure, start.getReferences(), templateManager,
NativeStructureTerrainIntegrator.resolveNativeTerrain(start, decision.terrain()));
chunk.setStartForStructure(structure, wrapped);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vertical adjustment", structureId, chunkPos.x(), chunkPos.z(), error);
}
recordWorldCheckStructureShift(structureId, start.getChunkPos(), offsetY);
}
}
void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures()) {
ChunkPos disabledChunk = chunk.getPos();
throw new IllegalStateException("Iris cannot generate native structures in chunk "
+ disabledChunk.x() + "," + disabledChunk.z()
+ " because generate-structures=false disables them outside the pack; set generate-structures=true, "
+ "restart the server, and deny individual structures through importedStructures.disabled");
}
ChunkPos chunkPos = chunk.getPos();
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
BlockPos origin = sectionPos.origin();
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<List<Structure>> byStep = structuresByStep(registry);
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long decorationSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
BoundingBox area = writableArea(chunk);
int steps = GenerationStep.Decoration.values().length;
Engine current = generator.engine();
List<NativePlacementGroup> placementGroups = new ArrayList<>();
List<StructureStart> heightmapStarts = new ArrayList<>();
List<StructureStart> nativeStarts = new ArrayList<>();
List<NativeStructureVegetationClearer.VegetationTarget> vegetationTargets = new ArrayList<>();
List<NativeStructureTerrainIntegrator.TerrainTarget> terrainTargets = new ArrayList<>();
for (int step = 0; step < steps; step++) {
int index = 0;
for (Structure structure : byStep.get(step)) {
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
try {
IrisNativeStructureDecision sourceDecision = NativeStructureGenerationPolicy.resolve(current,
structureId, NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
NativeStructureStartPlan plan = NativeStructurePlacementPlanner.matchingPlan(
current, structureId, start.getChunkPos().x(), start.getChunkPos().z());
IrisNativeStructureDecision decision = plan == null
? sourceDecision : NativeStructurePlacementPlanner.decisionFor(plan);
if (!decision.generate()) {
continue;
}
resolvedPlacements.add(new NativePlacement(start, decision));
heightmapStarts.add(start);
terrainTargets.add(new NativeStructureTerrainIntegrator.TerrainTarget(
structureId, start,
NativeStructureTerrainIntegrator.resolveNativeTerrain(
start, decision.terrain())));
if (plan == null || !plan.placement().isUnderground()) {
nativeStarts.add(start);
}
boolean clearEntireFootprint = NativeStructureVegetationClearer
.shouldClearEntireVegetationFootprint(
structure.step(), decision.clearVegetation());
vegetationTargets.add(new NativeStructureVegetationClearer.VegetationTarget(
start, clearEntireFootprint));
}
if (!resolvedPlacements.isEmpty()) {
placementGroups.add(new NativePlacementGroup(
structureId, index, step, List.copyOf(resolvedPlacements)));
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"resolution", structureId, chunkPos.x(), chunkPos.z(), error);
}
index++;
}
}
try {
int runtimeMinY = world.getMinY();
WorldgenTerrainHeightmaps.primeStructurePlacement(
world, heightmapStarts,
worldgenSurfaceHeight(current, runtimeMinY),
worldgenFloorHeight(current, runtimeMinY));
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"heightmap priming", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructureSurfaceFitter.prepareSurfaceStructures(
world, area, nativeStarts,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain integration", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructureVegetationClearer.clearIntersectingVegetation(
world, chunk, area, vegetationTargets);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"vegetation cleanup", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
try {
NativeStructurePostProcessor.prepareTerrain(
world, area, terrainTargets, this::resolvePaletteBlock);
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"terrain carving", nativeStructureBatchContext(placementGroups),
chunkPos.x(), chunkPos.z(), error);
}
for (NativePlacementGroup group : placementGroups) {
random.setFeatureSeed(decorationSeed, group.featureIndex(), group.step());
try {
for (NativePlacement placement : group.placements()) {
placeVanillaStructure(world, structureManager, random, area, chunkPos,
group.structureId(), placement.start(), placement.decision());
}
} catch (Throwable error) {
throw NativeStructureGenerationException.failure(
"placement", group.structureId(), chunkPos.x(), chunkPos.z(), error);
}
}
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
}
StringBuilder context = new StringBuilder("[");
for (int i = 0; i < placementGroups.size(); i++) {
if (i > 0) {
context.append(", ");
}
context.append(placementGroups.get(i).structureId());
}
return context.append(']').toString();
}
private void placeVanillaStructure(WorldGenLevel world, StructureManager structureManager,
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> generator.engine().getHeight(x, z, true) + generator.engine().getMinHeight());
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
synchronized (generator) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
List<List<Structure>> grouped = new ArrayList<>(steps);
for (int step = 0; step < steps; step++) {
grouped.add(new ArrayList<>());
}
for (Structure structure : registry) {
grouped.get(structure.step().ordinal()).add(structure);
}
for (int step = 0; step < steps; step++) {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
return resolved;
}
}
private void recordWorldCheckStructureShift(String structureId, ChunkPos startChunk, int offsetY) {
if (!WORLD_CHECK_ENABLED || structureId == null) {
return;
}
if (worldCheckStructureShifts.size() >= WORLD_CHECK_SHIFT_RECORD_LIMIT) {
worldCheckStructureShifts.clear();
}
worldCheckStructureShifts.put(new NativeStructureStartKey(structureId, startChunk.pack()), offsetY);
}
Integer worldCheckStructureShift(String structureId, ChunkPos startChunk) {
if (structureId == null || startChunk == null) {
return null;
}
return worldCheckStructureShifts.get(new NativeStructureStartKey(structureId, startChunk.pack()));
}
void clearWorldCheckStructureShifts() {
worldCheckStructureShifts.clear();
}
private BlockState resolvePaletteBlock(IrisMaterialPalette palette, RNG rng,
int x, int y, int z) {
PlatformBlockState platformState = palette.get(rng, x, y, z, generator.engine().getData());
if (platformState == null || !(platformState.nativeHandle() instanceof BlockState blockState)) {
throw new IllegalStateException("Configured native structure palette did not resolve a Minecraft block at "
+ x + "," + y + "," + z);
}
return blockState;
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
int minY = chunk.getMinY();
int maxY = minY + chunk.getHeight() - 1;
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
}
private IntBinaryOperator worldgenSurfaceHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, false) + runtimeMinY + 1;
}
private IntBinaryOperator worldgenFloorHeight(Engine generationEngine, int runtimeMinY) {
return (x, z) -> generationEngine.getHeight(x, z, true) + runtimeMinY + 1;
}
private record NativeStructureStartKey(String structureId, long chunkPosition) {
}
private record NativePlacement(StructureStart start, IrisNativeStructureDecision decision) {
}
private record NativePlacementGroup(String structureId, int featureIndex, int step,
List<NativePlacement> placements) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -141,8 +142,8 @@ public final class ModdedPlatform implements IrisPlatform {
}
@Override
public boolean spawnEntity(Object world, String entityKey, double x, double y, double z) {
if (!(world instanceof ServerLevel level) || entityKey == null) {
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
if (world == null || entityKey == null || !(world.nativeHandle() instanceof ServerLevel level)) {
return false;
}
PlatformEntityType resolved = registries.entity(entityKey);
@@ -0,0 +1,119 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.util.project.context.IrisContext;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.util.random.WeightedList;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Vanilla-derivative spawn table state for {@link IrisModdedChunkGenerator}. Every mutator locks the
* generator monitor because the repoint/bind/reset paths already hold it while resetting this state.
*/
final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator;
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile boolean vanillaSpawnBiomesInitialized;
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
synchronized (generator) {
if (vanillaSpawnBiomesInitialized) {
return;
}
Engine current = generator.engineOrNull();
if (current == null) {
return;
}
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
if (irisBiome == null || !irisBiome.isCustom()) {
continue;
}
Holder<Biome> vanillaHolder = resolveBiomeHolder(registry, irisBiome.getVanillaDerivativeKey());
if (vanillaHolder == null) {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
if (customHolder != null) {
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
}
}
vanillaSpawnBiomesInitialized = true;
}
}
}
Holder<Biome> vanillaSpawnBiome(Biome biome) {
return vanillaSpawnBiomes.get(biome);
}
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
Biome biome, MobCategory category,
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category);
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
return null;
}
Optional<Holder.Reference<Biome>> reference = registry.get(identifier);
return reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElse(null);
}
void resetVanillaSpawnBiomes() {
synchronized (generator) {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
vanillaSpawnBiomesInitialized = false;
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
}
}
@@ -73,6 +73,7 @@ public final class ModdedStartup {
if (!STARTED.compareAndSet(false, true)) {
return;
}
ModdedForcedDatapack.verifyInjected();
reinjectPersistentDimensions(server);
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
@@ -88,6 +89,8 @@ public final class ModdedStartup {
File[] packDirs = packsRoot.listFiles(File::isDirectory);
PackValidationRegistry.clear();
if (packDirs == null || packDirs.length == 0) {
LOGGER.info("Iris found no packs to validate under {}; install one with /iris download <pack>",
packsRoot.getAbsolutePath());
return;
}
for (File packDir : packDirs) {
@@ -182,13 +182,28 @@ public final class ModdedStateRotator implements IrisObjectRotation.StateRotator
private static Property<?> findRotation(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("rotation") && property instanceof IntegerProperty) {
if (property.getName().equals("rotation")
&& property instanceof IntegerProperty integer
&& isFullRotationCycle(integer)) {
return property;
}
}
return null;
}
private static boolean isFullRotationCycle(IntegerProperty property) {
List<Integer> values = property.getPossibleValues();
if (values.size() != ROTATION_CYCLE_MODS.length) {
return false;
}
for (int value : values) {
if (value < 0 || value >= ROTATION_CYCLE_MODS.length) {
return false;
}
}
return true;
}
private static Property<?> findAxis(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property.getName().equals("axis") && property.getValueClass() == Direction.Axis.class) {
@@ -41,7 +41,7 @@ import java.util.Locale;
import java.util.function.Supplier;
public final class ModdedTileReader implements TileData.TileReader {
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).create();
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setStrictness(Strictness.LENIENT).setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE).create();
private static final int DYE_COLOR_COUNT = 16;
private static final Identifier DEFAULT_SPAWNER_ENTITY = Identifier.parse("minecraft:pig");
private static final Identifier DEFAULT_BANNER_PATTERN = Identifier.parse("minecraft:base");
File diff suppressed because it is too large Load Diff
@@ -100,8 +100,8 @@ public final class ModdedWorldEngines {
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
ModdedEngineBootstrap.bind();
PackValidationRegistry.requireLoadable(pack);
File packDir = resolvePack(pack, dimensionKey);
PackValidationRegistry.requireLoadable(pack);
IrisData data = IrisData.openRuntime(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
@@ -0,0 +1,114 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.json.JSONObject;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.dimension.DimensionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class WorldCheckDimensionContract {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckDimensionContract() {
}
static boolean checkDimensionType(ServerLevel level, IrisModdedChunkGenerator generator) {
try {
IrisDimension dimension = generator.commandEngine().getDimension();
DimensionContract expected = expectedDimensionContract(dimension);
DimensionContract actual = runtimeDimensionContract(level.dimensionType());
boolean pass = matchesDimensionContract(level.getMinY(), level.getHeight(), expected, actual);
String detail = "expected=" + expected + ",actual=" + actual
+ ",levelMinY=" + level.getMinY() + ",levelHeight=" + level.getHeight();
WorldCheckPredicates.qaEvent("dimension_type", dimension.getLoadKey(), pass, detail);
if (!pass) {
LOGGER.error("[worldcheck] dimension type mismatch for {}: {}", dimension.getLoadKey(), detail);
} else {
LOGGER.info("[worldcheck] dimension type contract: {}", detail);
}
return pass;
} catch (Throwable error) {
LOGGER.error("[worldcheck] could not validate the Iris dimension type contract", error);
WorldCheckPredicates.qaEvent("dimension_type", generator.activeDimensionKey(), false,
"validationError=" + error.getClass().getSimpleName() + ":" + error.getMessage());
return false;
}
}
static DimensionContract expectedDimensionContract(IrisDimension dimension) {
JSONObject json = new JSONObject(dimension.getDimensionType().toJson(DataVersion.getLatest().get()));
return new DimensionContract(
json.getInt("min_y"),
json.getInt("height"),
json.getInt("logical_height"),
json.getDouble("coordinate_scale"),
(float) json.getDouble("ambient_light"),
json.getBoolean("has_skylight"),
json.getBoolean("has_ceiling"),
json.getBoolean("has_ender_dragon_fight"),
json.getInt("monster_spawn_block_light_limit"));
}
static DimensionContract runtimeDimensionContract(DimensionType dimensionType) {
return new DimensionContract(
dimensionType.minY(),
dimensionType.height(),
dimensionType.logicalHeight(),
dimensionType.coordinateScale(),
dimensionType.ambientLight(),
dimensionType.hasSkyLight(),
dimensionType.hasCeiling(),
dimensionType.hasEnderDragonFight(),
dimensionType.monsterSpawnBlockLightLimit());
}
static boolean matchesDimensionContract(int levelMinY, int levelHeight,
DimensionContract expected, DimensionContract actual) {
return levelMinY == expected.minY()
&& levelHeight == expected.height()
&& actual.equals(expected);
}
static boolean checkEntityMixins(ServerLevel level) {
ItemEntity item = new ItemEntity(level, 0D, level.getMinY(), 0D, Items.COBBLESTONE.getDefaultInstance());
boolean vanillaSave = item.shouldBeSaved();
ModdedEntityPersistence.configure(item, false);
boolean suppressed = !item.shouldBeSaved();
ModdedEntityPersistence.configure(item, true);
boolean restored = item.shouldBeSaved();
boolean pass = vanillaSave && suppressed && restored;
WorldCheckPredicates.qaEvent("entity_mixin", "persistence", pass,
"vanilla=" + vanillaSave + ",suppressed=" + suppressed + ",restored=" + restored);
if (!pass) {
LOGGER.error("[worldcheck] shared entity mixins are not active on this loader");
}
return pass;
}
record DimensionContract(int minY, int height, int logicalHeight, double coordinateScale,
float ambientLight, boolean hasSkyLight, boolean hasCeiling,
boolean hasEnderDragonFight, int monsterSpawnBlockLightLimit) {
}
}
@@ -0,0 +1,130 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import net.minecraft.resources.Identifier;
final class WorldCheckMaterials {
private WorldCheckMaterials() {
}
static boolean isCharacteristicMaterial(String structureLabel, Identifier structureKey, Identifier blockKey) {
if (structureKey == null || blockKey == null || !blockKey.getNamespace().equals("minecraft")) {
return false;
}
String block = blockKey.getPath();
return switch (structureLabel) {
case "stronghold" -> isStrongholdMaterial(block);
case "trial_chambers" -> isTrialChamberMaterial(block);
case "mansion" -> isWoodConstructionMaterial(block, "dark_oak")
|| isWoodConstructionMaterial(block, "birch")
|| isCobblestoneConstructionMaterial(block);
case "village" -> isVillageMaterial(structureKey.getPath(), block);
case "monument" -> isMonumentMaterial(block);
default -> false;
};
}
private static boolean isStrongholdMaterial(String block) {
return block.equals("stone_bricks")
|| block.equals("cracked_stone_bricks")
|| block.equals("mossy_stone_bricks")
|| block.equals("infested_stone_bricks")
|| block.equals("infested_cracked_stone_bricks")
|| block.equals("infested_mossy_stone_bricks")
|| block.equals("stone_brick_stairs")
|| block.equals("stone_brick_slab")
|| block.equals("stone_brick_wall");
}
private static boolean isTrialChamberMaterial(String block) {
return block.contains("tuff_brick")
|| block.equals("polished_tuff")
|| block.equals("chiseled_tuff")
|| block.endsWith("copper_grate")
|| block.equals("trial_spawner")
|| block.equals("vault");
}
private static boolean isVillageMaterial(String structure, String block) {
if (isCobblestoneConstructionMaterial(block)) {
return true;
}
return switch (structure) {
case "village_plains" -> isWoodConstructionMaterial(block, "oak");
case "village_desert" -> block.equals("cut_sandstone")
|| block.equals("smooth_sandstone")
|| block.equals("cut_sandstone_slab")
|| block.equals("smooth_sandstone_slab")
|| block.equals("smooth_sandstone_stairs")
|| block.equals("sandstone_stairs")
|| block.equals("sandstone_slab")
|| block.equals("sandstone_wall");
case "village_savanna" -> isWoodConstructionMaterial(block, "acacia");
case "village_snowy", "village_taiga" -> isWoodConstructionMaterial(block, "spruce");
default -> false;
};
}
private static boolean isWoodConstructionMaterial(String block, String wood) {
if (block.startsWith(wood)) {
int suffixOffset = wood.length();
if (matchesSuffix(block, suffixOffset, "_planks")
|| matchesSuffix(block, suffixOffset, "_stairs")
|| matchesSuffix(block, suffixOffset, "_slab")
|| matchesSuffix(block, suffixOffset, "_fence")
|| matchesSuffix(block, suffixOffset, "_fence_gate")
|| matchesSuffix(block, suffixOffset, "_door")
|| matchesSuffix(block, suffixOffset, "_trapdoor")) {
return true;
}
}
int strippedOffset = "stripped_".length();
if (!block.startsWith("stripped_")
|| !block.regionMatches(strippedOffset, wood, 0, wood.length())) {
return false;
}
int suffixOffset = strippedOffset + wood.length();
return matchesSuffix(block, suffixOffset, "_log")
|| matchesSuffix(block, suffixOffset, "_wood");
}
private static boolean isCobblestoneConstructionMaterial(String block) {
return block.equals("cobblestone")
|| block.equals("cobblestone_stairs")
|| block.equals("cobblestone_slab")
|| block.equals("cobblestone_wall")
|| block.equals("mossy_cobblestone")
|| block.equals("mossy_cobblestone_stairs")
|| block.equals("mossy_cobblestone_slab")
|| block.equals("mossy_cobblestone_wall");
}
private static boolean matchesSuffix(String value, int offset, String suffix) {
return value.length() == offset + suffix.length()
&& value.regionMatches(offset, suffix, 0, suffix.length());
}
private static boolean isMonumentMaterial(String block) {
return block.equals("prismarine")
|| block.equals("prismarine_bricks")
|| block.equals("dark_prismarine")
|| block.equals("sea_lantern");
}
}
@@ -0,0 +1,130 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.StructureVerticalBounds;
import art.arcane.iris.modded.WorldCheckStructureAudit.StructureCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
final class WorldCheckPredicates {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckPredicates() {
}
static void emitSkipped(StructureCheck check, String reason, String... events) {
for (String event : events) {
qaEvent(event, check.label(), false, "skipped=" + reason);
}
}
static void qaEvent(String event, String structure, boolean pass, String detail) {
LOGGER.info(qaEventJson(event, structure, pass, detail));
}
static String qaEventJson(String event, String structure, boolean pass, String detail) {
return "QA_EVT {\"event\":\"" + jsonEscape(event)
+ "\",\"structure\":\"" + jsonEscape(structure)
+ "\",\"pass\":" + pass
+ ",\"detail\":\"" + jsonEscape(detail) + "\"}";
}
static String jsonEscape(String value) {
StringBuilder escaped = new StringBuilder(value.length() + 16);
for (int i = 0; i < value.length(); i++) {
char character = value.charAt(i);
switch (character) {
case '"' -> escaped.append("\\\"");
case '\\' -> escaped.append("\\\\");
case '\b' -> escaped.append("\\b");
case '\f' -> escaped.append("\\f");
case '\n' -> escaped.append("\\n");
case '\r' -> escaped.append("\\r");
case '\t' -> escaped.append("\\t");
default -> {
if (character < 32) {
escaped.append("\\u");
String hex = Integer.toHexString(character);
escaped.append("0".repeat(4 - hex.length())).append(hex);
} else {
escaped.append(character);
}
}
}
}
return escaped.toString();
}
static boolean hasNativeStructureEvidence(boolean validStart, int references) {
return validStart || references > 0;
}
static boolean hasCharacteristicMaterialEvidence(int blocks, int chunksWithMaterial, int scannedChunks) {
if (blocks <= 0 || chunksWithMaterial <= 0 || scannedChunks <= 0
|| chunksWithMaterial > scannedChunks) {
return false;
}
return scannedChunks == 1 || chunksWithMaterial > 1;
}
static boolean verticalShiftMatches(int configuredShift, Integer appliedShift, int shiftedMinY,
int shiftedMaxY, int worldMinY, int worldMaxYExclusive) {
try {
if (appliedShift == null) {
return configuredShift == 0 && StructureVerticalBounds.clampOffset(
shiftedMinY, shiftedMaxY, 0, worldMinY, worldMaxYExclusive) == 0;
}
int originalMinY = Math.subtractExact(shiftedMinY, appliedShift);
int originalMaxY = Math.subtractExact(shiftedMaxY, appliedShift);
int expectedShift = StructureVerticalBounds.clampOffset(
originalMinY, originalMaxY, configuredShift, worldMinY, worldMaxYExclusive);
return appliedShift == expectedShift;
} catch (RuntimeException error) {
return false;
}
}
static boolean mansionVegetationPass(int remainingVegetationBlocks) {
return remainingVegetationBlocks == 0;
}
static boolean mansionVegetationAbovePiece(boolean vegetation, int blockY, int highestPieceY) {
return vegetation && blockY > highestPieceY;
}
static boolean villageFoundationPass(int unsupportedColumns) {
return unsupportedColumns == 0;
}
static boolean villagePoiPass(int inBounds, int outOfBounds) {
return inBounds > 0 && outOfBounds == 0;
}
static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,783 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.nativegen.NativeStructureFoundationBuilder;
import art.arcane.iris.nativegen.NativeStructureVegetationClearer;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.IntBinaryOperator;
final class WorldCheckStructureAudit {
static final List<StructureCheck> STRUCTURE_CHECKS = List.of(
new StructureCheck("stronghold", List.of("minecraft:stronghold"), 256),
new StructureCheck("trial_chambers", List.of("minecraft:trial_chambers"), 128),
new StructureCheck("mansion", List.of("minecraft:mansion"), 256),
new StructureCheck("village", List.of(
"minecraft:village_plains",
"minecraft:village_desert",
"minecraft:village_savanna",
"minecraft:village_snowy",
"minecraft:village_taiga"), 128),
new StructureCheck("monument", List.of("minecraft:monument"), 128)
);
private static final int MAX_FOOTPRINT_CHUNKS = 96;
private static final int MAX_START_REFERENCE_CHUNKS = 16;
private static final int MAX_STRUCTURE_CANDIDATES = 1024;
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private WorldCheckStructureAudit() {
}
static NativeStructureGate checkNativeStructures(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin) {
boolean pass = true;
int nonVillagePassed = 0;
boolean villagePass = false;
PendingVillagePoi pendingPoi = null;
for (StructureCheck check : STRUCTURE_CHECKS) {
StructureCheckResult result = checkNativeStructure(level, generator, origin, check);
if (!result.pass()) {
pass = false;
}
if (check.label().equals("village")) {
villagePass = result.pass();
pendingPoi = result.pendingPoi();
} else if (result.pass()) {
nonVillagePassed++;
}
}
return new NativeStructureGate(pass, nonVillagePassed, villagePass, pendingPoi);
}
private static StructureCheckResult checkNativeStructure(ServerLevel level,
IrisModdedChunkGenerator generator,
BlockPos origin,
StructureCheck check) {
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<Holder<Structure>> registered = new ArrayList<>(check.registryKeys().size());
LinkedHashSet<String> registeredKeys = new LinkedHashSet<>();
for (String key : check.registryKeys()) {
Identifier identifier = Identifier.tryParse(key);
if (identifier == null) {
continue;
}
Optional<Holder.Reference<Structure>> resolved = registry.get(identifier);
if (resolved.isPresent()) {
registered.add(resolved.get());
registeredKeys.add(identifier.toString());
}
}
boolean registryOk = registered.size() == check.registryKeys().size();
LOGGER.info("[worldcheck] {} registry: {}/{} resolved {}", check.label(), registered.size(),
check.registryKeys().size(), registeredKeys);
WorldCheckPredicates.qaEvent("structure_registry", check.label(), registryOk,
"resolved=" + registered.size() + ",expected=" + check.registryKeys().size()
+ ",keys=" + String.join("|", registeredKeys));
if (!registryOk) {
LOGGER.error("[worldcheck] {} registry resolution failed; expected {}", check.label(), check.registryKeys());
WorldCheckPredicates.emitSkipped(check, "registry", "structure_reachability", "structure_locate",
"structure_start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return new StructureCheckResult(false, null);
}
List<Holder<Structure>> reachable = new ArrayList<>(registered.size());
LinkedHashSet<String> reachableKeys = new LinkedHashSet<>();
for (Holder<Structure> holder : registered) {
if (!generator.isNativeStructureReachable(holder)) {
continue;
}
reachable.add(holder);
Identifier key = registry.getKey(holder.value());
if (key != null) {
reachableKeys.add(key.toString());
}
}
boolean reachableOk = !reachable.isEmpty();
LOGGER.info("[worldcheck] {} biome-reachable through Iris: {}", check.label(), reachableKeys);
WorldCheckPredicates.qaEvent("structure_reachability", check.label(), reachableOk,
"reachable=" + reachable.size() + ",registered=" + registered.size()
+ ",keys=" + String.join("|", reachableKeys));
if (!reachableOk) {
LOGGER.error("[worldcheck] {} cannot generate in any biome produced by this Iris pack", check.label());
WorldCheckPredicates.emitSkipped(check, "reachability", "structure_locate", "structure_start_reference",
"structure_footprint", "structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
long locateStart = System.nanoTime();
Pair<BlockPos, Holder<Structure>> found = findGeneratedStructureCandidate(
level, reachable, origin, check.locateRadius());
long locateMillis = (System.nanoTime() - locateStart) / 1_000_000L;
Identifier foundKey = found == null ? null : registry.getKey(found.getSecond().value());
boolean locateOk = found != null && foundKey != null && reachableKeys.contains(foundKey.toString());
WorldCheckPredicates.qaEvent("structure_locate", check.label(), locateOk,
"method=placement_candidates,millis=" + locateMillis + ",radius=" + check.locateRadius()
+ ",result=" + (foundKey == null ? "none" : foundKey));
if (found == null) {
LOGGER.error("[worldcheck] {} native placement candidates produced no valid start within {} rings after {}ms",
check.label(), check.locateRadius(), locateMillis);
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
BlockPos position = found.getFirst();
LOGGER.info("[worldcheck] {} generated candidate: {} {} {} in {}ms (radius={}, result={})",
check.label(), position.getX(), position.getY(), position.getZ(), locateMillis,
check.locateRadius(), foundKey);
if (!locateOk) {
LOGGER.error("[worldcheck] {} candidate scan returned unexpected structure {}", check.label(), foundKey);
WorldCheckPredicates.emitSkipped(check, "locate", "structure_start_reference", "structure_footprint",
"structure_material", "structure_block_entity");
return new StructureCheckResult(false, null);
}
int chunkX = position.getX() >> 4;
int chunkZ = position.getZ() >> 4;
ChunkAccess targetChunk = level.getChunk(chunkX, chunkZ);
Structure structure = found.getSecond().value();
StructureStart start = resolveStructureStart(level, targetChunk, structure);
boolean validStart = start != null && start.isValid();
int references = targetChunk.getReferencesForStructure(structure).size();
boolean startReferenceOk = WorldCheckPredicates.hasNativeStructureEvidence(validStart, references);
LOGGER.info("[worldcheck] {} target chunk {},{}: valid start={}, references={}",
check.label(), chunkX, chunkZ, validStart, references);
WorldCheckPredicates.qaEvent("structure_start_reference", check.label(), startReferenceOk,
"chunk=" + chunkX + "," + chunkZ + ",validStart=" + validStart
+ ",references=" + references);
if (!startReferenceOk || !validStart) {
LOGGER.error("[worldcheck] {} located at chunk {},{} but no resolvable valid start was generated",
check.label(), chunkX, chunkZ);
WorldCheckPredicates.emitSkipped(check, "start_reference", "structure_footprint", "structure_material",
"structure_block_entity");
return new StructureCheckResult(false, null);
}
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
generator.commandEngine(), foundKey.toString(),
NativeStructureVegetationClearer.isUndergroundStep(structure.step()));
Integer appliedShift = generator.worldCheckStructureShift(foundKey.toString(), start.getChunkPos());
BoundingBox shiftedBounds = start.getBoundingBox();
boolean verticalShiftOk = WorldCheckPredicates.verticalShiftMatches(
decision.yShift(), appliedShift, shiftedBounds.minY(), shiftedBounds.maxY(),
level.getMinY(), level.getMaxY());
WorldCheckPredicates.qaEvent("structure_vertical_shift", check.label(), verticalShiftOk,
"configured=" + decision.yShift() + ",applied="
+ (appliedShift == null ? "unrecorded" : appliedShift));
if (!verticalShiftOk) {
LOGGER.error("[worldcheck] {} expected vertical shift {} but generation recorded {}",
check.label(), decision.yShift(), appliedShift);
}
FootprintAudit footprint = auditFootprint(level, structure, start, check, foundKey);
boolean footprintOk = footprint.inspectedChunks() > 0
&& footprint.evidenceChunks() == footprint.inspectedChunks()
&& footprint.coveredPieces() == footprint.totalPieces();
LOGGER.info("[worldcheck] {} footprint: chunks={}/{} evidence={} pieces={}/{}",
check.label(), footprint.inspectedChunks(), footprint.availableChunks(),
footprint.evidenceChunks(), footprint.coveredPieces(), footprint.totalPieces());
WorldCheckPredicates.qaEvent("structure_footprint", check.label(), footprintOk,
"inspected=" + footprint.inspectedChunks() + ",available=" + footprint.availableChunks()
+ ",evidence=" + footprint.evidenceChunks() + ",coveredPieces="
+ footprint.coveredPieces() + ",totalPieces=" + footprint.totalPieces());
boolean materialOk = WorldCheckPredicates.hasCharacteristicMaterialEvidence(footprint.characteristicBlocks(),
footprint.characteristicChunks(), footprint.materialScannedChunks());
LOGGER.info("[worldcheck] {} material: blocks={} chunks={}/{}",
check.label(), footprint.characteristicBlocks(), footprint.characteristicChunks(),
footprint.materialScannedChunks());
WorldCheckPredicates.qaEvent("structure_material", check.label(), materialOk,
"blocks=" + footprint.characteristicBlocks() + ",chunks=" + footprint.characteristicChunks()
+ ",scanned=" + footprint.materialScannedChunks());
boolean vegetationOk = true;
if (check.label().equals("mansion")) {
boolean overlap = footprint.vegetationBlocks() > 0;
vegetationOk = WorldCheckPredicates.mansionVegetationPass(footprint.vegetationBlocks());
LOGGER.info("[worldcheck] mansion vegetation metric: remaining log/leaf blocks={} columns={} overlap={}",
footprint.vegetationBlocks(), footprint.vegetationColumns(), overlap);
WorldCheckPredicates.qaEvent("mansion_vegetation_metric", check.label(), vegetationOk,
"remainingLogsOrLeaves=" + footprint.vegetationBlocks() + ",columns="
+ footprint.vegetationColumns() + ",overlap=" + overlap);
}
boolean foundationOk = true;
PendingVillagePoi pendingPoi = null;
if (check.label().equals("village")) {
foundationOk = WorldCheckPredicates.villageFoundationPass(footprint.foundationGapColumns());
LOGGER.info("[worldcheck] village foundation metric: bases={} cobblestone={} columns={} unsupported={}",
footprint.foundationBaseColumns(), footprint.foundationBlocks(),
footprint.foundationColumns(), footprint.foundationGapColumns());
WorldCheckPredicates.qaEvent("village_foundation_metric", check.label(), foundationOk,
"bases=" + footprint.foundationBaseColumns() + ",cobblestoneBelowBase="
+ footprint.foundationBlocks() + ",columns="
+ footprint.foundationColumns() + ",unsupported=" + footprint.foundationGapColumns());
pendingPoi = new PendingVillagePoi(level, start);
}
boolean blockEntityOk = footprint.blockEntityStates() == footprint.blockEntitiesPresent();
LOGGER.info("[worldcheck] {} block entities: state blocks={}, present={}, missing={}",
check.label(), footprint.blockEntityStates(), footprint.blockEntitiesPresent(),
footprint.blockEntityStates() - footprint.blockEntitiesPresent());
WorldCheckPredicates.qaEvent("structure_block_entity", check.label(), blockEntityOk,
"states=" + footprint.blockEntityStates() + ",present=" + footprint.blockEntitiesPresent()
+ ",missing=" + (footprint.blockEntityStates() - footprint.blockEntitiesPresent()));
if (!footprintOk) {
LOGGER.error("[worldcheck] {} structure footprint is incomplete", check.label());
}
if (!materialOk) {
LOGGER.error("[worldcheck] {} has no distributed characteristic structure material", check.label());
}
if (!blockEntityOk) {
LOGGER.error("[worldcheck] {} generated block-entity states without matching block entities", check.label());
}
if (!vegetationOk) {
LOGGER.error("[worldcheck] mansion vegetation still intersects the generated structure footprint");
}
if (!foundationOk) {
LOGGER.error("[worldcheck] village has unsupported foundation columns after stilt placement");
}
boolean pass = verticalShiftOk && footprintOk && materialOk && blockEntityOk
&& vegetationOk && foundationOk;
return new StructureCheckResult(pass, pass ? pendingPoi : null);
}
private static StructureStart resolveStructureStart(ServerLevel level, ChunkAccess targetChunk,
Structure structure) {
StructureStart direct = targetChunk.getStartForStructure(structure);
if (direct != null && direct.isValid()) {
return direct;
}
int checked = 0;
for (long packed : targetChunk.getReferencesForStructure(structure)) {
if (checked++ >= MAX_START_REFERENCE_CHUNKS) {
break;
}
ChunkAccess referencedChunk = level.getChunk(ChunkPos.getX(packed), ChunkPos.getZ(packed));
StructureStart referenced = referencedChunk.getStartForStructure(structure);
if (referenced != null && referenced.isValid()) {
return referenced;
}
}
return direct;
}
private static Pair<BlockPos, Holder<Structure>> findGeneratedStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, BlockPos origin, int maxRadius) {
ChunkGeneratorStructureState state = level.getChunkSource().getGeneratorState();
Set<Long> attempted = new LinkedHashSet<>();
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof ConcentricRingsStructurePlacement rings)) {
continue;
}
List<ChunkPos> positions = state.getRingPositionsFor(rings);
if (positions == null) {
continue;
}
List<ChunkPos> sorted = new ArrayList<>(positions);
sorted.sort(Comparator.comparingLong(position -> distanceSquared(origin, position)));
for (ChunkPos position : sorted) {
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, position, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
int originChunkX = origin.getX() >> 4;
int originChunkZ = origin.getZ() >> 4;
for (int radius = 0; radius <= maxRadius; radius++) {
for (Holder<Structure> structure : structures) {
for (StructurePlacement placement : state.getPlacementsForStructure(structure)) {
if (!(placement instanceof RandomSpreadStructurePlacement randomSpread)) {
continue;
}
for (int x = -radius; x <= radius; x++) {
boolean xEdge = x == -radius || x == radius;
for (int z = -radius; z <= radius; z++) {
if (!xEdge && z != -radius && z != radius) {
continue;
}
int sectorX = originChunkX + randomSpread.spacing() * x;
int sectorZ = originChunkZ + randomSpread.spacing() * z;
ChunkPos candidate = randomSpread.getPotentialStructureChunk(
state.getLevelSeed(), sectorX, sectorZ);
if (!placement.isStructureChunk(state, candidate.x(), candidate.z())) {
continue;
}
Pair<BlockPos, Holder<Structure>> found = inspectStructureCandidate(
level, structures, placement, candidate, attempted);
if (found != null) {
return found;
}
if (attempted.size() >= MAX_STRUCTURE_CANDIDATES) {
return null;
}
}
}
}
}
}
return null;
}
private static Pair<BlockPos, Holder<Structure>> inspectStructureCandidate(
ServerLevel level, List<Holder<Structure>> structures, StructurePlacement placement,
ChunkPos candidate, Set<Long> attempted) {
if (!attempted.add(candidate.pack())) {
return null;
}
ChunkAccess chunk = level.getChunk(candidate.x(), candidate.z());
for (Holder<Structure> structure : structures) {
StructureStart start = resolveStructureStart(level, chunk, structure.value());
if (start == null || !start.isValid()) {
continue;
}
BlockPos locate = placement.getLocatePos(start.getChunkPos());
BlockPos resolved = new BlockPos(locate.getX(), start.getBoundingBox().minY(), locate.getZ());
return Pair.of(resolved, structure);
}
return null;
}
private static long distanceSquared(BlockPos origin, ChunkPos position) {
long x = (long) position.getMinBlockX() - origin.getX();
long z = (long) position.getMinBlockZ() - origin.getZ();
return x * x + z * z;
}
private static FootprintAudit auditFootprint(ServerLevel level, Structure structure, StructureStart start,
StructureCheck check, Identifier structureKey) {
List<StructurePiece> pieces = start.getPieces();
BoundingBox bounds = start.getBoundingBox();
int availableChunks = footprintChunkCount(bounds);
List<ChunkPos> selected = selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS);
int evidenceChunks = 0;
int blockEntityStates = 0;
int blockEntitiesPresent = 0;
int materialScannedChunks = 0;
int characteristicBlocks = 0;
int characteristicChunks = 0;
int vegetationBlocks = 0;
int vegetationColumns = 0;
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
BitSet visited = new BitSet(level.getHeight() << 8);
int[] maximumPieceY = new int[256];
for (ChunkPos chunkPos : selected) {
ChunkAccess chunk = level.getChunk(chunkPos.x(), chunkPos.z());
StructureStart localStart = chunk.getStartForStructure(structure);
boolean validStart = localStart != null && localStart.isValid();
int references = chunk.getReferencesForStructure(structure).size();
if (WorldCheckPredicates.hasNativeStructureEvidence(validStart, references)) {
evidenceChunks++;
}
BlockEntityAudit blockEntities = auditBlockEntities(level, chunk);
blockEntityStates += blockEntities.states();
blockEntitiesPresent += blockEntities.present();
StructureMaterialAudit material = auditStructureMaterial(level, chunk, start, check,
structureKey, visited, maximumPieceY);
if (material.scanned()) {
materialScannedChunks++;
}
characteristicBlocks += material.characteristicBlocks();
if (material.characteristicBlocks() > 0) {
characteristicChunks++;
}
vegetationBlocks += material.vegetationBlocks();
vegetationColumns += material.vegetationColumns();
foundationBaseColumns += material.foundationBaseColumns();
foundationBlocks += material.foundationBlocks();
foundationColumns += material.foundationColumns();
foundationGapColumns += material.foundationGapColumns();
}
int coveredPieces = 0;
for (StructurePiece piece : pieces) {
boolean covered = false;
for (ChunkPos chunkPos : selected) {
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
covered = true;
break;
}
}
if (covered) {
coveredPieces++;
}
}
return new FootprintAudit(selected.size(), availableChunks, evidenceChunks,
coveredPieces, pieces.size(), blockEntityStates, blockEntitiesPresent,
materialScannedChunks, characteristicBlocks, characteristicChunks,
vegetationBlocks, vegetationColumns, foundationBaseColumns, foundationBlocks,
foundationColumns, foundationGapColumns);
}
private static StructureMaterialAudit auditStructureMaterial(ServerLevel level, ChunkAccess chunk,
StructureStart start,
StructureCheck check,
Identifier structureKey,
BitSet visited,
int[] maximumPieceY) {
List<StructurePiece> pieces = start.getPieces();
visited.clear();
Arrays.fill(maximumPieceY, Integer.MIN_VALUE);
int characteristicBlocks = 0;
int minimumWorldY = level.getMinY();
int maximumWorldY = level.getMaxY() - 1;
int minimumChunkX = chunk.getPos().getMinBlockX();
int maximumChunkX = chunk.getPos().getMaxBlockX();
int minimumChunkZ = chunk.getPos().getMinBlockZ();
int maximumChunkZ = chunk.getPos().getMaxBlockZ();
boolean scanned = false;
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (StructurePiece piece : pieces) {
BoundingBox bounds = piece.getBoundingBox();
int minimumX = Math.max(minimumChunkX, bounds.minX());
int maximumX = Math.min(maximumChunkX, bounds.maxX());
int minimumY = Math.max(minimumWorldY, bounds.minY());
int maximumY = Math.min(maximumWorldY, bounds.maxY());
int minimumZ = Math.max(minimumChunkZ, bounds.minZ());
int maximumZ = Math.min(maximumChunkZ, bounds.maxZ());
if (minimumX > maximumX || minimumY > maximumY || minimumZ > maximumZ) {
continue;
}
scanned = true;
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
maximumPieceY[column] = Math.max(maximumPieceY[column], maximumY);
}
}
for (int y = minimumY; y <= maximumY; y++) {
int verticalIndex = (y - minimumWorldY) << 8;
for (int z = minimumZ; z <= maximumZ; z++) {
int localZ = z - minimumChunkZ;
for (int x = minimumX; x <= maximumX; x++) {
int column = (localZ << 4) | (x - minimumChunkX);
int index = verticalIndex | column;
if (visited.get(index)) {
continue;
}
visited.set(index);
BlockState state = chunk.getBlockState(position.set(x, y, z));
Identifier blockKey = BuiltInRegistries.BLOCK.getKey(state.getBlock());
if (WorldCheckMaterials.isCharacteristicMaterial(check.label(), structureKey, blockKey)) {
characteristicBlocks++;
}
}
}
}
}
int vegetationBlocks = 0;
int vegetationColumns = 0;
if (check.label().equals("mansion")) {
for (int column = 0; column < maximumPieceY.length; column++) {
int highestPieceY = maximumPieceY[column];
if (highestPieceY == Integer.MIN_VALUE || highestPieceY >= maximumWorldY) {
continue;
}
boolean vegetationColumn = false;
int x = minimumChunkX + (column & 15);
int z = minimumChunkZ + (column >> 4);
for (int y = highestPieceY + 1; y <= maximumWorldY; y++) {
BlockState state = chunk.getBlockState(position.set(x, y, z));
boolean vegetation = state.is(BlockTags.LOGS) || state.is(BlockTags.LEAVES);
if (!WorldCheckPredicates.mansionVegetationAbovePiece(vegetation, y, highestPieceY)) {
continue;
}
vegetationBlocks++;
vegetationColumn = true;
}
if (vegetationColumn) {
vegetationColumns++;
}
}
}
int foundationBaseColumns = 0;
int foundationBlocks = 0;
int foundationColumns = 0;
int foundationGapColumns = 0;
if (check.label().equals("village")) {
BoundingBox area = new BoundingBox(
minimumChunkX, minimumWorldY, minimumChunkZ,
maximumChunkX, maximumWorldY, maximumChunkZ);
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
if (!(chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator)) {
throw new IllegalStateException("Iris structure audit requires the Iris chunk generator");
}
Engine engine = irisGenerator.commandEngine();
IntBinaryOperator surfaceHeight = (x, z) ->
engine.getHeight(x, z, true) + engine.getMinHeight();
NativeStructureFoundationBuilder.StiltSupportAudit foundation =
NativeStructureFoundationBuilder.auditStiltSupport(
level, area, start, Blocks.COBBLESTONE.defaultBlockState(), surfaceHeight);
foundationBaseColumns = foundation.baseColumns();
foundationBlocks = foundation.stiltBlocks();
foundationColumns = foundation.stiltColumns();
foundationGapColumns = foundation.unsupportedColumns();
}
return new StructureMaterialAudit(scanned, characteristicBlocks, vegetationBlocks,
vegetationColumns, foundationBaseColumns, foundationBlocks, foundationColumns,
foundationGapColumns);
}
private static List<ChunkPos> selectFootprintChunks(StructureStart start, int limit) {
LinkedHashSet<ChunkPos> selected = new LinkedHashSet<>();
addBounded(selected, start.getChunkPos(), limit);
List<ChunkPos> pieceAnchors = new ArrayList<>();
for (StructurePiece piece : start.getPieces()) {
BoundingBox bounds = piece.getBoundingBox();
pieceAnchors.add(new ChunkPos((bounds.minX() + bounds.maxX()) >> 5,
(bounds.minZ() + bounds.maxZ()) >> 5));
pieceAnchors.add(new ChunkPos(bounds.minX() >> 4, bounds.minZ() >> 4));
pieceAnchors.add(new ChunkPos(bounds.maxX() >> 4, bounds.maxZ() >> 4));
}
pieceAnchors.sort(Comparator.comparingInt((ChunkPos chunkPos) ->
chunkPos.distanceSquared(start.getChunkPos())));
for (ChunkPos chunkPos : pieceAnchors) {
addBounded(selected, chunkPos, limit);
}
for (ChunkPos chunkPos : boundedFootprintChunks(start.getBoundingBox(), start.getChunkPos(), limit)) {
if (intersectsAnyPiece(start.getPieces(), chunkPos)) {
addBounded(selected, chunkPos, limit);
}
}
return List.copyOf(selected);
}
static List<ChunkPos> boundedFootprintChunks(BoundingBox bounds, ChunkPos origin, int limit) {
if (limit <= 0) {
return List.of();
}
int minChunkX = bounds.minX() >> 4;
int maxChunkX = bounds.maxX() >> 4;
int minChunkZ = bounds.minZ() >> 4;
int maxChunkZ = bounds.maxZ() >> 4;
long width = (long) maxChunkX - minChunkX + 1L;
long depth = (long) maxChunkZ - minChunkZ + 1L;
long total = width * depth;
LinkedHashSet<ChunkPos> chunks = new LinkedHashSet<>();
addBounded(chunks, origin, limit);
if (total <= limit) {
for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) {
for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) {
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
}
}
return List.copyOf(chunks);
}
addBounded(chunks, new ChunkPos(minChunkX, minChunkZ), limit);
addBounded(chunks, new ChunkPos(maxChunkX, minChunkZ), limit);
addBounded(chunks, new ChunkPos(minChunkX, maxChunkZ), limit);
addBounded(chunks, new ChunkPos(maxChunkX, maxChunkZ), limit);
int samplesPerAxis = Math.max(2, (int) Math.floor(Math.sqrt(limit)));
for (int sampleZ = 0; sampleZ < samplesPerAxis; sampleZ++) {
int chunkZ = sampleCoordinate(minChunkZ, maxChunkZ, sampleZ, samplesPerAxis);
for (int sampleX = 0; sampleX < samplesPerAxis; sampleX++) {
int chunkX = sampleCoordinate(minChunkX, maxChunkX, sampleX, samplesPerAxis);
addBounded(chunks, new ChunkPos(chunkX, chunkZ), limit);
}
}
return List.copyOf(chunks);
}
private static int footprintChunkCount(BoundingBox bounds) {
long width = (long) (bounds.maxX() >> 4) - (bounds.minX() >> 4) + 1L;
long depth = (long) (bounds.maxZ() >> 4) - (bounds.minZ() >> 4) + 1L;
long total = width * depth;
return total > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) total;
}
private static int sampleCoordinate(int minimum, int maximum, int index, int samples) {
if (samples <= 1 || minimum == maximum) {
return minimum;
}
double progress = (double) index / (double) (samples - 1);
return minimum + (int) Math.round((maximum - minimum) * progress);
}
private static void addBounded(Set<ChunkPos> chunks, ChunkPos chunkPos, int limit) {
if (chunks.size() < limit) {
chunks.add(chunkPos);
}
}
private static boolean intersectsAnyPiece(List<StructurePiece> pieces, ChunkPos chunkPos) {
for (StructurePiece piece : pieces) {
if (intersectsChunk(piece.getBoundingBox(), chunkPos)) {
return true;
}
}
return false;
}
private static boolean intersectsChunk(BoundingBox bounds, ChunkPos chunkPos) {
return bounds.maxX() >= chunkPos.getMinBlockX()
&& bounds.minX() <= chunkPos.getMaxBlockX()
&& bounds.maxZ() >= chunkPos.getMinBlockZ()
&& bounds.minZ() <= chunkPos.getMaxBlockZ();
}
private static BlockEntityAudit auditBlockEntities(ServerLevel level, ChunkAccess chunk) {
int states = 0;
int present = 0;
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (!section.maybeHas(BlockState::hasBlockEntity)) {
continue;
}
int sectionMinY = chunk.getSectionYFromSectionIndex(sectionIndex) << 4;
for (int localY = 0; localY < 16; localY++) {
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
BlockState state = section.getBlockState(localX, localY, localZ);
if (!state.hasBlockEntity()) {
continue;
}
states++;
position.set(chunk.getPos().getBlockX(localX), sectionMinY + localY,
chunk.getPos().getBlockZ(localZ));
if (level.getBlockEntity(position) != null) {
present++;
}
}
}
}
}
return new BlockEntityAudit(states, present);
}
static PoiAudit auditStructurePois(ServerLevel level, StructureStart start) {
int inBounds = 0;
int outOfBounds = 0;
PoiManager poiManager = level.getPoiManager();
for (ChunkPos chunkPos : selectFootprintChunks(start, MAX_FOOTPRINT_CHUNKS)) {
List<PoiRecord> records = poiManager.getInChunk(
holder -> true, chunkPos, PoiManager.Occupancy.ANY).toList();
for (PoiRecord record : records) {
BlockPos position = record.getPos();
if (position.getY() < level.getMinY() || position.getY() >= level.getMaxY()) {
outOfBounds++;
continue;
}
if (insideAnyPiece(start.getPieces(), position)) {
inBounds++;
}
}
}
return new PoiAudit(inBounds, outOfBounds);
}
private static boolean insideAnyPiece(List<StructurePiece> pieces, BlockPos position) {
for (StructurePiece piece : pieces) {
if (piece.getBoundingBox().isInside(position)) {
return true;
}
}
return false;
}
record StructureCheck(String label, List<String> registryKeys, int locateRadius) {
}
record NativeStructureGate(boolean passBeforePoi, int nonVillagePassed,
boolean villagePassBeforePoi, PendingVillagePoi pendingPoi) {
}
private record StructureCheckResult(boolean pass, PendingVillagePoi pendingPoi) {
}
record PendingVillagePoi(ServerLevel level, StructureStart start) {
}
private record FootprintAudit(int inspectedChunks, int availableChunks, int evidenceChunks,
int coveredPieces, int totalPieces, int blockEntityStates,
int blockEntitiesPresent, int materialScannedChunks,
int characteristicBlocks, int characteristicChunks,
int vegetationBlocks, int vegetationColumns,
int foundationBaseColumns, int foundationBlocks, int foundationColumns,
int foundationGapColumns) {
}
private record BlockEntityAudit(int states, int present) {
}
private record StructureMaterialAudit(boolean scanned, int characteristicBlocks,
int vegetationBlocks, int vegetationColumns,
int foundationBaseColumns, int foundationBlocks,
int foundationColumns,
int foundationGapColumns) {
}
record PoiAudit(int inBounds, int outOfBounds) {
}
}
@@ -25,10 +25,33 @@ import art.arcane.iris.modded.command.ModdedPregenJob;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.ChunkGenerator;
/**
* Entry point for mods integrating with Iris on Fabric, Forge and NeoForge.
* <p>
* Everything here is static and null-tolerant: a null or non-Iris {@link ServerLevel} produces false, null, or a
* no-op rather than an exception, so a caller never has to pre-check whether a level is generated by Iris.
* <p>
* <b>Threading.</b> {@link #isIrisLevel(ServerLevel)}, {@link #isStudioLevel(ServerLevel)} and
* {@link #getEngine(ServerLevel)} read the level's chunk generator reference and are safe from any thread once
* the level is loaded. The mantle accessors are safe off the server thread but touch engine storage - see their
* own notes. {@link #pregenerate(ServerLevel, int)} and {@link #registerProvider(ModdedDataProvider)} mutate
* global state and belong on the server thread, during mod setup or from a command.
* <p>
* <b>Stability.</b> This class and the {@code Modded*} types beside it are the intended integration surface. The
* types they expose from {@code art.arcane.iris.engine.*} and {@code art.arcane.iris.core.*} - notably
* {@link Engine} - are internal to Iris and change without a deprecation cycle. Treat {@link Engine} as an opaque
* token to hand back to Iris, and prefer the wrappers here over reaching into it.
*
* @see ModdedDataProvider for supplying custom blocks, items and entities to the generator
*/
public final class IrisModdedAPI {
private IrisModdedAPI() {
}
/**
* Whether {@code level}'s chunk generator is an Iris generator. False for null and for every vanilla or
* third-party generated level. The cheapest available Iris check.
*/
public static boolean isIrisLevel(ServerLevel level) {
if (level == null) {
return false;
@@ -36,11 +59,22 @@ public final class IrisModdedAPI {
return level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator;
}
/**
* Whether {@code level} is an Iris studio level - a throwaway world opened for pack authoring, which is
* deleted on shutdown. Persist nothing against one. False for null and for non-Iris levels.
*/
public static boolean isStudioLevel(ServerLevel level) {
Engine engine = getEngine(level);
return engine != null && engine.isStudio();
}
/**
* The Iris engine driving {@code level}, or null when the level is null, is not Iris-generated, or its engine
* is not currently available - during shutdown, or while the generator is still binding.
* <p>
* Never cached: resolve per use. Reloading a pack or unloading the level replaces the engine, and a stale
* reference goes inert. {@link Engine} is internal to Iris; see the stability note on this class.
*/
public static Engine getEngine(ServerLevel level) {
if (level == null) {
return null;
@@ -56,10 +90,27 @@ public final class IrisModdedAPI {
}
}
/**
* Starts a cached, asynchronous pregeneration of {@code radiusBlocks} around the world origin.
* Equivalent to {@code pregenerate(level, radiusBlocks, 0, 0, false, true)}.
*/
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
return pregenerate(level, radiusBlocks, 0, 0, false, true);
}
/**
* Starts a pregeneration job over a square region.
* <p>
* Returns as soon as the job is queued; progress is reported through Iris's own logging and boss bar, not to
* the caller. Only one job runs server-wide, so this returns false if one is already active. Call on the
* server thread.
*
* @param radiusBlocks half-extent of the square in blocks, measured from the centre
* @param sync write chunks synchronously; slower but avoids the async write queue
* @param cached reuse and update the on-disk pregeneration cache so an interrupted job resumes instead of
* regenerating
* @return false when {@code level} is not Iris-generated or another pregeneration job is already running
*/
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -68,6 +119,19 @@ public final class IrisModdedAPI {
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached);
}
/**
* Reads a mantle value of {@code type} at world coordinates.
* <p>
* The mantle is Iris's own per-block sidecar storage, independent of chunk NBT, and it is how Iris carries
* data that must survive between generation stages. Coordinates are world-space: {@code y} is translated by
* the engine's minimum height internally.
* <p>
* Returns null when the level is not Iris-generated, no mantle region exists for that column yet - reads
* never create or load one - or nothing of {@code type} is stored there. A {@code y} outside the engine's
* height range reads as null rather than throwing.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -76,6 +140,19 @@ public final class IrisModdedAPI {
return engine.getMantle().getMantle().get(x, y - engine.getMinHeight(), z, type);
}
/**
* Writes a mantle value at world coordinates, replacing any previous value of the same type there.
* <p>
* Unlike {@link #getMantleData(ServerLevel, int, int, int, Class)}, a write creates the mantle region if it
* does not exist, which can touch disk - do not call it per block in a tick loop from the server thread. A
* null {@code data}, a non-Iris level, or a {@code y} outside the engine's height range is a silent no-op;
* remove values with {@link #deleteMantleData(ServerLevel, int, int, int, Class)}.
* <p>
* Values written under a custom type are discarded when Iris trims a mantle region unless the type is
* declared with {@link #retainMantleDataForSlice(Class)}.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> void setMantleData(ServerLevel level, int x, int y, int z, T data) {
Engine engine = getEngine(level);
if (engine == null || data == null) {
@@ -84,6 +161,12 @@ public final class IrisModdedAPI {
engine.getMantle().getMantle().set(x, y - engine.getMinHeight(), z, data);
}
/**
* Removes any mantle value of {@code type} at world coordinates. A non-Iris level or an out-of-range
* {@code y} is a silent no-op. Like a write, this creates the mantle region if it is absent.
*
* @throws IllegalStateException if the engine's mantle has already been closed
*/
public static <T> void deleteMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
Engine engine = getEngine(level);
if (engine == null) {
@@ -92,6 +175,14 @@ public final class IrisModdedAPI {
engine.getMantle().getMantle().remove(x, y - engine.getMinHeight(), z, type);
}
/**
* Declares that mantle slices of {@code sliceType} must be kept rather than discarded.
* <p>
* Iris drops slices it does not need once a region's generation data has served its purpose. Any type a mod
* writes with {@link #setMantleData(ServerLevel, int, int, int, Object)} and expects to read back later must be
* declared here first. Registration is by canonical class name, process-wide across every Iris world, and
* cannot be undone - declare it once during mod setup. A null {@code sliceType} is ignored.
*/
public static void retainMantleDataForSlice(Class<?> sliceType) {
if (sliceType == null) {
return;
@@ -99,10 +190,31 @@ public final class IrisModdedAPI {
WorldMaintenance.retainMantleDataForSlice(sliceType.getCanonicalName());
}
/**
* Registers a custom content provider imperatively, for mods that would rather call Iris than ship a
* {@link java.util.ServiceLoader} entry.
* <p>
* Providers are keyed by {@link ModdedDataProvider#modId()}; a second registration under an id already present
* is logged and ignored. {@link ModdedDataProvider#init()} runs during this call, and a throwable it raises is
* logged rather than propagated. A null {@code provider} is ignored.
* <p>
* Ordering matters: Iris only consults providers registered before a pack resolves the block in question, so
* register during mod setup. Registering after Iris's own {@link java.util.ServiceLoader} discovery is
* supported; registering after a world has generated is not - blocks already resolved are not revisited.
*/
public static void registerProvider(ModdedDataProvider provider) {
ModdedCustomContentRegistry.register(provider);
}
/**
* Maps a custom {@code namespace:key} onto a fixed vanilla block state, for mods that only need a static alias
* and no provider class.
* <p>
* {@code state} is a block state string in the same syntax packs use, for example
* {@code minecraft:oak_log[axis=y]}, and is parsed immediately: an unparseable state or an invalid identifier
* is logged and the registration is dropped, so a typo shows up at startup rather than as missing blocks.
* Aliases take precedence over provider lookups for the same key. Null arguments are ignored.
*/
public static void registerCustomBlockData(String namespace, String key, String state) {
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
}
@@ -22,15 +22,39 @@ import net.minecraft.world.level.block.state.BlockState;
import java.util.Objects;
/**
* A provider's answer to a block lookup: the state to write, and whether the provider wants a second pass once the
* chunk is loaded.
* <p>
* Immutable. Returned from {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)};
* construct with {@link #direct(BlockState)} or {@link #deferred(BlockState)} rather than the canonical constructor.
*
* @param state the block state Iris writes. Never null
* @param deferredPlacement whether {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)}
* should run for this position after the chunk is loaded
*/
public record ModdedBlockData(BlockState state, boolean deferredPlacement) {
/**
* @throws NullPointerException if {@code state} is null
*/
public ModdedBlockData {
Objects.requireNonNull(state);
}
/**
* The state is final - Iris writes it during generation and does nothing further.
*/
public static ModdedBlockData direct(BlockState state) {
return new ModdedBlockData(state, false);
}
/**
* {@code state} is a placeholder written during generation; the provider finishes the job in
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} once the chunk is loaded. Use
* when the real block needs a level - a block entity, neighbour state, or mod registries not available on a
* generation thread. Pick a placeholder with the same shape and occlusion as the final block so terrain around
* it generates correctly.
*/
public static ModdedBlockData deferred(BlockState state) {
return new ModdedBlockData(state, true);
}
@@ -27,6 +27,22 @@ import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.Objects;
/**
* Everything a provider needs to finish a deferred block placement, handed to
* {@link ModdedDataProvider#processBlockPlacement(ModdedBlockPlacementContext)} on the server thread.
* <p>
* Immutable, and constructed by Iris rather than by mods. {@code state} is defensively copied; {@code position} is
* already immutable. Because delivery is on the server thread with the chunk loaded, it is safe to write blocks,
* attach block entities and read neighbours from here.
*
* @param engine the Iris engine for this level. Internal Iris type - treat it as an opaque token
* @param level the level to write into. Never null
* @param position the block the placeholder was written at. Never null
* @param blockId the identifier the pack named, without state properties. Never null
* @param blockState the state currently at {@code position} - normally the placeholder returned as deferred, though
* another provider or a later generation stage may have replaced it. Never null
* @param state the {@code [prop=value]} pairs from the pack's key, possibly empty. Never null; unmodifiable
*/
public record ModdedBlockPlacementContext(
Engine engine,
ServerLevel level,
@@ -34,6 +50,9 @@ public record ModdedBlockPlacementContext(
Identifier blockId,
Map<String, String> state,
BlockState blockState) {
/**
* @throws NullPointerException if any component is null
*/
public ModdedBlockPlacementContext {
Objects.requireNonNull(engine);
Objects.requireNonNull(level);
@@ -37,6 +37,22 @@ import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Registry of {@link ModdedDataProvider} instances and static block-data aliases, and the resolution path Iris
* itself calls into.
* <p>
* Mods should go through {@link IrisModdedAPI#registerProvider(ModdedDataProvider)} and
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)} rather than calling this class directly;
* the resolution methods here are Iris internals and are public only because the adapter's generation code lives in
* another package.
* <p>
* <b>Threading.</b> Mutation ({@link #register(ModdedDataProvider)},
* {@link #registerCustomBlockData(String, String, String)}, {@link #discover()}) is serialized on the class
* monitor. Resolution ({@link #resolveBlock(String)}, {@link #spawnMob}, {@link #processBlockPlacement}) is lock
* free over a copy-on-write provider list and a concurrent alias map, so it runs on generation threads. Every
* resolution method catches provider throwables, logs them against the provider's mod id, and continues with the
* next provider.
*/
public final class ModdedCustomContentRegistry {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final List<ModdedDataProvider> PROVIDERS = new CopyOnWriteArrayList<>();
@@ -47,6 +63,11 @@ public final class ModdedCustomContentRegistry {
private ModdedCustomContentRegistry() {
}
/**
* Registers a static {@code namespace:key} to block-state alias. Invalid identifiers and unparseable states are
* logged and dropped; null arguments are ignored. See
* {@link IrisModdedAPI#registerCustomBlockData(String, String, String)}.
*/
public static synchronized void registerCustomBlockData(String namespace, String key, String state) {
if (namespace == null || key == null || state == null) {
return;
@@ -72,6 +93,11 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom block data {}:{} -> {}", namespace, key, state);
}
/**
* Registers a provider, rejecting a duplicate {@link ModdedDataProvider#modId()} with a warning and ignoring
* null. {@link ModdedDataProvider#init()} runs here; a throwable it raises is logged, not propagated. See
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}.
*/
public static synchronized void register(ModdedDataProvider provider) {
if (provider == null) {
return;
@@ -96,6 +122,15 @@ public final class ModdedCustomContentRegistry {
LOGGER.info("Iris registered custom content provider '{}'", provider.modId());
}
/**
* Runs {@link ServiceLoader} discovery for {@link ModdedDataProvider} against Iris's own class loader, once per
* process. Called during Iris mod initialization; a second call is a no-op that returns an inert handle.
* <p>
* All-or-nothing: a provider whose {@link ModdedDataProvider#init()} throws aborts the pass, restores the
* previous provider and alias state, logs the failing provider's identity, and rethrows.
*
* @return a handle whose {@link Discovery#rollback()} undoes this pass, used by the bootstrap's rollback chain
*/
public static synchronized Discovery discover() {
if (scanned) {
return Discovery.unchanged();
@@ -110,9 +145,12 @@ public final class ModdedCustomContentRegistry {
boolean previousDiscoveryComplete = scanned;
DiscoveryBatch batch = new DiscoveryBatch(previousProviders, previousCustomBlocks);
discoveryBatch = batch;
ModdedDataProvider failingProvider = null;
try {
for (ModdedDataProvider provider : discoveredProviders) {
failingProvider = provider;
batch.add(provider);
failingProvider = null;
}
PROVIDERS.addAll(batch.additions);
CUSTOM_BLOCKS.putAll(batch.customBlocks);
@@ -130,7 +168,8 @@ public final class ModdedCustomContentRegistry {
failure.addSuppressed(rollbackFailure);
}
}
LOGGER.error("Iris custom content provider discovery failed", failure);
LOGGER.warn("Iris custom content provider discovery failed at {}",
providerIdentity(failingProvider), failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -143,6 +182,19 @@ public final class ModdedCustomContentRegistry {
}
}
private static String providerIdentity(ModdedDataProvider provider) {
if (provider == null) {
return "the provider service loader";
}
String className = provider.getClass().getName();
try {
String modId = provider.modId();
return modId == null || modId.isBlank() ? className : "provider '" + modId + "' (" + className + ")";
} catch (Throwable identityFailure) {
return className;
}
}
static synchronized boolean hasProvider(String modId) {
for (ModdedDataProvider provider : PROVIDERS) {
if (Objects.equals(provider.modId(), modId)) {
@@ -156,10 +208,19 @@ public final class ModdedCustomContentRegistry {
return scanned;
}
/**
* Whether any provider or alias is registered. Iris checks this to skip custom resolution entirely on a server
* with no integrating mods.
*/
public static boolean hasProviders() {
return !PROVIDERS.isEmpty() || !CUSTOM_BLOCKS.isEmpty();
}
/**
* Resolves a pack block key against aliases first, then each ready provider that claims it, in registration
* order. {@code key} may carry {@code [prop=value]} properties, which are parsed and passed along. Returns null
* when nothing claims it, which lets the caller fall back to air. Called from generation threads.
*/
public static ModdedBlockData resolveBlock(String key) {
if (key == null || (PROVIDERS.isEmpty() && CUSTOM_BLOCKS.isEmpty())) {
return null;
@@ -192,6 +253,11 @@ public final class ModdedCustomContentRegistry {
return null;
}
/**
* Delivers a deferred placement to the first ready provider claiming {@code key}; later providers are not
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
* server thread with the chunk loaded.
*/
public static void processBlockPlacement(Engine engine, ServerLevel level, BlockPos position, String key) {
Identifier base = parseIdentifier(key);
if (base == null) {
@@ -214,6 +280,10 @@ public final class ModdedCustomContentRegistry {
LOGGER.warn("Iris deferred custom block placement has no provider for {}", key);
}
/**
* Asks each ready provider claiming {@code key} to spawn a custom entity, returning the first non-null result.
* Null when no provider claims it or every attempt declined. Called on the server thread.
*/
public static Entity spawnMob(ServerLevel level, double x, double y, double z, String key) {
if (PROVIDERS.isEmpty() || level == null || key == null) {
return null;
@@ -276,6 +346,10 @@ public final class ModdedCustomContentRegistry {
scanned = discoveryComplete;
}
/**
* Undo handle for one {@link #discover()} pass, so a failure later in Iris's bootstrap can restore the registry
* to its pre-discovery state.
*/
public static final class Discovery {
private final List<ModdedDataProvider> providers;
private final Map<String, BlockState> customBlocks;
@@ -294,6 +368,10 @@ public final class ModdedCustomContentRegistry {
return new Discovery(List.of(), Map.of(), true, false);
}
/**
* Restores the providers and aliases captured before the pass. Idempotent; a no-op on a handle from a
* discovery that did not run.
*/
public synchronized void rollback() {
if (!active) {
return;
@@ -25,28 +25,91 @@ import net.minecraft.world.entity.Entity;
import java.util.Collection;
import java.util.Map;
/**
* Extension point letting a mod resolve its own blocks, items and entities for Iris packs, so a pack can name
* {@code yourmod:something} and have it placed.
* <p>
* Discovered through {@link java.util.ServiceLoader} at Iris mod initialization, or registered imperatively with
* {@link IrisModdedAPI#registerProvider(ModdedDataProvider)}. For ServiceLoader discovery, ship
* {@code META-INF/services/art.arcane.iris.modded.api.ModdedDataProvider} listing the implementation's binary
* name; the class needs a public no-argument constructor.
* <p>
* <b>Threading.</b> Implementations must be thread-safe.
* {@link #getBlockData(Identifier, Map)} is called from generation threads, potentially many at once, for every
* unresolved key a pack names - it must be fast and must not touch world state.
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} and
* {@link #spawnMob(ServerLevel, double, double, double, Identifier)} are called on the server thread, where
* touching the level is safe.
* <p>
* <b>Failure handling.</b> Iris catches throwables from every callback except {@link #init()} during
* ServiceLoader discovery, logs them against {@link #modId()}, and carries on with the remaining providers - one
* broken provider does not stop world generation. A throwable from {@link #init()} during discovery aborts
* discovery and rolls back every provider registered in that pass.
*/
public interface ModdedDataProvider {
/**
* The owning mod's id. Used as the provider's identity: duplicates are rejected, and it labels every log line
* Iris emits about this provider. Must be non-null and stable; returning null aborts discovery.
*/
String modId();
/**
* Whether this provider can answer lookups yet. Iris skips a provider that reports false rather than treating
* it as absent, so a provider whose registries populate late can gate itself instead of returning wrong
* answers. Defaults to true.
*/
default boolean isReady() {
return true;
}
/**
* Every identifier this provider can supply for {@code type}. Used for command suggestion and pack tooling, not
* on the resolution path - {@link #isValidProvider(Identifier, ModdedDataType)} decides that. Return an empty
* collection rather than null.
*/
Collection<Identifier> getTypes(ModdedDataType type);
/**
* Whether this provider claims {@code id} for {@code type}. Called before every resolution callback, on
* generation threads, so keep it to a set lookup. A cheap namespace check is usually enough.
*/
boolean isValidProvider(Identifier id, ModdedDataType type);
/**
* Resolves a claimed block identifier into a concrete block state.
* <p>
* {@code state} holds the {@code [prop=value]} pairs from the pack's key, already parsed and possibly empty;
* never null. Return null to decline, in which case Iris tries the next provider and finally falls back to air.
* Return {@link ModdedBlockData#deferred(net.minecraft.world.level.block.state.BlockState)} when the real block
* needs a loaded level - Iris then writes the placeholder state and calls
* {@link #processBlockPlacement(ModdedBlockPlacementContext)} later. Called from generation threads.
*/
default ModdedBlockData getBlockData(Identifier blockId, Map<String, String> state) {
return null;
}
/**
* Finishes a deferred placement once the chunk is loaded: swap in the real block, attach a block entity, seed
* NBT.
* <p>
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
* later providers are not consulted for that position. Only fires for states returned as deferred.
*/
default void processBlockPlacement(ModdedBlockPlacementContext context) {
}
/**
* Spawns a claimed custom entity at the given position. Return null to decline and let the next provider try.
* Called on the server thread from Iris's entity spawning.
*/
default Entity spawnMob(ServerLevel level, double x, double y, double z, Identifier entityId) {
return null;
}
/**
* One-time setup, called by Iris immediately after this provider is accepted. A throwable raised here aborts
* ServiceLoader discovery; when registered imperatively it is logged and the provider stays registered.
*/
default void init() {
}
}
@@ -18,8 +18,16 @@
package art.arcane.iris.modded.api;
/**
* The kinds of custom content a {@link ModdedDataProvider} can claim.
* <p>
* Constants may be added. Switch expressions over this enum need a {@code default} arm.
*/
public enum ModdedDataType {
/** Block states, resolved through {@link ModdedDataProvider#getBlockData(net.minecraft.resources.Identifier, java.util.Map)}. */
BLOCK,
/** Item types, claimed for loot and pack tooling. */
ITEM,
/** Entity types, spawned through {@link ModdedDataProvider#spawnMob(net.minecraft.server.level.ServerLevel, double, double, double, net.minecraft.resources.Identifier)}. */
ENTITY
}
@@ -0,0 +1,191 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
final class ModdedCommandSuggestions {
static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int TAB_FAILURE_KEYS_MAX = 256;
private static final Set<String> REPORTED_TAB_FAILURES = ConcurrentHashMap.newKeySet();
private ModdedCommandSuggestions() {
}
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
}
} catch (Throwable e) {
warnTabFailure("biome keys", context.getSource(), e);
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
}
} catch (Throwable e) {
warnTabFailure("region keys", context.getSource(), e);
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestObjectKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable e) {
warnTabFailure("object keys", context.getSource(), e);
}
return builder.buildFuture();
}
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
Collection<String> irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine);
Registry<Structure> registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<String> nativeKeys = new ArrayList<>(registry.keySet().size());
for (Identifier identifier : registry.keySet()) {
nativeKeys.add(identifier.toString());
}
return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder);
} catch (Throwable e) {
warnTabFailure("structure keys", context.getSource(), e);
}
return builder.buildFuture();
}
static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) {
String origin = tabOrigin(source);
if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) {
return;
}
if (REPORTED_TAB_FAILURES.size() > TAB_FAILURE_KEYS_MAX) {
REPORTED_TAB_FAILURES.clear();
}
LOGGER.warn("Iris tab-complete for {} in {} failed; suggestions will be empty", suggestion, origin, error);
}
private static String tabOrigin(CommandSourceStack source) {
if (source == null) {
return "<no source>";
}
try {
return source.getLevel().dimension().identifier().toString();
} catch (Throwable originFailure) {
return "<no level>";
}
}
static List<String> combineStructureKeys(Collection<String> irisKeys, Collection<String> nativeKeys) {
Set<String> combined = new TreeSet<>();
combined.addAll(irisKeys);
combined.addAll(nativeKeys);
return List.copyOf(combined);
}
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
Set<String> names = new TreeSet<>();
names.add("overworld");
try {
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
File[] children = packs.listFiles();
if (children != null) {
for (File child : children) {
if (!child.isDirectory()) {
continue;
}
String packName = child.getName();
names.add(packName);
File dimensions = new File(child, "dimensions");
File[] dimensionFiles = dimensions.listFiles(
(File directory, String name) -> name.endsWith(".json"));
if (dimensionFiles == null) {
continue;
}
for (File dimensionFile : dimensionFiles) {
String fileName = dimensionFile.getName();
names.add(packName + ":" + fileName.substring(0, fileName.length() - 5));
}
}
}
} catch (Throwable e) {
warnTabFailure("pack names", context.getSource(), e);
}
return SharedSuggestionProvider.suggest(names, builder);
}
private static CompletableFuture<Suggestions> suggestDimensionNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
ModdedCommandFeedback.tab(context.getSource());
List<String> names = new ArrayList<>();
for (ServerLevel level : context.getSource().getServer().getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
names.add(level.dimension().identifier().toString());
}
}
return SharedSuggestionProvider.suggest(names, builder);
}
}
@@ -0,0 +1,344 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.resources.Identifier;
import java.util.function.Predicate;
final class ModdedCommandTree {
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private ModdedCommandTree() {
}
static LiteralArgumentBuilder<CommandSourceStack> rootTree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("iris");
root.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""));
root.then(helpTree());
root.then(Commands.literal("version")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.version(context.getSource())));
root.then(Commands.literal("info").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null))
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(ModdedWhatCommands.tree());
root.then(teleportTree("teleport"));
root.then(teleportTree("tp"));
root.then(Commands.literal("evacuate").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), null))
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension")))));
root.then(Commands.literal("debug").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.debug(context.getSource())));
root.then(Commands.literal("reload").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.reload(context.getSource())));
root.then(Commands.literal("height").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.height(context.getSource())));
root.then(Commands.literal("worlds").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
root.then(Commands.literal("accesslist").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.info(context.getSource(), null)));
root.then(gotoTree("goto"));
root.then(gotoTree("find"));
root.then(Commands.literal("seed").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.seed(context.getSource())));
root.then(goldenhashTree("goldenhash"));
root.then(goldenhashTree("gold"));
root.then(downloadTree("download"));
root.then(downloadTree("dl"));
root.then(metricsTree("metrics"));
root.then(metricsTree("measure"));
root.then(regenTree("regen"));
root.then(regenTree("rg"));
root.then(pregenTree("pregen"));
root.then(pregenTree("pregenerate"));
root.then(Commands.literal("wand").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveWand(context.getSource())));
root.then(Commands.literal("dust").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
root.then(Commands.literal("d").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveDust(context.getSource())));
root.then(ModdedObjectCommands.tree("object"));
root.then(ModdedObjectCommands.tree("o"));
root.then(editTree());
root.then(createTree("create"));
root.then(createTree("c"));
root.then(ModdedStudioCommands.tree("studio"));
root.then(ModdedStudioCommands.tree("std"));
root.then(ModdedStudioCommands.tree("s"));
root.then(ModdedPackCommands.tree("pack"));
root.then(ModdedPackCommands.tree("pk"));
root.then(ModdedWorldCommands.tree("world"));
root.then(ModdedWorldCommands.tree("w"));
root.then(ModdedDatapackCommands.tree("datapack"));
root.then(ModdedDatapackCommands.tree("datapacks"));
root.then(ModdedDatapackCommands.tree("dp"));
root.then(ModdedStructureCommands.tree("structure"));
root.then(ModdedStructureCommands.tree("struct"));
root.then(ModdedStructureCommands.tree("str"));
root.then(ModdedDeveloperCommands.tree("developer"));
root.then(ModdedDeveloperCommands.tree("dev"));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> createTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("name", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
ModdedWorldCommands.createWorld(
context.getSource(),
StringArgumentType.getString(context, "name"),
"overworld",
1337L))
.then(Commands.argument("pack", StringArgumentType.string()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
1337L))
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
StringArgumentType.getString(context, "name"),
StringArgumentType.getString(context, "pack"),
LongArgumentType.getLong(context, "seed"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> teleportTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null))
.then(Commands.argument("player", EntityArgument.player())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"),
EntityArgument.getPlayer(context, "player")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> helpTree() {
return Commands.literal("help")
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), ""))
.then(Commands.argument("section", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> downloadTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.argument("pack", StringArgumentType.word()).suggests(ModdedCommandSuggestions.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable", true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"), "stable",
BoolArgumentType.getBool(context, "overwrite"))))
.then(Commands.argument("branch", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), false))
.then(Commands.literal("force")
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"), true)))
.then(Commands.argument("overwrite", BoolArgumentType.bool())
.executes((CommandContext<CommandSourceStack> context) ->
IrisModdedCommands.download(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "branch"),
BoolArgumentType.getBool(context, "overwrite"))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> metricsTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.metrics(context.getSource()));
}
private static LiteralArgumentBuilder<CommandSourceStack> regenTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), 0))
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 64))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))));
}
private static LiteralArgumentBuilder<CommandSourceStack> gotoTree(String name) {
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
.then(Commands.literal("biome")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("object")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("structure")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("poi")
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.POI_TYPES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoPoi(context.getSource(), StringArgumentType.getString(context, "type")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree(String name) {
RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, false, false, false, false, false));
attachPregenCenter(radius, false);
attachPregenFlags(radius, false, false, false, false, false);
RequiredArgumentBuilder<CommandSourceStack, Identifier> dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(ModdedCommandSuggestions.DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, true, false, false, false, false));
attachPregenCenter(dimension, true);
attachPregenFlags(dimension, true, false, false, false, false);
radius.then(dimension);
return Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
.then(Commands.literal("start")
.then(radius))
.then(Commands.literal("stop")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
.then(Commands.literal("x")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStop(context.getSource())))
.then(Commands.literal("pause")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
.then(Commands.literal("resume")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenPause(context.getSource())))
.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStatus(context.getSource())));
}
private static void attachPregenCenter(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension) {
RequiredArgumentBuilder<CommandSourceStack, Integer> z = Commands.argument("z", IntegerArgumentType.integer())
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, true, false, false, false));
attachPregenFlags(z, withDimension, true, false, false, false);
node.then(Commands.literal("at")
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(z)));
}
private static void attachPregenFlags(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
if (!gui) {
node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache));
}
if (!sync) {
node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache));
}
if (!nocache) {
node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true));
}
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
LiteralArgumentBuilder<CommandSourceStack> flag = Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> ModdedPregenCommands.pregenStart(context, withDimension, withCenter, gui, sync, nocache));
attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache);
return flag;
}
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal(name).requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radiusAndThreads, (CommandContext<CommandSourceStack> context) -> 8, (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radius, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64))
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO));
attachModes(threads, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "threads"));
radius.then(threads);
radiusAndThreads.then(radius);
return radiusAndThreads;
}
private interface IntExtractor {
int extract(CommandContext<CommandSourceStack> context);
}
private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> node, IntExtractor radius, IntExtractor threads) {
node.then(Commands.literal("capture")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE)));
node.then(Commands.literal("verify")
.executes((CommandContext<CommandSourceStack> context) -> IrisModdedCommands.goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY)));
}
private static LiteralArgumentBuilder<CommandSourceStack> editTree() {
return Commands.literal("edit").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "edit"))
.then(Commands.literal("biome")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("b")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("r")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), null))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("dimension")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())))
.then(Commands.literal("d")
.executes((CommandContext<CommandSourceStack> context) -> ModdedEditCommands.editDimension(context.getSource())));
}
}
@@ -108,7 +108,7 @@ public final class ModdedDustRevealer {
pos.immutable(),
key,
level.getMinY(),
level.getMaxY(),
level.getMaxY() + 1,
new AtomicBoolean());
RevealRun previous = ACTIVE_RUNS.put(player.getUUID(), run);
if (previous != null) {
@@ -144,14 +144,14 @@ public final class ModdedDustRevealer {
run.key(),
run.engine().getMinHeight(),
run.minY(),
run.maxY(),
run.maxYExclusive(),
run.cancelled(),
(int x, int relativeY, int z) ->
run.engine().getObjectPlacementKey(x, relativeY, z));
}
static List<BlockPos> collect(BlockPos origin, String key, int engineMinY,
int minY, int maxY, AtomicBoolean cancelled,
int minY, int maxYExclusive, AtomicBoolean cancelled,
ObjectPlacementLookup lookup) {
List<BlockPos> hits = new ArrayList<>();
Set<BlockPos> visited = new HashSet<>();
@@ -169,7 +169,7 @@ public final class ModdedDustRevealer {
}
BlockPos next = current.offset(dx, dy, dz);
if (next.getY() < minY
|| next.getY() >= maxY
|| next.getY() >= maxYExclusive
|| !visited.add(next)) {
continue;
}
@@ -483,7 +483,7 @@ public final class ModdedDustRevealer {
BlockPos origin,
String key,
int minY,
int maxY,
int maxYExclusive,
AtomicBoolean cancelled
) {
}
@@ -0,0 +1,133 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.gui.GuiHost;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.localization.MessageArgument;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
final class ModdedEditCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private ModdedEditCommands() {
}
static int editBiome(CommandSourceStack source, String key) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS));
return 0;
}
IrisBiome biome;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_BIOME_IRIS_EDIT_BIOME_KEY));
return 0;
}
BlockPos pos = player.blockPosition();
try {
biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
}
} else {
biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME, MessageArgument.untrusted("key", key)));
return 0;
}
}
return openJson(source, biome);
}
static int editRegion(CommandSourceStack source, String key) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_2));
return 0;
}
IrisRegion region;
if (key == null || key.isBlank()) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CONSOLE_MUST_NAME_REGION_IRIS_EDIT_REGION_KEY));
return 0;
}
BlockPos pos = player.blockPosition();
try {
region = engine.getRegion(pos.getX(), pos.getZ());
} catch (Throwable e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName())));
return 0;
}
} else {
region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION, MessageArgument.untrusted("key", key)));
return 0;
}
}
return openJson(source, region);
}
static int editDimension(CommandSourceStack source) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_3));
return 0;
}
return openJson(source, engine.getDimension());
}
private static int openJson(CommandSourceStack source, IrisRegistrant registrant) {
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_OPEN_FILES_HERE, MessageArgument.untrusted("value", ModdedGuiHost.guiUnavailableReason())));
return 0;
}
if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CANNOT_FIND_FILE_PERHAPS_IT_WAS_NOT_LOADED_DIRECTLY_FROM));
return 0;
}
File file = registrant.getLoadFile();
try {
Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOGGER.error("Iris edit failed to open {}", file, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_OPEN, MessageArgument.untrusted("value", file.getName()), MessageArgument.untrusted("value2", e.getClass().getSimpleName())));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_OPENING_YOUR_EDITOR, MessageArgument.untrusted("value", registrant.getTypeName()), MessageArgument.untrusted("value2", file.getName())));
return 1;
}
}
@@ -0,0 +1,535 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.math.Position2;
import com.mojang.datafixers.util.Pair;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
final class ModdedLocateCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final long LOCATE_TIMEOUT_MS = 120000L;
private static final int NATIVE_STRUCTURE_LOCATE_RADIUS = 100;
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
private ModdedLocateCommands() {
}
static int gotoBiome(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_3));
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
return 0;
}
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0;
}
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1;
}
static int gotoRegion(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_4));
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
return 0;
}
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
return 0;
}
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
return 0;
}
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
return 1;
}
static int gotoObject(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_10));
return 0;
}
String key = keyRaw.trim();
if (!engine.hasObjectPlacement(key)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_OBJECT_KEY, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
return 0;
}
locate(source, level, engine, player, Locator.object(key), "object " + key);
return 1;
}
static int gotoStructure(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_11));
return 0;
}
String key = keyRaw.trim();
if (key.isEmpty()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NAME_IRIS_NATIVE_STRUCTURE_LOCATE));
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_5));
return 0;
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_USE_TAB_COMPLETION_CHOOSE_IRIS_PLACEMENT_REGISTERED_NATIVE, MessageArgument.untrusted("key", key)));
return 0;
}
NativeStructureTarget target = resolved.get();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
if (!decision.generate()
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
IrisModdedCommands.fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
target.key(), decision.status()));
return 0;
}
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
locateIrisStructure(source, level, engine, player, target.key());
return 1;
}
if (target.availability() != NativeStructureAvailability.AVAILABLE) {
IrisModdedCommands.fail(source, nativeUnavailableMessage(target.key(), target.availability()));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
runNativeStructureLocate(source, level, player, target);
return 1;
}
private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String key) {
MinecraftServer server = source.getServer();
int blockX = player.blockPosition().getX();
int blockZ = player.blockPosition().getZ();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_IRIS_PLACED_STRUCTURE, MessageArgument.untrusted("key", key)));
Thread thread = new Thread(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
return;
}
if (!result.found()) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_IRIS_PLACED_STRUCTURE_WITHIN_1024_CHUNKS, MessageArgument.untrusted("key", key))));
return;
}
int targetX = result.originX();
int targetY = result.baseY() + 2;
int targetZ = result.originZ();
server.execute(() -> teleportToStructure(source, level, player, targetX, targetY, targetZ,
"Iris-placed structure " + key));
} catch (Throwable e) {
LOGGER.error("Iris structure locate failed for {}", key, e);
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED, MessageArgument.untrusted("value", e.getClass().getSimpleName()))));
}
}, "Iris Structure Locator");
thread.setDaemon(true);
thread.start();
}
private static void runNativeStructureLocate(CommandSourceStack source, ServerLevel level,
ServerPlayer player, NativeStructureTarget target) {
MinecraftServer server = source.getServer();
Runnable locateTask = () -> locateNativeStructure(source, level, player, target);
if (Thread.currentThread() == server.getRunningThread()) {
locateTask.run();
return;
}
server.execute(locateTask);
}
private static void locateNativeStructure(CommandSourceStack source, ServerLevel level,
ServerPlayer player, NativeStructureTarget target) {
try {
ChunkGenerator generator = level.getChunkSource().getGenerator();
Pair<BlockPos, Holder<Structure>> found = generator.findNearestMapStructure(
level,
HolderSet.direct(target.holder()),
player.blockPosition(),
NATIVE_STRUCTURE_LOCATE_RADIUS,
false);
if (found == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
return;
}
BlockPos position = found.getFirst();
int targetX = position.getX();
int targetZ = position.getZ();
level.getChunk(targetX >> 4, targetZ >> 4);
int surfaceY = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, targetX, targetZ) + 1;
int targetY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, surfaceY));
teleportToStructure(source, level, player, targetX, targetY, targetZ,
"native structure " + target.key());
} catch (Throwable e) {
LOGGER.error("Native structure locate failed for {}", target.key(), e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_NATIVE_STRUCTURE_FAILED, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("value2", e.getClass().getSimpleName())));
}
}
private static void teleportToStructure(CommandSourceStack source, ServerLevel level, ServerPlayer player,
int targetX, int targetY, int targetZ, String label) {
if (player.hasDisconnected() || player.isRemoved()) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PLAYER_DISCONNECTED_BEFORE_STRUCTURE_SEARCH_COMPLETED));
return;
}
if (player.level() != level) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_YOU_CHANGED_DIMENSIONS_BEFORE_STRUCTURE_SEARCH_COMPLETED_RUN_COMMAND_AGAIN));
return;
}
level.getChunk(targetX >> 4, targetZ >> 4);
int clampedY = Math.max(level.getMinY() + 1, Math.min(level.getMaxY() - 1, targetY));
boolean teleported = player.teleportTo(level, targetX + 0.5D, clampedY, targetZ + 0.5D,
Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
if (!teleported) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT, MessageArgument.untrusted("label", label), MessageArgument.untrusted("targetX", targetX), MessageArgument.untrusted("clampedY", clampedY), MessageArgument.untrusted("targetZ", targetZ)));
}
static int verifyStructures(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_12));
return 0;
}
String key = keyRaw == null ? "" : keyRaw.trim();
if (!key.isEmpty()) {
return verifyStructure(source, level, engine, key);
}
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
int available = 0;
int disabled = 0;
int suppressed = 0;
int unreachableBiomes = 0;
int unsupported = 0;
for (Identifier identifier : registry.keySet()) {
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
if (holder.isEmpty()) {
continue;
}
NativeStructureAvailability availability = nativeAvailability(source, level, engine,
identifier.toString(), holder.get());
switch (availability) {
case AVAILABLE -> available++;
case WORLD_DISABLED, FILTERED -> disabled++;
case IRIS_SUPPRESSED -> suppressed++;
case BIOME_UNREACHABLE -> unreachableBiomes++;
case NO_PLACEMENT -> unsupported++;
}
}
int irisPlaced = IrisStructureLocator.placedKeys(engine).size();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_REACHABILITY_NATIVE_GENERATION_ELIGIBLE_IRIS_PLACED_NATIVE_DISABLED_NATIVE, MessageArgument.untrusted("available", available), MessageArgument.untrusted("irisPlaced", irisPlaced), MessageArgument.untrusted("disabled", disabled), MessageArgument.untrusted("suppressed", suppressed), MessageArgument.untrusted("unreachableBiomes", unreachableBiomes), MessageArgument.untrusted("unsupported", unsupported)));
return 1;
}
private static int verifyStructure(CommandSourceStack source, ServerLevel level, Engine engine, String key) {
Optional<NativeStructureTarget> target = resolveNativeStructure(source, level, engine, key);
if (target.isEmpty()) {
if (IrisStructureLocator.isPlaced(engine, key)) {
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_IRIS_PLACED_LOCATABLE_WITH_IRIS_GOTO_STRUCTURE, MessageArgument.untrusted("key", key), MessageArgument.untrusted("key2", key)));
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_STRUCTURE_IT_IS_NEITHER_IRIS_PLACED_NOR_REGISTERED_BY, MessageArgument.untrusted("key", key)));
return 0;
}
NativeStructureTarget resolved = target.get();
if (resolved.availability() == NativeStructureAvailability.IRIS_SUPPRESSED) {
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STRUCTURE_IS_EXPLICITLY_REPLACED_BY_IRIS_PLACEMENT_LOCATABLE_WITH_IRIS, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key())));
return 1;
}
if (resolved.availability() != NativeStructureAvailability.AVAILABLE) {
IrisModdedCommands.fail(source, nativeUnavailableMessage(resolved.key(), resolved.availability()));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NATIVE_STRUCTURE_IS_ENABLED_SUPPORTED_BY_THIS_DIMENSION_S_GENERATOR, MessageArgument.untrusted("value", resolved.key()), MessageArgument.untrusted("value2", resolved.key())));
return 1;
}
private static Optional<NativeStructureTarget> resolveNativeStructure(CommandSourceStack source,
ServerLevel level,
Engine engine,
String keyRaw) {
Identifier identifier = Identifier.tryParse(keyRaw);
if (identifier == null) {
return Optional.empty();
}
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
if (holder.isEmpty()) {
return Optional.empty();
}
String key = identifier.toString();
NativeStructureAvailability availability = nativeAvailability(source, level, engine, key, holder.get());
return Optional.of(new NativeStructureTarget(key, holder.get(), availability));
}
private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level,
Engine engine, String key,
Holder.Reference<Structure> holder) {
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
&& irisGenerator.isNativeStructureReachable(holder);
boolean hasPlacement = false;
if (worldEnabled && selected && !suppressed && biomeReachable) {
hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty();
}
return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement);
}
static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected,
boolean suppressed, boolean biomeReachable,
boolean hasPlacement) {
if (!worldEnabled) {
return NativeStructureAvailability.WORLD_DISABLED;
}
if (!selected) {
return NativeStructureAvailability.FILTERED;
}
if (suppressed) {
return NativeStructureAvailability.IRIS_SUPPRESSED;
}
if (!biomeReachable) {
return NativeStructureAvailability.BIOME_UNREACHABLE;
}
if (!hasPlacement) {
return NativeStructureAvailability.NO_PLACEMENT;
}
return NativeStructureAvailability.AVAILABLE;
}
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
return switch (availability) {
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
case AVAILABLE -> "Native structure " + key + " is available.";
};
}
static int gotoPoi(CommandSourceStack source, String typeRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_13));
return 0;
}
String type = typeRaw.trim();
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_COMMAND_CAN_ONLY_BE_USED_BY_PLAYERS_POI_TYPE, MessageArgument.untrusted("type", type)));
return 0;
}
locate(source, level, engine, player, Locator.poi(type), "POI " + type);
return 1;
}
private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator<?> locator, String label) {
MinecraftServer server = source.getServer();
int chunkX = player.blockPosition().getX() >> 4;
int chunkZ = player.blockPosition().getZ() >> 4;
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING, MessageArgument.untrusted("label", label)));
CompletableFuture<Position2> search;
try {
search = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
});
} catch (WrongEngineBroException e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_THIS_WORLD_HAS_BEEN_CLOSED_REJOIN_DIMENSION_TRY_AGAIN));
return;
}
UUID playerId = player.getUUID();
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
if (previous != null && previous != search) {
previous.cancel(true);
}
search.whenComplete((Position2 at, Throwable error) -> completeLocate(
source, level, engine, player, label, server, playerId, search, at, error));
}
private static void completeLocate(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, MinecraftServer server, UUID playerId,
CompletableFuture<Position2> search, Position2 at, Throwable error) {
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
return;
}
Throwable failure = unwrapCompletionFailure(error);
if (failure instanceof CancellationException) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
return;
}
if (failure != null) {
LOGGER.error("Iris locate failed for {}", label, failure);
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCH_FAILED_2, MessageArgument.untrusted("failure", failure)));
}
});
return;
}
if (at == null) {
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_COULD_NOT_FIND_WITHIN_SEARCH_TIMEOUT, MessageArgument.untrusted("label", label)));
}
});
return;
}
server.execute(() -> {
if (ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
teleportToLocateResult(source, level, engine, player, label, at);
}
});
}
private static void teleportToLocateResult(CommandSourceStack source, ServerLevel level, Engine engine,
ServerPlayer player, String label, Position2 at) {
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
try (GenerationSessionLease lease = engine.acquireGenerationLease("modded_locator_teleport");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
boolean teleported = player.teleportTo(
level,
blockX + 0.5D,
blockY,
blockZ + 0.5D,
Set.<Relative>of(),
player.getYRot(),
player.getXRot(),
false);
if (!teleported) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_FOUND_AT_BUT_TELEPORTATION_FAILED,
MessageArgument.untrusted("label", label),
MessageArgument.trusted("targetX", blockX),
MessageArgument.trusted("clampedY", blockY),
MessageArgument.trusted("targetZ", blockZ)));
return;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_TELEPORTED_AT_2, MessageArgument.untrusted("label", label), MessageArgument.untrusted("blockX", blockX), MessageArgument.untrusted("blockY", blockY), MessageArgument.untrusted("blockZ", blockZ)));
} catch (GenerationSessionException e) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_ENGINE_CHANGED_WHILE_LOCATING_TRY_AGAIN, MessageArgument.untrusted("label", label)));
}
}
private static Throwable unwrapCompletionFailure(Throwable error) {
Throwable failure = error;
while ((failure instanceof CompletionException || failure instanceof ExecutionException)
&& failure.getCause() != null) {
failure = failure.getCause();
}
return failure;
}
enum NativeStructureAvailability {
AVAILABLE,
WORLD_DISABLED,
FILTERED,
IRIS_SUPPRESSED,
BIOME_UNREACHABLE,
NO_PLACEMENT
}
private record NativeStructureTarget(String key, Holder.Reference<Structure> holder,
NativeStructureAvailability availability) {
}
}
@@ -87,7 +87,8 @@ public final class ModdedObjectCommands {
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisModdedCommands.warnTabFailure("object keys", context.getSource(), e);
}
return builder.buildFuture();
};
@@ -0,0 +1,101 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.localization.MessageArgument;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.DimensionArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
final class ModdedPregenCommands {
private ModdedPregenCommands() {
}
static int pregenStart(CommandContext<CommandSourceStack> context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
int radius = IntegerArgumentType.getInteger(context, "radius");
int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0;
int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0;
ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
if (withDimension) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, MessageArgument.untrusted("value", level.dimension().identifier())));
} else {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("radius", radius)));
}
return 0;
}
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
return 0;
}
ModdedPregenBossBar.begin(source.getPlayer());
String guiNote;
if (!gui) {
guiNote = "";
} else if (showGui) {
guiNote = " A progress map window is opening on the server display.";
} else {
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
}
String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache).");
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, MessageArgument.untrusted("value", level.dimension().identifier()), MessageArgument.untrusted("value2", (radius * 2)), MessageArgument.untrusted("value3", (radius * 2)), MessageArgument.untrusted("centerX", centerX), MessageArgument.untrusted("centerZ", centerZ), MessageArgument.untrusted("modeNote", modeNote), MessageArgument.untrusted("guiNote", guiNote)));
return 1;
}
static int pregenStop(CommandSourceStack source) {
if (ModdedPregenJob.stop()) {
ModdedPregenBossBar.clear();
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION));
return 1;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP));
return 0;
}
static int pregenPause(CommandSourceStack source) {
Boolean paused = ModdedPregenJob.pauseResume();
if (paused == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_PAUSE_RESUME));
return 0;
}
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_IS_NOW, MessageArgument.trusted("value", IrisLanguage.plain(paused.booleanValue() ? RuntimeUiMessages.STATUS_PAUSED_LOWER : RuntimeUiMessages.STATUS_RUNNING_LOWER))));
return 1;
}
static int pregenStatus(CommandSourceStack source) {
Component status = ModdedPregenJob.statusComponent();
if (status == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK));
return 0;
}
IrisModdedCommands.ok(source, status);
return 1;
}
}
@@ -102,7 +102,8 @@ public final class ModdedStudioCommands {
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getGeneratorLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
} catch (Throwable e) {
IrisModdedCommands.warnTabFailure("generator keys", context.getSource(), e);
}
return builder.buildFuture();
};
@@ -20,6 +20,7 @@ package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.MainWorldService;
@@ -303,7 +304,14 @@ public final class ModdedWorldCommands {
}
} catch (Throwable e) {
LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
if (PackValidationRegistry.get(pack) == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_PACK_IS_NOT_READY_YET_STILL_LOADING_VALIDATING_TRY_COMMAND, MessageArgument.untrusted("pack", pack)));
return 0;
}
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_PACK_COMMANDS_VALIDATION_FAILED,
MessageArgument.untrusted("value", pack + ":" + packDimension),
MessageArgument.trusted("value2", e.getClass().getSimpleName() + IrisLanguage.errorDetail(e))));
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FIX_PACK_RUN_IRIS_PACK_VALIDATE_REVALIDATE, MessageArgument.untrusted("pack", pack)));
return 0;
}
ModdedModConfig.setMainWorld(packRef, seed);
@@ -1,620 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.JigsawBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.LegacySinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.SinglePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool;
import net.minecraft.world.level.levelgen.structure.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
final class ModdedJigsawStructureCapture {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final MinecraftServer server;
private final StructureKey sourceKey;
private final StructureKey targetKey;
private final StructureSource.Kind sourceKind;
private final Registry<Structure> structureRegistry;
private final Registry<StructureTemplatePool> poolRegistry;
private final Registry<Block> blockRegistry;
private final StructureTemplateManager templateManager;
private final RegistryOps<Tag> registryOps;
private final Map<ResourceKey<StructureTemplatePool>, ResourceKey<StructureTemplatePool>> aliases;
private final Map<String, byte[]> objects;
private final Map<String, Map<String, Object>> pieces;
private final Map<String, Map<String, Object>> pools;
private final EnumSet<StructureCapability> capabilities;
private final List<StructureLoss> losses;
private final Set<String> recordedLosses;
private final Set<String> visitedPools;
private final Deque<String> pendingPools;
private int blocks;
private ModdedJigsawStructureCapture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) {
this.server = Objects.requireNonNull(server);
this.sourceKey = Objects.requireNonNull(sourceKey);
this.targetKey = Objects.requireNonNull(targetKey);
this.sourceKind = Objects.requireNonNull(sourceKind);
structureRegistry = server.registryAccess().lookupOrThrow(Registries.STRUCTURE);
poolRegistry = server.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
blockRegistry = server.registryAccess().lookupOrThrow(Registries.BLOCK);
templateManager = server.getStructureManager();
registryOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess());
aliases = new HashMap<>();
objects = new LinkedHashMap<>();
pieces = new LinkedHashMap<>();
pools = new LinkedHashMap<>();
capabilities = EnumSet.of(
StructureCapability.BLOCKS,
StructureCapability.CONNECTORS,
StructureCapability.IRIS_PLACEMENT
);
losses = new ArrayList<>();
recordedLosses = new HashSet<>();
visitedPools = new HashSet<>();
pendingPools = new ArrayDeque<>();
}
static Capture capture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) throws IOException {
return new ModdedJigsawStructureCapture(server, sourceKey, targetKey, sourceKind).capture();
}
private Capture capture() throws IOException {
Identifier sourceIdentifier = identifier(sourceKey);
Structure structure = structureRegistry.getValue(sourceIdentifier);
if (structure == null) {
throw new IllegalArgumentException("No registered structure exists for " + sourceKey);
}
if (!(structure instanceof JigsawStructure jigsaw)) {
throw new UnsupportedStructureTypeException("Structure " + sourceKey + " uses "
+ structure.getClass().getSimpleName() + "; only jigsaw structure graphs can be converted to Iris assembly resources");
}
CompoundTag encodedStructure = encodeStructure(structure);
configureAliases(jigsaw);
recordRootLosses(jigsaw);
String startPoolKey = poolKey(jigsaw.getStartPool().value());
pendingPools.add(startPoolKey);
while (!pendingPools.isEmpty()) {
capturePool(pendingPools.removeFirst());
}
if (pieces.isEmpty()) {
throw new IllegalStateException("Structure " + sourceKey + " produced no importable pieces");
}
int maxDepth = Math.max(1, encodedStructure.getIntOr("size", 1));
int maxDistance = readHorizontalDistance(encodedStructure);
StructureSource source = StructureSource.identified(
sourceKind,
sourceKey,
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(encodedStructure).getBytes(StandardCharsets.UTF_8)
);
StructureResourceBundle bundle = buildBundle(source, startPoolKey, maxDepth, maxDistance);
return new Capture(bundle, blocks, pieces.size(), pools.size());
}
private CompoundTag encodeStructure(Structure structure) {
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, structure).getOrThrow();
if (!(encoded instanceof CompoundTag compound)) {
throw new IllegalStateException("Structure codec did not produce a compound for " + sourceKey);
}
return compound;
}
private void configureAliases(JigsawStructure structure) {
List<PoolAliasBinding> bindings = structure.getPoolAliases();
if (bindings.isEmpty()) {
return;
}
RandomSource random = RandomSource.create(stableSeed(sourceKey.value()));
for (PoolAliasBinding binding : bindings) {
binding.forEachResolved(random, aliases::put);
}
addLossOnce(
"pool_aliases_resolved_once",
StructureLoss.warning(
StructureCapability.CONNECTORS,
"pool_aliases_resolved_once",
bindings.size() + " native pool alias binding(s) were resolved deterministically for the imported graph; per-placement alias variation is not represented."
)
);
}
private void recordRootLosses(JigsawStructure structure) {
losses.add(StructureLoss.warning(
StructureCapability.NATIVE_PLACEMENT,
"native_placement_settings_not_imported",
"Native start height, heightmap projection, expansion, padding, and placement-set settings are not represented by Iris assembly placement."
));
losses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"native_liquid_settings_not_imported",
"Native structure liquid placement behavior is not represented beyond the captured block and waterlogged states."
));
if (structure.terrainAdaptation() != TerrainAdjustment.NONE) {
losses.add(StructureLoss.warning(
StructureCapability.TERRAIN_ADAPTATION,
"terrain_adaptation_not_imported",
"Native terrain adaptation '" + structure.terrainAdaptation().getSerializedName()
+ "' is not represented by the Iris assembly."
));
}
}
private void capturePool(String sourcePoolKey) throws IOException {
if (!visitedPools.add(sourcePoolKey)) {
return;
}
Identifier sourcePoolIdentifier = Identifier.tryParse(sourcePoolKey);
StructureTemplatePool pool = sourcePoolIdentifier == null ? null : poolRegistry.getValue(sourcePoolIdentifier);
if (pool == null) {
throw new IllegalStateException("Jigsaw graph references missing template pool " + sourcePoolKey);
}
String irisPoolName = poolName(targetKey.path(), sourcePoolKey);
List<Object> entries = new ArrayList<>();
List<Pair<StructurePoolElement, Integer>> templates = pool.getTemplates();
for (int index = 0; index < templates.size(); index++) {
Pair<StructurePoolElement, Integer> weighted = templates.get(index);
StructurePoolElement element = weighted.getFirst();
int weight = Math.max(1, weighted.getSecond());
Map<String, Object> entry = new LinkedHashMap<>();
if (element == EmptyPoolElement.INSTANCE) {
entry.put("empty", true);
} else {
entry.put("piece", captureElement(sourcePoolKey, index, element));
}
entry.put("weight", weight);
entries.add(entry);
}
Map<String, Object> poolJson = new LinkedHashMap<>();
poolJson.put("pieces", entries);
String fallback = resolvedPoolKey(pool.getFallback().value());
if (!fallback.equals(sourcePoolKey)) {
poolJson.put("fallback", poolName(targetKey.path(), fallback));
pendingPools.addLast(fallback);
}
pools.put(irisPoolName, poolJson);
}
private String captureElement(String sourcePoolKey, int index, StructurePoolElement element) throws IOException {
Objects.requireNonNull(element, "pool element");
if (element instanceof SinglePoolElement single) {
return captureSingle(single);
}
String generatedName = generatedPieceName(targetKey.path(), sourcePoolKey, index, element);
if (pieces.containsKey(generatedName)) {
return generatedName;
}
if (element instanceof ListPoolElement list) {
capabilities.add(StructureCapability.LIST_ELEMENTS);
CompositeCapture composite = captureList(list, generatedName);
blocks += composite.object().getBlocks().size();
emitPiece(generatedName, composite.object(), composite.losses(), element);
return generatedName;
}
IrisObject object = emptyObject(element);
StructureCapability unsupported = element instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.BLOCKS;
StructureLoss loss = StructureLoss.warning(
unsupported,
"unsupported_pool_element",
"Pool element " + element.getClass().getSimpleName() + " was represented as an empty Iris piece."
);
emitPiece(generatedName, object, List.of(loss), element);
return generatedName;
}
private String captureSingle(SinglePoolElement element) throws IOException {
Identifier templateIdentifier = element.getTemplateLocation();
StructureKey templateKey = StructureKey.parse(templateIdentifier.toString());
boolean legacy = element instanceof LegacySinglePoolElement;
String pieceName = legacy
? legacyPieceName(targetKey.path(), templateKey.value())
: pieceName(targetKey.path(), templateKey.value());
if (pieces.containsKey(pieceName)) {
return pieceName;
}
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
templateKey, template, blockRegistry, true, !legacy);
capabilities.addAll(capture.capabilities());
blocks += capture.blocks();
List<StructureLoss> pieceLosses = new ArrayList<>(capture.losses());
pieceLosses.addAll(elementLosses(element));
emitPiece(pieceName, capture.object(), pieceLosses, element);
return pieceName;
}
private CompositeCapture captureList(ListPoolElement list, String pieceName) throws IOException {
Vec3i size = list.getSize(templateManager, Rotation.NONE);
IrisObject composite = new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
List<StructureLoss> compositeLosses = new ArrayList<>();
for (StructurePoolElement child : list.getElements()) {
if (child == EmptyPoolElement.INSTANCE) {
continue;
}
if (child instanceof SinglePoolElement single) {
Identifier templateIdentifier = single.getTemplateLocation();
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
StructureKey.parse(templateIdentifier.toString()),
template,
blockRegistry,
true,
!(single instanceof LegacySinglePoolElement)
);
merge(composite, capture.object());
capabilities.addAll(capture.capabilities());
compositeLosses.addAll(capture.losses());
compositeLosses.addAll(elementLosses(single));
continue;
}
if (child instanceof ListPoolElement nested) {
CompositeCapture nestedCapture = captureList(nested, pieceName);
merge(composite, nestedCapture.object());
compositeLosses.addAll(nestedCapture.losses());
continue;
}
StructureCapability unsupported = child instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.LIST_ELEMENTS;
compositeLosses.add(StructureLoss.warning(
unsupported,
"list_child_not_imported",
"List element child " + child.getClass().getSimpleName() + " could not be flattened into the Iris object."
));
}
compositeLosses.addAll(elementLosses(list));
return new CompositeCapture(composite, compositeLosses);
}
private List<StructureLoss> elementLosses(StructurePoolElement element) {
List<StructureLoss> elementLosses = new ArrayList<>();
if (element.getProjection() != StructureTemplatePool.Projection.RIGID) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROJECTION,
"terrain_matching_projection_not_imported",
"Pool projection '" + element.getProjection().getSerializedName()
+ "' was converted to rigid Iris piece placement."
));
}
Tag encoded = StructurePoolElement.CODEC.encodeStart(registryOps, element).getOrThrow();
if (encoded instanceof CompoundTag compound) {
String processors = compound.getStringOr("processors", "");
if (!processors.isEmpty() && !processors.equals("minecraft:empty")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"native_processors_not_imported",
"Native processor list '" + processors + "' was not applied to the captured template."
));
}
if (compound.contains("override_liquid_settings")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"element_liquid_settings_not_imported",
"The pool element's liquid setting override is not represented by Iris placement."
));
}
}
return elementLosses;
}
private void emitPiece(
String pieceName,
IrisObject object,
List<StructureLoss> pieceLosses,
StructurePoolElement element
) throws IOException {
String objectResource = "objects/" + pieceName + ".iob";
objects.put(pieceName, serialize(object));
for (StructureLoss loss : pieceLosses) {
losses.add(loss.affecting(objectResource));
}
List<Map<String, Object>> connectors = connectors(element, pieceName);
Map<String, Object> pieceJson = new LinkedHashMap<>();
pieceJson.put("object", pieceName);
pieceJson.put("connectors", connectors);
pieceJson.put("rotatable", true);
pieces.put(pieceName, pieceJson);
}
private List<Map<String, Object>> connectors(StructurePoolElement element, String pieceName) {
List<StructureTemplate.JigsawBlockInfo> sourceConnectors = element.getShuffledJigsawBlocks(
templateManager,
BlockPos.ZERO,
Rotation.NONE,
RandomSource.create(stableSeed(sourceKey.value() + ":" + pieceName))
);
List<Map<String, Object>> connectors = new ArrayList<>(sourceConnectors.size());
for (StructureTemplate.JigsawBlockInfo source : sourceConnectors) {
recordConnectorLosses(source, pieceName);
ResourceKey<StructureTemplatePool> resolvedPool = aliases.getOrDefault(source.pool(), source.pool());
String sourcePoolKey = resolvedPool.identifier().toString();
pendingPools.addLast(sourcePoolKey);
Map<String, Object> position = new LinkedHashMap<>();
position.put("x", source.info().pos().getX());
position.put("y", source.info().pos().getY());
position.put("z", source.info().pos().getZ());
Map<String, Object> connector = new LinkedHashMap<>();
connector.put("position", position);
connector.put("direction", directionName(JigsawBlock.getFrontFacing(source.info().state())));
connector.put("top", directionName(JigsawBlock.getTopFacing(source.info().state())));
connector.put("pool", poolName(targetKey.path(), sourcePoolKey));
connector.put("name", source.name().toString());
connector.put("targetName", source.target().toString());
connector.put("joint", source.jointType().getSerializedName().equals("aligned") ? "ALIGNED" : "ROLLABLE");
connectors.add(connector);
}
return connectors;
}
private void recordConnectorLosses(StructureTemplate.JigsawBlockInfo connector, String pieceName) {
String pieceResource = "jigsaw-pieces/" + pieceName + ".json";
if (connector.placementPriority() != 0 || connector.selectionPriority() != 0) {
addLossOnce(
"connector-priority:" + pieceName,
StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_priorities_not_imported",
"Native connector placement and selection priorities are not represented by Iris assembly ordering."
).affecting(pieceResource)
);
}
}
private StructureResourceBundle buildBundle(
StructureSource source,
String startPoolKey,
int maxDepth,
int maxDistance
) {
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(targetKey)
.source(source)
.backend(StructureBackend.IRIS_ASSEMBLY)
.capabilities(capabilities)
.losses(losses);
for (Map.Entry<String, byte[]> entry : objects.entrySet()) {
bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue());
}
for (Map.Entry<String, Map<String, Object>> entry : pieces.entrySet()) {
bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
for (Map.Entry<String, Map<String, Object>> entry : pools.entrySet()) {
bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
bundle.textResource(
"structures/" + targetKey.path() + ".json",
GSON.toJson(structureJson(sourceKey.value(), poolName(targetKey.path(), startPoolKey), maxDepth, maxDistance))
);
return bundle.build();
}
private String resolvedPoolKey(StructureTemplatePool pool) {
ResourceKey<StructureTemplatePool> raw = poolResourceKey(pool);
return aliases.getOrDefault(raw, raw).identifier().toString();
}
private String poolKey(StructureTemplatePool pool) {
return poolResourceKey(pool).identifier().toString();
}
private ResourceKey<StructureTemplatePool> poolResourceKey(StructureTemplatePool pool) {
Identifier identifier = poolRegistry.getKey(pool);
if (identifier == null) {
throw new IllegalStateException("Jigsaw structure references an unregistered template pool");
}
return ResourceKey.create(Registries.TEMPLATE_POOL, identifier);
}
private void addLossOnce(String key, StructureLoss loss) {
if (recordedLosses.add(key)) {
losses.add(loss);
}
}
private static void merge(IrisObject target, IrisObject source) {
for (IrisBlockVector position : source.getBlocks().keys()) {
int x = position.getBlockX() + source.getCenter().getX();
int y = position.getBlockY() + source.getCenter().getY();
int z = position.getBlockZ() + source.getCenter().getZ();
if (x < 0 || y < 0 || z < 0 || x >= target.getW() || y >= target.getH() || z >= target.getD()) {
continue;
}
target.setUnsignedTile(x, y, z, null);
target.setUnsigned(x, y, z, source.getBlocks().get(position));
TileData tile = source.getStates().get(position);
if (tile != null) {
target.setUnsignedTile(x, y, z, tile);
}
}
}
private static IrisObject emptyObject() {
return new IrisObject(1, 1, 1);
}
private IrisObject emptyObject(StructurePoolElement element) {
Vec3i size = element.getSize(templateManager, Rotation.NONE);
return new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
}
private static byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
private static int readHorizontalDistance(CompoundTag encodedStructure) {
int scalar = encodedStructure.getIntOr("max_distance_from_center", -1);
if (scalar > 0) {
return scalar;
}
CompoundTag compound = encodedStructure.getCompoundOrEmpty("max_distance_from_center");
return Math.max(1, compound.getIntOr("horizontal", 80));
}
static Map<String, Object> structureJson(String source, String startPool, int maxDepth, int maxDistance) {
Map<String, Object> root = new LinkedHashMap<>();
root.put("startPool", startPool);
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
root.put("maxSizeChunks", Math.max(1, Math.min(32, (Math.max(1, maxDistance) + 15) / 16)));
root.put("placeMode", "STRUCTURE_PIECE");
root.put("vanillaSource", source);
return root;
}
static String poolName(String base, String sourcePoolKey) {
StructureKey key = StructureKey.parse(sourcePoolKey);
return base + "/pool/" + key.namespace() + "/" + key.path();
}
static String pieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/" + key.namespace() + "/" + key.path();
}
static String legacyPieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/generated/legacy/" + key.namespace() + "/" + key.path();
}
static String directionName(Direction direction) {
return switch (direction) {
case UP -> "UP_POSITIVE_Y";
case DOWN -> "DOWN_NEGATIVE_Y";
case SOUTH -> "SOUTH_POSITIVE_Z";
case EAST -> "EAST_POSITIVE_X";
case WEST -> "WEST_NEGATIVE_X";
case NORTH -> "NORTH_NEGATIVE_Z";
};
}
private static String generatedPieceName(String base, String sourcePoolKey, int index, StructurePoolElement element) {
StructureKey poolKey = StructureKey.parse(sourcePoolKey);
String type = element instanceof ListPoolElement ? "list" : element instanceof FeaturePoolElement ? "feature" : "unsupported";
return base + "/piece/generated/" + type + "/" + poolKey.namespace() + "/" + poolKey.path() + "/" + index;
}
private static long stableSeed(String value) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < value.length(); index++) {
hash ^= value.charAt(index);
hash *= 0x100000001b3L;
}
return hash;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
record Capture(StructureResourceBundle bundle, int blocks, int pieces, int pools) {
Capture {
Objects.requireNonNull(bundle);
}
}
static final class UnsupportedStructureTypeException extends IllegalArgumentException {
UnsupportedStructureTypeException(String message) {
super(message);
}
}
private record CompositeCapture(IrisObject object, List<StructureLoss> losses) {
CompositeCapture {
Objects.requireNonNull(object);
losses = List.copyOf(losses);
}
}
}
@@ -1,426 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import net.minecraft.SharedConstants;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public final class ModdedStructureImportService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private final Supplier<MinecraftServer> server;
public ModdedStructureImportService(Supplier<MinecraftServer> server) {
this.server = Objects.requireNonNull(server);
}
public List<StructureKey> templateKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
return activeServer.getStructureManager().listTemplates()
.map((Identifier identifier) -> StructureKey.parse(identifier.toString()))
.sorted()
.toList();
} catch (RuntimeException failure) {
throw report("Failed to list native structure templates", failure);
}
}
public List<StructureKey> jigsawStructureKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
Registry<Structure> registry = activeServer.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<StructureKey> keys = new ArrayList<>();
for (Identifier identifier : registry.keySet()) {
if (registry.getValue(identifier) instanceof JigsawStructure) {
keys.add(StructureKey.parse(identifier.toString()));
}
}
keys.sort(Comparator.naturalOrder());
return List.copyOf(keys);
} catch (RuntimeException failure) {
throw report("Failed to list registered jigsaw structures", failure);
}
}
public PreparedImport prepareTemplate(TemplateImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
Identifier sourceIdentifier = identifier(options.sourceKey());
StructureTemplate template = activeServer.getStructureManager().get(sourceIdentifier)
.orElseThrow(() -> new IllegalArgumentException("No structure template exists for " + options.sourceKey()));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
options.sourceKey(),
template,
activeServer.registryAccess().lookupOrThrow(Registries.BLOCK),
false
);
StructureSource source = StructureSource.identified(
options.sourceKind(),
options.sourceKey(),
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(capture.sourceTag()).getBytes(StandardCharsets.UTF_8)
);
IrisStructureBundleFactory.SinglePieceOptions bundleOptions = new IrisStructureBundleFactory.SinglePieceOptions(
options.targetKey(),
source,
options.targetKey().path(),
capture.object(),
Math.max(capture.width(), capture.depth()),
options.placeMode(),
options.objectOnly(),
capture.capabilities(),
capture.losses()
);
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(bundleOptions);
if (!options.objectOnly()) {
StructureResourceBundleGraphCompiler.requireViable(bundle);
}
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.TEMPLATE,
bundle,
capture.blocks(),
1,
options.objectOnly() ? 0 : 1
);
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare structure template import " + options.sourceKey(), failure);
}
}
public PreparedImport prepareJigsawStructure(JigsawImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
ModdedJigsawStructureCapture.Capture capture = ModdedJigsawStructureCapture.capture(
activeServer,
options.sourceKey(),
options.targetKey(),
options.sourceKind()
);
StructureResourceBundleGraphCompiler.requireViable(capture.bundle());
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.JIGSAW_STRUCTURE,
capture.bundle(),
capture.blocks(),
capture.pieces(),
capture.pools()
);
} catch (ModdedJigsawStructureCapture.UnsupportedStructureTypeException failure) {
StructureLoss loss = StructureLoss.error(
StructureCapability.IRIS_PLACEMENT,
"unsupported_native_structure_type",
failure.getMessage()
);
throw report("Cannot import native structure graph " + options.sourceKey(), failure, List.of(loss));
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare jigsaw structure import " + options.sourceKey(), failure);
}
}
public ImportResult write(PreparedImport prepared) {
Objects.requireNonNull(prepared);
StructureWriteResult writeResult = new StructureTransactionWriter(prepared.packRoot())
.write(prepared.bundle(), prepared.writeOptions());
writeResult.failure().ifPresent((Throwable failure) -> LOGGER.error(
"Failed to commit modded structure import {} to {}",
prepared.bundle().key(),
prepared.packRoot(),
failure
));
if (writeResult.committed()) {
IrisData.getLoaded(prepared.packRoot().toFile()).ifPresent(IrisData::invalidateStructureResources);
}
String message = writeMessage(prepared, writeResult);
return new ImportResult(
writeResult.successful(),
message,
prepared.kind(),
prepared.bundle().key(),
prepared.blocks(),
prepared.pieces(),
prepared.pools(),
prepared.bundle().capabilities(),
prepared.bundle().losses(),
Optional.of(writeResult)
);
}
public ImportResult importTemplate(TemplateImportOptions options) {
try {
return write(prepareTemplate(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.TEMPLATE, failure.getMessage(), failure.losses());
}
}
public ImportResult importJigsawStructure(JigsawImportOptions options) {
try {
return write(prepareJigsawStructure(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.JIGSAW_STRUCTURE, failure.getMessage(), failure.losses());
}
}
private MinecraftServer requireServerThread() {
MinecraftServer activeServer = server.get();
if (activeServer == null) {
throw new IllegalStateException("Minecraft server is not available");
}
if (!activeServer.isSameThread()) {
throw new IllegalStateException("Structure registry capture must run on the logical server thread");
}
return activeServer;
}
private StructureImportException report(String context, Exception failure) {
return report(context, failure, List.of());
}
private StructureImportException report(String context, Exception failure, List<StructureLoss> losses) {
LOGGER.error(context, failure);
return new StructureImportException(context + ": " + failureDetail(failure), failure, losses);
}
private static ImportResult failed(
StructureKey targetKey,
ImportKind kind,
String message,
List<StructureLoss> losses
) {
return new ImportResult(
false,
message,
kind,
targetKey,
0,
0,
0,
Set.of(),
losses,
Optional.empty()
);
}
private static String writeMessage(PreparedImport prepared, StructureWriteResult result) {
if (result.successful()) {
return switch (result.status()) {
case DRY_RUN -> "Validated import of '" + prepared.bundle().key() + "' without writing files";
case ADDED -> "Imported '" + prepared.bundle().key() + "'";
case OVERWRITTEN -> "Overwrote owned import '" + prepared.bundle().key() + "'";
case UNCHANGED -> "Import '" + prepared.bundle().key() + "' is already current";
case COMMITTED_CLEANUP_REQUIRED -> "Imported '" + prepared.bundle().key()
+ "'; obsolete staging cleanup is still required";
default -> "Imported '" + prepared.bundle().key() + "'";
};
}
if (!result.conflicts().isEmpty()) {
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
return "Import conflict for '" + prepared.bundle().key() + "': " + conflict.relativePath()
+ " is " + conflict.reason().name().toLowerCase() + "; existing files were preserved";
}
return "Import failed for '" + prepared.bundle().key() + "': "
+ result.failure().map(ModdedStructureImportService::failureDetail).orElse(result.status().name());
}
private static String failureDetail(Throwable failure) {
String message = failure.getMessage();
return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
public enum ImportKind {
TEMPLATE,
JIGSAW_STRUCTURE
}
public record TemplateImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions,
boolean objectOnly,
String placeMode
) {
public TemplateImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(placeMode);
if (placeMode.isBlank()) {
throw new IllegalArgumentException("Structure place mode cannot be blank");
}
}
public static TemplateImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new TemplateImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false),
false,
"CENTER_HEIGHT"
);
}
}
public record JigsawImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions
) {
public JigsawImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
}
public static JigsawImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new JigsawImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false)
);
}
}
public record PreparedImport(
Path packRoot,
StructureWriteOptions writeOptions,
ImportKind kind,
StructureResourceBundle bundle,
int blocks,
int pieces,
int pools
) {
public PreparedImport {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(kind);
Objects.requireNonNull(bundle);
if (blocks < 0 || pieces < 0 || pools < 0) {
throw new IllegalArgumentException("Prepared import counts cannot be negative");
}
}
}
public record ImportResult(
boolean success,
String message,
ImportKind kind,
StructureKey targetKey,
int blocks,
int pieces,
int pools,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
Optional<StructureWriteResult> writeResult
) {
public ImportResult {
Objects.requireNonNull(message);
Objects.requireNonNull(kind);
Objects.requireNonNull(targetKey);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
Objects.requireNonNull(writeResult);
}
}
public static final class StructureImportException extends Exception {
private final List<StructureLoss> losses;
public StructureImportException(String message, Throwable cause, List<StructureLoss> losses) {
super(message, cause);
this.losses = List.copyOf(losses);
}
public List<StructureLoss> losses() {
return losses;
}
}
private static Path normalizedRoot(Path path) {
return Objects.requireNonNull(path).toAbsolutePath().normalize();
}
private static StructureSource.Kind inferredSourceKind(StructureKey sourceKey) {
return sourceKey.namespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
}
}
@@ -1,339 +0,0 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
final class ModdedStructureTemplateCapture {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private ModdedStructureTemplateCapture() {
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return capture(sourceKey, template, blockLookup, connectorsPreserved, true);
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(template);
CompoundTag sourceTag = template.save(new CompoundTag());
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, includeAir);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, true);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(sourceTag);
Objects.requireNonNull(blockLookup);
Vec3i size = readSize(sourceTag);
IrisObject object = new IrisObject(size.getX(), size.getY(), size.getZ());
List<StructureLoss> losses = new ArrayList<>();
ListTag palette = firstPalette(sourceTag, losses);
ListTag blocks = sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG);
CaptureCounts counts = captureBlocks(sourceKey, palette, blocks, blockLookup, object, losses, includeAir);
recordEntityLoss(sourceTag, losses);
recordMarkerLosses(counts, connectorsPreserved, losses);
EnumSet<StructureCapability> capabilities = EnumSet.of(StructureCapability.BLOCKS);
if (counts.tiles() > 0) {
capabilities.add(StructureCapability.BLOCK_ENTITIES);
}
if (connectorsPreserved && counts.jigsaws() > 0) {
capabilities.add(StructureCapability.CONNECTORS);
}
return new Capture(
object,
counts.blocks(),
counts.tiles(),
counts.jigsaws(),
counts.dataMarkers(),
size.getX(),
size.getY(),
size.getZ(),
capabilities,
losses,
sourceTag.copy()
);
}
private static Vec3i readSize(CompoundTag sourceTag) {
ListTag size = sourceTag.getListOrEmpty(StructureTemplate.SIZE_TAG);
int width = size.getIntOr(0, 0);
int height = size.getIntOr(1, 0);
int depth = size.getIntOr(2, 0);
if (width < 1 || height < 1 || depth < 1) {
throw new IllegalArgumentException("Structure template has invalid dimensions "
+ width + "x" + height + "x" + depth);
}
return new Vec3i(width, height, depth);
}
private static ListTag firstPalette(CompoundTag sourceTag, List<StructureLoss> losses) {
ListTag palettes = sourceTag.getList(StructureTemplate.PALETTE_LIST_TAG).orElse(null);
if (palettes != null) {
if (palettes.isEmpty()) {
throw new IllegalArgumentException("Structure template has no palettes");
}
if (palettes.size() > 1) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"palette_variants_not_imported",
"Only palette 0 was converted; " + (palettes.size() - 1)
+ " additional palette(s) remain native-only."
));
}
return palettes.getListOrEmpty(0);
}
ListTag palette = sourceTag.getListOrEmpty(StructureTemplate.PALETTE_TAG);
if (palette.isEmpty() && !sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG).isEmpty()) {
throw new IllegalArgumentException("Structure template has blocks but no palette");
}
return palette;
}
private static CaptureCounts captureBlocks(
StructureKey sourceKey,
ListTag palette,
ListTag blocks,
HolderGetter<Block> blockLookup,
IrisObject object,
List<StructureLoss> losses,
boolean includeAir
) {
int blockCount = 0;
int tiles = 0;
int jigsaws = 0;
int dataMarkers = 0;
for (int index = 0; index < blocks.size(); index++) {
CompoundTag blockTag = blocks.getCompoundOrEmpty(index);
BlockPosition position = readPosition(blockTag);
if (!withinObject(position, object)) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"out_of_bounds_block_not_imported",
"Block " + index + " at " + position.x() + "," + position.y() + "," + position.z()
+ " is outside the declared template bounds."
));
continue;
}
int paletteIndex = blockTag.getIntOr(StructureTemplate.BLOCK_TAG_STATE, 0);
BlockState state = NbtUtils.readBlockState(blockLookup, palette.getCompoundOrEmpty(paletteIndex));
CompoundTag blockEntityTag = blockTag.getCompound(StructureTemplate.BLOCK_TAG_NBT).orElse(null);
if (state.is(Blocks.STRUCTURE_VOID)) {
continue;
}
if (state.is(Blocks.STRUCTURE_BLOCK)) {
dataMarkers++;
continue;
}
if (state.is(Blocks.JIGSAW)) {
jigsaws++;
BlockState finalState = resolveJigsawFinalState(sourceKey, position, blockEntityTag, losses);
if (finalState == null || finalState.isAir()) {
continue;
}
state = finalState;
blockEntityTag = null;
}
if (!includeAir && state.isAir()) {
continue;
}
object.setUnsigned(position.x(), position.y(), position.z(), ModdedBlockState.of(state, null));
blockCount++;
if (blockEntityTag != null && captureTile(sourceKey, position, state, blockEntityTag, object, losses)) {
tiles++;
}
}
return new CaptureCounts(blockCount, tiles, jigsaws, dataMarkers);
}
private static BlockPosition readPosition(CompoundTag blockTag) {
ListTag position = blockTag.getListOrEmpty(StructureTemplate.BLOCK_TAG_POS);
return new BlockPosition(
position.getIntOr(0, Integer.MIN_VALUE),
position.getIntOr(1, Integer.MIN_VALUE),
position.getIntOr(2, Integer.MIN_VALUE)
);
}
private static boolean withinObject(BlockPosition position, IrisObject object) {
return position.x() >= 0 && position.x() < object.getW()
&& position.y() >= 0 && position.y() < object.getH()
&& position.z() >= 0 && position.z() < object.getD();
}
private static BlockState resolveJigsawFinalState(
StructureKey sourceKey,
BlockPosition position,
CompoundTag blockEntityTag,
List<StructureLoss> losses
) {
String finalState = blockEntityTag == null
? "minecraft:air"
: blockEntityTag.getStringOr("final_state", "minecraft:air");
try {
return ModdedBlockResolution.strictParse(finalState).handle();
} catch (IllegalArgumentException failure) {
LOGGER.error("Failed to parse jigsaw final state '{}' in {} at {},{},{}",
finalState, sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"jigsaw_final_state_not_imported",
"Jigsaw final state '" + finalState + "' at " + position.x() + "," + position.y() + ","
+ position.z() + " could not be parsed."
));
return null;
}
}
private static boolean captureTile(
StructureKey sourceKey,
BlockPosition position,
BlockState state,
CompoundTag blockEntityTag,
IrisObject object,
List<StructureLoss> losses
) {
try {
String blockKey = ModdedBlockState.serialize(state);
ModdedTileData tile = ModdedTileData.capture(blockKey, NbtUtils.structureToSnbt(blockEntityTag));
object.setUnsignedTile(position.x(), position.y(), position.z(), tile);
return true;
} catch (IOException | RuntimeException failure) {
LOGGER.error("Failed to capture block entity in {} at {},{},{}",
sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCK_ENTITIES,
"block_entity_not_imported",
"Block entity data at " + position.x() + "," + position.y() + "," + position.z()
+ " could not be encoded."
));
return false;
}
}
private static void recordEntityLoss(CompoundTag sourceTag, List<StructureLoss> losses) {
int entityCount = sourceTag.getListOrEmpty(StructureTemplate.ENTITIES_TAG).size();
if (entityCount > 0) {
losses.add(StructureLoss.warning(
StructureCapability.ENTITIES,
"entities_not_imported",
entityCount + " structure entit" + (entityCount == 1 ? "y was" : "ies were")
+ " not converted into the Iris snapshot."
));
}
}
private static void recordMarkerLosses(
CaptureCounts counts,
boolean connectorsPreserved,
List<StructureLoss> losses
) {
if (counts.dataMarkers() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"data_markers_not_imported",
counts.dataMarkers() + " structure data marker(s) require native pool-element handlers and were omitted."
));
}
if (!connectorsPreserved && counts.jigsaws() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connectors_not_imported",
counts.jigsaws() + " jigsaw connector(s) were resolved to final blocks without importing their pool graph."
));
}
}
record Capture(
IrisObject object,
int blocks,
int tiles,
int jigsaws,
int dataMarkers,
int width,
int height,
int depth,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
CompoundTag sourceTag
) {
Capture {
Objects.requireNonNull(object);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
sourceTag = sourceTag.copy();
}
}
private record CaptureCounts(int blocks, int tiles, int jigsaws, int dataMarkers) {
}
private record BlockPosition(int x, int y, int z) {
}
}
@@ -58,8 +58,8 @@ public class IrisModdedChunkGeneratorSpawnTest {
int spawnEnd = source.indexOf("@Override", spawnStart + 1);
String spawn = source.substring(spawnStart, spawnEnd);
assertTrue(spawn.contains("initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("vanillaSpawnBiomes.get(visibleBiome.value())"));
assertTrue(spawn.contains("spawnTables.initializeVanillaSpawnBiomes(registry)"));
assertTrue(spawn.contains("spawnTables.vanillaSpawnBiome(visibleBiome.value())"));
assertTrue(spawn.contains("NaturalSpawner.spawnMobsForChunkGeneration("));
assertTrue(spawn.contains("new LegacyRandomSource(RandomSupport.generateUniqueSeed())"));
assertTrue(spawn.contains("random.setDecorationSeed(region.getSeed()"));

Some files were not shown because too many files have changed in this diff Show More