mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-30 05:50:48 +00:00
🧹
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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__/
|
||||
|
||||
@@ -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)
|
||||
|
||||
+30
-7
@@ -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
|
||||
|
||||
+32
-24
@@ -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());
|
||||
|
||||
+60
-11
@@ -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,
|
||||
|
||||
+21
@@ -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);
|
||||
|
||||
+15
-6
@@ -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
@@ -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;
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+12
-12
@@ -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);
|
||||
|
||||
+8
-8
@@ -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"));
|
||||
|
||||
+10
-10
@@ -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());
|
||||
|
||||
+89
-90
@@ -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];
|
||||
|
||||
+15
-15
@@ -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;
|
||||
}
|
||||
}
|
||||
+173
@@ -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());
|
||||
}
|
||||
}
|
||||
+228
@@ -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();
|
||||
}
|
||||
}
|
||||
+34
-16
@@ -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)));
|
||||
// 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;
|
||||
|
||||
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, sender(), centerX, centerZ, radius).start();
|
||||
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)
|
||||
|
||||
+1
-14
@@ -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)
|
||||
|
||||
+107
-55
@@ -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));
|
||||
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());
|
||||
Location[] b = WandSVC.getCuboid(player);
|
||||
if (b == null || b[0] == null || b[1] == null) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_NO_AREA_SELECTED));
|
||||
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();
|
||||
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);
|
||||
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));
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND));
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
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();
|
||||
g[1] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[1] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
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,13 +507,17 @@ 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));
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_READY_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (WandSVC.isHoldingIrisWand(player())) {
|
||||
Location[] g = WandSVC.getCuboid(player());
|
||||
if (WandSVC.isHoldingIrisWand(player)) {
|
||||
Location[] g = WandSVC.getCuboid(player);
|
||||
|
||||
if (g == null) {
|
||||
return;
|
||||
@@ -506,12 +525,13 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
if (!here) {
|
||||
// TODO: WARNING HEIGHT
|
||||
g[0] = player().getTargetBlock(null, 256).getLocation().clone();
|
||||
g[0] = player.getTargetBlock(null, 256).getLocation().clone();
|
||||
} else {
|
||||
g[0] = player().getLocation().getBlock().getLocation().clone().add(0, -1, 0);
|
||||
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) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
|
||||
return;
|
||||
}
|
||||
|
||||
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)));
|
||||
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)) {
|
||||
wand = newWand;
|
||||
player().getInventory().setItemInMainHand(wand);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB, MessageArgument.untrusted("value", o.getLoadKey())));
|
||||
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());
|
||||
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())));
|
||||
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);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", o.getLoadKey())));
|
||||
player.getInventory().setItem(slot, newWand);
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_UPDATED_WAND_OBJECTS_IOB_2, MessageArgument.untrusted("value", placed.getLoadKey())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_PLACED, MessageArgument.untrusted("object", object)));
|
||||
});
|
||||
})) {
|
||||
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,19 +658,23 @@ 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));
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
onPlayerThread(player, () -> {
|
||||
if (!WandSVC.isHoldingWand(player)) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_OBJECT_HOLD_YOUR_WAND_2));
|
||||
return;
|
||||
}
|
||||
|
||||
Location[] b = WandSVC.getCuboid(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));
|
||||
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();
|
||||
Direction d = Direction.closest(player.getLocation().getDirection()).reverse();
|
||||
if (d == null) {
|
||||
return; // HOW DID THIS HAPPEN
|
||||
}
|
||||
@@ -637,9 +683,10 @@ public class CommandObject implements DirectorExecutor {
|
||||
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);
|
||||
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)
|
||||
|
||||
+44
-6
@@ -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,11 +730,17 @@ 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);
|
||||
|
||||
// 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();
|
||||
|
||||
try {
|
||||
Location l = player().getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
Location l = reporter.getTargetBlockExact(48, FluidCollisionMode.NEVER).getLocation();
|
||||
|
||||
int cx = l.getChunk().getX();
|
||||
int cz = l.getChunk().getZ();
|
||||
@@ -732,6 +750,26 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
+56
-19
@@ -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,68 +102,86 @@ 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() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
World world = world();
|
||||
Engine engine = engine();
|
||||
|
||||
// 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);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
|
||||
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);
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
|
||||
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() {
|
||||
VolmitSender commandSender = sender();
|
||||
Player player = player();
|
||||
|
||||
// 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();
|
||||
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));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLEASE_LOOK_AT_ANY_BLOCK_NOT_AT_SKY));
|
||||
bd = null;
|
||||
}
|
||||
|
||||
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))));
|
||||
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.isStorage(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_STORAGE_BLOCK_LOOT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isLit(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_LIT_BLOCK_LIGHT_CAPABLE));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliage(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isDecorant(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DECORANT_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFluid(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FLUID_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isFoliagePlantable(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_PLANTABLE_FOLIAGE_BLOCK));
|
||||
}
|
||||
|
||||
if (BukkitBlockResolution.isSolid(bd)) {
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_SOLID_BLOCK));
|
||||
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();
|
||||
|
||||
// Chunk lookup plus the block signals both need the thread owning the player's chunk.
|
||||
onPlayerThread(player, () -> {
|
||||
Chunk c = player.getLocation().getChunk();
|
||||
|
||||
if (IrisToolbelt.isIrisWorld(c.getWorld())) {
|
||||
int m = 1;
|
||||
AtomicInteger v = new AtomicInteger(0);
|
||||
|
||||
for (int xxx = c.getX() - 4; xxx <= c.getX() + 4; xxx++) {
|
||||
@@ -171,9 +194,23 @@ public class CommandWhat implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_FOUND_NEARBY_MARKERS, MessageArgument.untrusted("value", v.get()), MessageArgument.untrusted("marker", marker)));
|
||||
commandSender.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_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() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+92
-22
@@ -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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
}
|
||||
markers.add(GuiMarker.entity(label, entity.getLocation().getX(), entity.getLocation().getY(), entity.getLocation().getZ(),
|
||||
entity.getHealth(), maxHealth));
|
||||
}
|
||||
sink.accept(markers);
|
||||
});
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
+81
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+34
-814
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+590
@@ -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;
|
||||
}
|
||||
}
|
||||
+151
@@ -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 {
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+215
@@ -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) {
|
||||
}
|
||||
}
|
||||
+9
-1578
File diff suppressed because it is too large
Load Diff
+148
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
+279
@@ -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) {
|
||||
}
|
||||
}
|
||||
+508
@@ -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) {
|
||||
}
|
||||
}
|
||||
+174
@@ -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) {
|
||||
}
|
||||
}
|
||||
+365
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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,14 +29,14 @@ 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,
|
||||
public static void primeTerrain(ChunkAccess chunk, IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
Objects.requireNonNull(chunk, "Iris worldgen heightmap priming requires a chunk");
|
||||
Objects.requireNonNull(surfaceFirstFreeY,
|
||||
@@ -47,7 +47,7 @@ final class WorldgenTerrainHeightmaps {
|
||||
write(chunk, Heightmap.Types.OCEAN_FLOOR_WG, floorFirstFreeY);
|
||||
}
|
||||
|
||||
static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
|
||||
public static void primeStructurePlacement(WorldGenLevel world, List<StructureStart> starts,
|
||||
IntBinaryOperator surfaceFirstFreeY,
|
||||
IntBinaryOperator floorFirstFreeY) {
|
||||
Objects.requireNonNull(world,
|
||||
+24
-3
@@ -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)
|
||||
|
||||
+40
-605
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -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;
|
||||
|
||||
+98
@@ -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) {
|
||||
}
|
||||
}
|
||||
+32
-12
@@ -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<>();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+22
-1
@@ -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,16 +267,22 @@ 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());
|
||||
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) {
|
||||
int size = container.getContainerSize();
|
||||
|
||||
+439
@@ -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);
|
||||
|
||||
+119
@@ -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) {
|
||||
|
||||
+16
-1
@@ -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");
|
||||
|
||||
+19
-1041
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) {
|
||||
|
||||
+114
@@ -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");
|
||||
}
|
||||
}
|
||||
+130
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+783
@@ -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);
|
||||
}
|
||||
|
||||
+19
@@ -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);
|
||||
|
||||
+79
-1
@@ -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;
|
||||
|
||||
+63
@@ -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
|
||||
}
|
||||
|
||||
+26
-1091
File diff suppressed because it is too large
Load Diff
+191
@@ -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);
|
||||
}
|
||||
}
|
||||
+344
@@ -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())));
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
) {
|
||||
}
|
||||
|
||||
+133
@@ -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;
|
||||
}
|
||||
}
|
||||
+535
@@ -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) {
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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();
|
||||
};
|
||||
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-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();
|
||||
};
|
||||
|
||||
+8
@@ -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,9 +304,16 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
|
||||
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);
|
||||
if (!MainWorldService.stage(packRef, seed)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_WORLD_COMMANDS_FAILED_WRITE_SERVER_PROPERTIES_CHECK_FILE_PERMISSIONS_SET_LEVEL_TYPE));
|
||||
|
||||
-620
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-426
@@ -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;
|
||||
}
|
||||
}
|
||||
-339
@@ -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) {
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user