Improve generation and lifecycle safety

- Reduce terrain, noise, hydrology, and decorator sampling overhead.
- Reduce object transaction allocations while preserving rollback behavior.
- Optimize structure placement and cave anchor sampling.
- Parallelize Studio cache warmup and protect world entry and hotload recovery.
- Validate pack fingerprints and publish pack replacements atomically.
- Notify modded operators about outdated packs without synchronizing pack data.
- Harden world creation, loading, evacuation, and teleport failure recovery.
- Coalesce queued region flushes while preserving chunk save ordering.
- Remove obsolete mantle, pack, and cave planning code.
- Add regression tests and reusable gameplay fixtures.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-06 20:51:28 -04:00
1 parent a2bcd7a302
commit 7f5ce70f49
197 files changed
+7560 -4420

No files matched your search

@@ -54,7 +54,7 @@ public final class IrisSplashPackScanner {
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
JsonElement version = json.get("version");
if (version == null || !version.isJsonPrimitive()) {
return null;
return new SplashPackMetadata(dimName, "unknown");
}
return new SplashPackMetadata(dimName, version.getAsString());
@@ -96,8 +96,6 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
private static final int CHILDREN_SEED_SALT = 2137;
private final transient AtomicCache<KMap<String, IrisBiomeGeneratorLink>> genCache = new AtomicCache<>();
private final transient AtomicCache<KMap<String, Integer>> genCacheMax = new AtomicCache<>();
private final transient AtomicCache<KMap<String, Integer>> genCacheMin = new AtomicCache<>();
private final transient AtomicCache<KList<IrisObjectPlacement>> surfaceObjectsCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisObjectPlacement>> carveObjectsCache = new AtomicCache<>();
private final transient AtomicCache<Color> cacheColor = new AtomicCache<>();
@@ -415,12 +413,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
return getCustomDerivitives() != null && getCustomDerivitives().isNotEmpty();
}
public double getGenLinkMax(String loadKey, Engine engine) {
return IrisBiomeGenLinks.getGenLinkMax(this, loadKey, engine);
public double getGenLinkMax(String loadKey) {
return IrisBiomeGenLinks.getGenLinkMax(this, loadKey);
}
public double getGenLinkMin(String loadKey, Engine engine) {
return IrisBiomeGenLinks.getGenLinkMin(this, loadKey, engine);
public double getGenLinkMin(String loadKey) {
return IrisBiomeGenLinks.getGenLinkMin(this, loadKey);
}
public IrisBiomeGeneratorLink getGenLink(String loadKey) {
@@ -19,7 +19,6 @@
package art.arcane.iris.generation.biome;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.collection.KMap;
/**
@@ -31,24 +30,14 @@ final class IrisBiomeGenLinks {
private IrisBiomeGenLinks() {
}
static double getGenLinkMax(IrisBiome biome, String loadKey, Engine engine) {
if (loadKey == null || loadKey.isBlank()) {
return 0;
}
Integer v = maxIndex(biome).get(loadKey);
return v == null ? 0 : v;
static double getGenLinkMax(IrisBiome biome, String loadKey) {
IrisBiomeGeneratorLink link = getGenLink(biome, loadKey);
return link == null ? 0 : link.getMax();
}
static double getGenLinkMin(IrisBiome biome, String loadKey, Engine engine) {
if (loadKey == null || loadKey.isBlank()) {
return 0;
}
Integer v = minIndex(biome).get(loadKey);
return v == null ? 0 : v;
static double getGenLinkMin(IrisBiome biome, String loadKey) {
IrisBiomeGeneratorLink link = getGenLink(biome, loadKey);
return link == null ? 0 : link.getMin();
}
static IrisBiomeGeneratorLink getGenLink(IrisBiome biome, String loadKey) {
@@ -59,57 +48,6 @@ final class IrisBiomeGenLinks {
return linkIndex(biome).get(loadKey);
}
private static KMap<String, Integer> maxIndex(IrisBiome biome) {
AtomicCache<KMap<String, Integer>> cache = biome.getGenCacheMax();
KMap<String, Integer> cached = cache.getIfPresent();
if (cached != null) {
return cached;
}
return cache.aquire(() ->
{
KMap<String, Integer> l = new KMap<>();
for (IrisBiomeGeneratorLink i : biome.getGenerators()) {
String generatorKey = i.getGenerator();
if (generatorKey == null || generatorKey.isBlank()) {
continue;
}
l.put(generatorKey, i.getMax());
}
return l;
});
}
private static KMap<String, Integer> minIndex(IrisBiome biome) {
AtomicCache<KMap<String, Integer>> cache = biome.getGenCacheMin();
KMap<String, Integer> cached = cache.getIfPresent();
if (cached != null) {
return cached;
}
return cache.aquire(() ->
{
KMap<String, Integer> l = new KMap<>();
for (IrisBiomeGeneratorLink i : biome.getGenerators()) {
String generatorKey = i.getGenerator();
if (generatorKey == null || generatorKey.isBlank()) {
continue;
}
l.put(generatorKey, i.getMin());
}
return l;
});
}
private static KMap<String, IrisBiomeGeneratorLink> linkIndex(IrisBiome biome) {
AtomicCache<KMap<String, IrisBiomeGeneratorLink>> cache = biome.getGenCache();
KMap<String, IrisBiomeGeneratorLink> cached = cache.getIfPresent();
@@ -225,7 +225,7 @@ final class IrisBiomeLayerGenerator {
int sampledDepth = Math.min(maxDepth, cycleLength);
for (int depth = 0; depth < sampledDepth; depth++) {
int position = Math.floorMod(512L - height - depth, cycleLength);
int position = Math.floorMod(512L - height + depth, cycleLength);
int layerIndex = findLayer(boundaries, position);
IrisBiomePaletteLayer layer = layers.get(layerIndex);
int offset = position - boundaries[layerIndex];
@@ -14,27 +14,31 @@ import java.util.function.Consumer;
public final class ConcurrentClockCache<V> {
private static final int WAYS = 4;
private final AtomicReferenceArray<Entry<V>> slots;
private final AtomicReferenceArray<CacheSet<V>> slots;
private final long sets;
private final int capacity;
public ConcurrentClockCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Cache capacity must be positive");
}
int setCount = Math.ceilDiv(capacity, WAYS);
this.capacity = Math.multiplyExact(setCount, WAYS);
sets = setCount;
slots = new AtomicReferenceArray<>(Math.multiplyExact(setCount, WAYS));
slots = new AtomicReferenceArray<>(setCount);
}
public int capacity() {
return slots.length();
return capacity;
}
public V get(long key) {
long hash = CacheKey.mix(key);
int base = base(hash);
CacheSet<V> snapshot = slots.getAcquire(index(CacheKey.mix(key)));
if (snapshot == null) {
return null;
}
for (int way = 0; way < WAYS; way++) {
Entry<V> entry = slots.getAcquire(base + way);
Entry<V> entry = snapshot.get(way);
if (entry != null && entry.key == key) {
entry.touch();
return entry.value;
@@ -46,12 +50,23 @@ public final class ConcurrentClockCache<V> {
public V putIfAbsent(long key, V value) {
Objects.requireNonNull(value, "Cached value");
long hash = CacheKey.mix(key);
int base = base(hash);
int index = index(hash);
int hand = (int) (hash >>> 60) & (WAYS - 1);
Entry<V> created = null;
while (true) {
CacheSet<V> snapshot = slots.getAcquire(index);
if (snapshot == null) {
if (created == null) {
created = new Entry<>(key, value);
}
if (slots.compareAndSet(index, null, new CacheSet<>(created, null, null, null))) {
return value;
}
continue;
}
int empty = -1;
for (int way = 0; way < WAYS; way++) {
Entry<V> entry = slots.getAcquire(base + way);
Entry<V> entry = snapshot.get(way);
if (entry == null) {
if (empty < 0) {
empty = way;
@@ -61,30 +76,23 @@ public final class ConcurrentClockCache<V> {
return entry.value;
}
}
Entry<V> created = new Entry<>(key, value);
if (empty >= 0) {
if (slots.compareAndSet(base + empty, null, created)) {
return value;
}
continue;
}
int victim = -1;
Entry<V> victimEntry = null;
int victim = empty;
for (int step = 0; step < WAYS * 2 && victim < 0; step++) {
int way = (hand + step) & (WAYS - 1);
Entry<V> entry = slots.getAcquire(base + way);
if (entry == null || !entry.referenced) {
Entry<V> entry = snapshot.get(way);
if (!entry.referenced) {
victim = way;
victimEntry = entry;
} else {
entry.referenced = false;
}
}
if (victim < 0) {
victim = hand;
victimEntry = slots.getAcquire(base + victim);
}
if (slots.compareAndSet(base + victim, victimEntry, created)) {
if (created == null) {
created = new Entry<>(key, value);
}
if (slots.compareAndSet(index, snapshot, snapshot.with(victim, created))) {
return value;
}
}
@@ -92,9 +100,15 @@ public final class ConcurrentClockCache<V> {
public void forEach(Consumer<? super V> action) {
for (int index = 0; index < slots.length(); index++) {
Entry<V> entry = slots.getAcquire(index);
if (entry != null) {
action.accept(entry.value);
CacheSet<V> snapshot = slots.getAcquire(index);
if (snapshot == null) {
continue;
}
for (int way = 0; way < WAYS; way++) {
Entry<V> entry = snapshot.get(way);
if (entry != null) {
action.accept(entry.value);
}
}
}
}
@@ -105,14 +119,36 @@ public final class ConcurrentClockCache<V> {
}
}
private int base(long hash) {
return (int) (((hash & 0xffffffffL) * sets) >>> 32) * WAYS;
private int index(long hash) {
return (int) (((hash & 0xffffffffL) * sets) >>> 32);
}
private record CacheSet<V>(Entry<V> first, Entry<V> second, Entry<V> third, Entry<V> fourth) {
private Entry<V> get(int way) {
return switch (way) {
case 0 -> first;
case 1 -> second;
case 2 -> third;
case 3 -> fourth;
default -> throw new IndexOutOfBoundsException(way);
};
}
private CacheSet<V> with(int way, Entry<V> entry) {
return switch (way) {
case 0 -> new CacheSet<>(entry, second, third, fourth);
case 1 -> new CacheSet<>(first, entry, third, fourth);
case 2 -> new CacheSet<>(first, second, entry, fourth);
case 3 -> new CacheSet<>(first, second, third, entry);
default -> throw new IndexOutOfBoundsException(way);
};
}
}
private static final class Entry<V> {
private final long key;
private final V value;
private boolean referenced;
private volatile boolean referenced;
private Entry(long key, V value) {
this.key = key;
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.cave;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
@@ -31,12 +33,15 @@ import java.util.List;
public final class IrisCaveProfileSampler {
private static final double FLOATING_THRESHOLD_BIAS_SCALE = 0.2D;
private final IrisData data;
private final IrisCaveProfile profile;
private final CNG baseDensity;
private final CNG detailDensity;
private final CNG warpDensity;
private final RNG thresholdRng;
private final CNG thresholdDensity;
private final double thresholdMin;
private final double thresholdMax;
private final double constantThreshold;
private final boolean thresholdHasNoise;
private final ModuleState[] modules;
private final double inverseNormalization;
private final double baseWeight;
@@ -47,14 +52,19 @@ public final class IrisCaveProfileSampler {
private final boolean hasModules;
public IrisCaveProfileSampler(Engine engine, IrisCaveProfile profile) {
this.data = engine.getData();
IrisData data = engine.getData();
this.profile = profile;
List<ModuleState> moduleStates = new ArrayList<>();
RNG baseRng = new RNG(engine.getSeedManager().getCarve());
this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data);
this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data);
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
this.thresholdRng = baseRng.nextParallelRNG(489_112);
IrisStyledRange threshold = profile.getDensityThreshold();
thresholdMin = threshold.getMin();
thresholdMax = threshold.getMax();
thresholdHasNoise = thresholdMin != thresholdMax && !threshold.getStyle().isFlat();
thresholdDensity = thresholdHasNoise ? threshold.getStyle().create(baseRng.nextParallelRNG(489_112), data) : null;
constantThreshold = thresholdMin == thresholdMax ? thresholdMin : M.lerp(thresholdMin, thresholdMax, 0.5D);
this.baseWeight = profile.getBaseWeight();
this.detailWeight = profile.getDetailWeight();
this.warpStrength = profile.getWarpStrength();
@@ -82,7 +92,8 @@ public final class IrisCaveProfileSampler {
return false;
}
double threshold = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias();
double threshold = (thresholdHasNoise ? thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)
: constantThreshold) - profile.getThresholdBias();
threshold += floatingThresholdBias(floatingCarveThreshold);
return sampleDensity(x, y, z) <= threshold;
}
@@ -98,14 +98,6 @@ final class DecoratorCore {
return picked;
}
private static String topHalfValue(String half) {
return half.equals("upper") || half.equals("lower") ? "upper" : "top";
}
private static String bottomHalfValue(String half) {
return half.equals("upper") || half.equals("lower") ? "lower" : "bottom";
}
static void placeSurfaceSingle(IrisDecorator decorator,
int x, int z, int realX, int height, int realZ,
Hunk<NativeBlockState> data, RNG rng, IrisData irisData,
@@ -150,7 +142,7 @@ final class DecoratorCore {
}
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
if ("upper".equals(half) || "lower".equals(half)) {
int lowerY = height + 1;
int upperY = height + 2;
if (!canPlaceTwoBlockPlant(data, x, z, lowerY, upperY, caveSkipFluid)) {
@@ -158,9 +150,9 @@ final class DecoratorCore {
}
try {
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
NativeBlockState upper = bd.withProperty("half", "upper");
NativeBlockState lower = fixFacesForHunk(
bd.withProperty("half", bottomHalfValue(half)),
bd.withProperty("half", "lower"),
data, x, z, realX, lowerY, realZ, mantle);
data.set(x, lowerY, z, lower);
data.set(x, upperY, z, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(x, upperY, z), true));
@@ -228,6 +220,8 @@ final class DecoratorCore {
int stack = computeStack(decorator, rng, realX, realZ, irisData, effectiveMax);
int placed = 0;
boolean hasSpikes = false;
NativeBlockState sampledBlock = null;
boolean sampledTop = false;
for (int i = 0; i < stack; i++) {
int y = height + 1 + i;
if (y >= data.getHeight()) {
@@ -239,9 +233,14 @@ final class DecoratorCore {
break;
}
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
boolean top = threshold >= decorator.getTopThreshold();
if (sampledBlock == null || top != sampledTop) {
sampledBlock = top
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
sampledTop = top;
}
NativeBlockState block = sampledBlock;
if (block == null) {
break;
}
@@ -282,6 +281,8 @@ final class DecoratorCore {
int stack = computeStack(decorator, rng, realX, realZ, irisData, max);
int placed = 0;
boolean hasSpikes = false;
NativeBlockState sampledBlock = null;
boolean sampledTop = false;
for (int i = 0; i < stack; i++) {
int y = height - i;
if (y < 0 || y < minHeight) {
@@ -293,9 +294,14 @@ final class DecoratorCore {
break;
}
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
boolean top = threshold >= decorator.getTopThreshold();
if (sampledBlock == null || top != sampledTop) {
sampledBlock = top
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
sampledTop = top;
}
NativeBlockState block = sampledBlock;
if (block == null) {
break;
}
@@ -346,7 +352,7 @@ final class DecoratorCore {
}
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
if ("upper".equals(half) || "lower".equals(half)) {
int lowerY = height + 1;
int upperY = height + 2;
if (max <= 2 || !canPlaceTwoBlockPlant(data, xf, zf, lowerY, upperY, false)) {
@@ -354,8 +360,8 @@ final class DecoratorCore {
}
try {
NativeBlockState upper = bd.withProperty("half", topHalfValue(half));
NativeBlockState lower = bd.withProperty("half", bottomHalfValue(half));
NativeBlockState upper = bd.withProperty("half", "upper");
NativeBlockState lower = bd.withProperty("half", "lower");
data.set(xf, lowerY, zf, IrisProceduralBlocks.normalizeWaterlogging(lower, data.get(xf, lowerY, zf), true));
data.set(xf, upperY, zf, IrisProceduralBlocks.normalizeWaterlogging(upper, data.get(xf, upperY, zf), true));
} catch (Throwable e) {
@@ -386,15 +392,22 @@ final class DecoratorCore {
int placed = 0;
boolean hasSpikes = false;
NativeBlockState sampledBlock = null;
boolean sampledTop = false;
for (int i = 0; i < stack; i++) {
int h = height + 1 + i;
if (h >= height + max || h >= data.getHeight()) {
break;
}
double threshold = stack == 1 ? 0.0 : ((double) i) / (stack - 1);
NativeBlockState bd = threshold >= decorator.getTopThreshold()
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
boolean top = threshold >= decorator.getTopThreshold();
if (sampledBlock == null || top != sampledTop) {
sampledBlock = top
? decorator.pickBlockDataTop(rng, irisData, realX, realZ)
: decorator.pickBlockData(rng, irisData, realX, realZ);
sampledTop = top;
}
NativeBlockState bd = sampledBlock;
if (bd == null) {
break;
}
@@ -193,20 +193,22 @@ public class IrisDecorator {
}
public NativeBlockState getBlockData100(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) {
if (getBlockData(data).isEmpty()) {
KList<NativeBlockState> palette = getBlockData(data);
if (palette.isEmpty()) {
IrisLogging.warnOnce("decorator-empty:" + b.getName(), "Empty Block Data for " + b.getName());
return null;
}
if (getBlockData(data).size() == 1) {
return getBlockData(data).get(0);
if (palette.size() == 1) {
return palette.get(0);
}
return getVarianceGenerator(rng, data).fit(getBlockData(data), z, y, x); //X and Z must be switched
return getVarianceGenerator(rng, data).fit(palette, z, y, x); //X and Z must be switched
}
public NativeBlockState getBlockDataForTop(IrisBiome b, RNG rng, double x, double y, double z, IrisData data) {
if (getBlockDataTops(data).isEmpty()) {
KList<NativeBlockState> topPalette = getBlockDataTops(data);
if (topPalette.isEmpty()) {
return getBlockData100(b, rng, x, y, z, data);
}
@@ -214,11 +216,11 @@ public class IrisDecorator {
double zz = z / style.getZoom();
if (getGenerator(rng, data).fitDouble(0D, 1D, xx, zz) <= chance) { //Exclude y from here
if (getBlockData(data).size() == 1) {
return getBlockDataTops(data).get(0);
if (topPalette.size() == 1) {
return topPalette.get(0);
}
return getVarianceGenerator(rng, data).fit(getBlockDataTops(data), z, y, x); //X and Z must be switched
return getVarianceGenerator(rng, data).fit(topPalette, z, y, x); //X and Z must be switched
}
return null;
@@ -146,13 +146,17 @@ public final class IrisProceduralBlocks {
}
public static NativeBlockState normalizeWaterlogging(NativeBlockState state, NativeBlockState existing, boolean enabled) {
if (state == null || !hasProperty(state, "waterlogged")) {
if (state == null) {
return null;
}
String current = propertyValue(state, "waterlogged");
if (current == null) {
return state;
}
boolean authored = "true".equals(propertyValue(state, "waterlogged"));
boolean authored = "true".equals(current);
boolean submerged = existing != null && (existing.isWater() || "true".equals(propertyValue(existing, "waterlogged")));
String value = submerged && (enabled || authored) ? "true" : "false";
return value.equals(propertyValue(state, "waterlogged")) ? state : state.withProperty("waterlogged", value);
return value.equals(current) ? state : state.withProperty("waterlogged", value);
}
public static IrisObject assemble(Map<Vector3i, NativeBlockState> blocks) {
@@ -111,12 +111,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
int stack = decorator.getHeight(partRNG, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
stack = Math.min(max - height, stack);
}
int stack = DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height);
if (stack == 1) {
int targetY = height + 1;
@@ -5,7 +5,6 @@ import art.arcane.iris.generation.hydrology.cave.CavePositionIndex;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveFluidPolicy;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveGrottoShape;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
@@ -170,11 +169,8 @@ final class CaveCandidateBuilder {
volume,
1,
1,
1,
1,
dryHeadroom,
HydrologyCaveFluidPolicy.REJECT_EXISTING,
HydrologyCaveGrottoShape.ELLIPSOID,
horizontalRadius,
maximumDepth
);
@@ -114,7 +114,7 @@ final class HydrologyOutletPlanner {
return selectedOcean;
}
if (surface) {
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
if (budgets.overridden()) {
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
}
@@ -243,7 +243,7 @@ final class HydrologyOutletPlanner {
}
sortSurfaceFallbackOutletCandidates(grid, oceanCandidates);
sortSurfaceFallbackOutletCandidates(grid, inlandCandidates);
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
if (budgets.overridden()) {
HydrologyTileKey key = HydrologyTileKey.fromBlock(grid.ownerMinimumX(), grid.ownerMinimumZ(), grid.ownerSize());
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
@@ -279,7 +279,7 @@ final class HydrologyOutletPlanner {
List<OutletCandidate> limitSurfaceOutlets(HydrologyTileKey key, HydrologySampledGrid grid,
List<OutletCandidate> candidates, boolean coastal) {
HydrologySurfaceBudgets budgets = HydrologySurfaceBudgets.sample(grid, planner.settings.surface().sources());
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
if (!budgets.overridden()) {
return coastal ? limitOutletsByType(candidates, planner.settings.outlets().maximumCoastalPerTile())
: limitOutlets(candidates, planner.settings.outlets().maximumPerTile());
@@ -12,6 +12,7 @@ final class HydrologySampledGrid {
private final int spacing;
private final List<HydrologyGridNode> nodes;
private volatile HydrologyDrainageGraph drainage;
private volatile SurfaceBudgetSnapshot surfaceBudgets;
HydrologySampledGrid(int minimumX, int minimumZ, int ownerMinimumX, int ownerMinimumZ,
int ownerSize, int width, int spacing, List<HydrologyGridNode> nodes) {
@@ -77,6 +78,21 @@ final class HydrologySampledGrid {
return nodes.get(index);
}
HydrologySurfaceBudgets surfaceBudgets(HydrologyPlannerSettings.Source sources) {
SurfaceBudgetSnapshot current = surfaceBudgets;
if (current != null && current.sources().equals(sources)) {
return current.budgets();
}
synchronized (this) {
current = surfaceBudgets;
if (current == null || !current.sources().equals(sources)) {
current = new SurfaceBudgetSnapshot(sources, HydrologySurfaceBudgets.sample(this, sources));
surfaceBudgets = current;
}
return current.budgets();
}
}
HydrologyGridNode nodeAt(int gridX, int gridZ) {
if (gridX < 0 || gridZ < 0 || gridX >= width || gridZ >= width) {
return null;
@@ -102,4 +118,7 @@ final class HydrologySampledGrid {
return x >= ownerMinimumX && x < ownerMinimumX + ownerSize
&& z >= ownerMinimumZ && z < ownerMinimumZ + ownerSize;
}
private record SurfaceBudgetSnapshot(HydrologyPlannerSettings.Source sources, HydrologySurfaceBudgets budgets) {
}
}
@@ -219,7 +219,7 @@ final class HydrologySourcePlanner {
return SourceSelection.empty(surface);
}
long sourceSalt = surface ? SURFACE_SOURCE_SALT : UNDERGROUND_SOURCE_SALT;
HydrologySurfaceBudgets surfaceBudgets = surface ? HydrologySurfaceBudgets.sample(grid, sourceSettings) : null;
HydrologySurfaceBudgets surfaceBudgets = surface ? grid.surfaceBudgets(sourceSettings) : null;
ArrayList<SourceCandidate> candidates = new ArrayList<>();
boolean hasRequiredCandidate = false;
for (HydrologyGridNode node : grid.nodes()) {
@@ -1,18 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
record CaveBoundaryResult(Set<CavePosition> sealGuards, HydrologyCaveRejection rejection) {
static CaveBoundaryResult accepted(Set<CavePosition> sealGuards) {
return new CaveBoundaryResult(
Collections.unmodifiableSet(new LinkedHashSet<>(sealGuards)),
HydrologyCaveRejection.NONE
);
}
static CaveBoundaryResult rejected(HydrologyCaveRejection rejection) {
return new CaveBoundaryResult(Set.of(), rejection);
}
}
@@ -1,18 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
record CaveComponentResult(Set<CavePosition> positions, HydrologyCaveRejection rejection) {
static CaveComponentResult accepted(Set<CavePosition> positions) {
return new CaveComponentResult(
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
HydrologyCaveRejection.NONE
);
}
static CaveComponentResult rejected(HydrologyCaveRejection rejection) {
return new CaveComponentResult(Set.of(), rejection);
}
}
@@ -1,18 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
record CaveGrottoResult(Set<CavePosition> positions, HydrologyCaveRejection rejection) {
static CaveGrottoResult accepted(Set<CavePosition> positions) {
return new CaveGrottoResult(
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
HydrologyCaveRejection.NONE
);
}
static CaveGrottoResult rejected(HydrologyCaveRejection rejection) {
return new CaveGrottoResult(Set.of(), rejection);
}
}
@@ -1,13 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
import java.util.List;
record CavePathResult(List<CavePosition> positions, HydrologyCaveRejection rejection) {
static CavePathResult accepted(List<CavePosition> positions) {
return new CavePathResult(List.copyOf(positions), HydrologyCaveRejection.NONE);
}
static CavePathResult rejected(HydrologyCaveRejection rejection) {
return new CavePathResult(List.of(), rejection);
}
}
@@ -1,809 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
final class HydrologyCaveChamberPlanner {
private final HydrologyCaveContainmentPlanner planner;
HydrologyCaveChamberPlanner(HydrologyCaveContainmentPlanner planner) {
this.planner = planner;
}
HydrologyCavePlan planGrottoOrClosedComponent(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
if (planner.isFluidReachable(targetVoxel, settings)) {
return planClosedComponent(view, source, settings, throat);
}
if (targetVoxel == CaveVoxel.LAVA) {
return planner.rejected(source, HydrologyCaveRejection.LAVA_CONTACT);
}
if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) {
return planner.rejected(source, HydrologyCaveRejection.INCOMPATIBLE_FLUID);
}
return planGeneratedGrotto(view, source, settings, throat);
}
HydrologyCavePlan planWaterfallPool(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
if (!view.isOpenToSurface(source.target())) {
return planGrottoOrClosedComponent(view, source, settings, throat);
}
HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
if (dryThroatRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, dryThroatRejection);
}
HydrologyCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat);
if (shaftRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, shaftRejection);
}
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
if (!planner.isFluidReachable(targetVoxel, settings)) {
return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings));
}
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
addThroatActions(actions, throat, source);
addSealGuards(view, source, actions);
return planner.accepted(view, source, actions);
}
HydrologyCavePlan planClosedComponent(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat);
if (dryThroatRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, dryThroatRejection);
}
HydrologyCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat);
if (waterfallRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, waterfallRejection);
}
CaveVoxel targetVoxel = planner.voxelAt(view, source.target());
if (!planner.isFluidReachable(targetVoxel, settings)) {
return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings));
}
CaveComponentResult component = resolveClosedComponent(view, source, settings, throat);
if (component.rejection() != HydrologyCaveRejection.NONE) {
return planner.rejected(source, component.rejection());
}
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
addThroatActions(actions, throat, source);
for (CavePosition position : component.positions()) {
actions.put(position, HydrologyCaveAction.WET_SOURCE);
}
addSealGuards(view, source, actions);
return planner.accepted(view, source, actions);
}
HydrologyCaveRejection validateDryThroatContacts(
CaveVoxelView view,
HydrologyCaveSource source,
List<CavePosition> throat
) {
Set<CavePosition> throatPositions = Set.copyOf(throat);
for (CavePosition position : throat) {
if (position.y() <= source.waterHeadY()) {
continue;
}
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
CaveVoxel voxel = planner.voxelAt(view, neighbor);
if (voxel == CaveVoxel.LAVA) {
return HydrologyCaveRejection.LAVA_CONTACT;
}
if (voxel == CaveVoxel.COMPATIBLE_FLUID) {
return HydrologyCaveRejection.EXISTING_FLUID;
}
if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) {
return HydrologyCaveRejection.INCOMPATIBLE_FLUID;
}
}
}
return HydrologyCaveRejection.NONE;
}
HydrologyCaveRejection validateWaterfallShaft(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
if (source.mode() != HydrologyCaveMode.WATERFALL_POOL) {
return HydrologyCaveRejection.NONE;
}
Set<CavePosition> throatPositions = Set.copyOf(throat);
for (CavePosition position : throat) {
if (position.y() <= source.waterHeadY()) {
continue;
}
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
CaveVoxel voxel = planner.voxelAt(view, neighbor);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return hazard;
}
if (voxel != CaveVoxel.SOLID) {
return HydrologyCaveRejection.WATERFALL_SHAFT_OPEN;
}
}
}
return HydrologyCaveRejection.NONE;
}
HydrologyCavePlan planGeneratedGrotto(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
CaveGrottoResult grotto = buildGrotto(source, settings);
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
return planner.rejected(source, grotto.rejection());
}
Set<CavePosition> chamber = grotto.positions();
Set<CavePosition> carve = new LinkedHashSet<>(chamber.size() + throat.size());
carve.addAll(chamber);
carve.addAll(throat);
HydrologyCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve);
if (carveRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, carveRejection);
}
CaveBoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve);
if (boundary.rejection() != HydrologyCaveRejection.NONE) {
return planner.rejected(source, boundary.rejection());
}
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
addChamberActions(actions, chamber, source.waterHeadY());
addThroatActions(actions, throat, source);
for (CavePosition position : boundary.sealGuards()) {
actions.put(position, HydrologyCaveAction.SEAL_GUARD);
}
return planner.accepted(view, source, actions);
}
HydrologyCavePlan planDeepPool(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
CaveGrottoResult grotto = buildGrotto(source, settings);
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
return planner.rejected(source, grotto.rejection());
}
Set<CavePosition> chamber = grotto.positions();
Set<CavePosition> carve = new LinkedHashSet<>(chamber.size() + throat.size());
carve.addAll(chamber);
carve.addAll(throat);
HydrologyCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve);
if (carveRejection != HydrologyCaveRejection.NONE) {
return planner.rejected(source, carveRejection);
}
CaveBoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve);
if (boundary.rejection() != HydrologyCaveRejection.NONE) {
return planner.rejected(source, boundary.rejection());
}
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
addChamberActions(actions, chamber, source.waterHeadY());
addThroatActions(actions, throat, source);
for (CavePosition position : boundary.sealGuards()) {
actions.put(position, HydrologyCaveAction.SEAL_GUARD);
}
return planner.accepted(view, source, actions);
}
HydrologyCaveRejection validateDeepPoolCarve(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
Set<CavePosition> carve
) {
for (CavePosition position : carve) {
if (!view.isInWorld(position)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
CaveVoxel voxel = planner.voxelAt(view, position);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return hazard;
}
if (voxel == CaveVoxel.SOLID) {
continue;
}
if (position.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(position)) {
continue;
}
return HydrologyCaveRejection.GROTTO_INTERSECTION;
}
return HydrologyCaveRejection.NONE;
}
CaveBoundaryResult validateDeepPoolBoundary(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
Set<CavePosition> carve
) {
Set<CavePosition> guards = new LinkedHashSet<>();
for (CavePosition position : carve) {
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (carve.contains(neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY);
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return CaveBoundaryResult.rejected(boundsRejection);
}
CaveVoxel voxel = planner.voxelAt(view, neighbor);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return CaveBoundaryResult.rejected(hazard);
}
if (voxel == CaveVoxel.SOLID) {
guards.add(neighbor);
continue;
}
if (neighbor.y() > source.waterHeadY()
&& voxel == CaveVoxel.CAVE_AIR
&& !view.isOpenToSurface(neighbor)) {
continue;
}
return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN);
}
}
return CaveBoundaryResult.accepted(guards);
}
CaveComponentResult resolveClosedComponent(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat
) {
Queue<CavePosition> queue = new ArrayDeque<>();
Set<CavePosition> queued = new LinkedHashSet<>();
int visitedCount = 0;
queue.add(source.target());
queued.add(source.target());
HydrologyCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued);
if (seedRejection != HydrologyCaveRejection.NONE) {
return CaveComponentResult.rejected(seedRejection);
}
while (!queue.isEmpty()) {
CavePosition position = queue.remove();
HydrologyCaveRejection positionRejection = validateReachablePosition(view, source, settings, position);
if (positionRejection != HydrologyCaveRejection.NONE) {
return CaveComponentResult.rejected(positionRejection);
}
if (++visitedCount > settings.maxFloodVolume()) {
return CaveComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
HydrologyCaveRejection neighborRejection = inspectReachableNeighbor(
view,
source,
settings,
neighbor,
queue,
queued
);
if (neighborRejection != HydrologyCaveRejection.NONE) {
return CaveComponentResult.rejected(neighborRejection);
}
}
}
return CaveComponentResult.accepted(queued);
}
HydrologyCaveRejection addThroatContacts(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> throat,
Queue<CavePosition> queue,
Set<CavePosition> queued
) {
for (CavePosition position : throat) {
if (position.y() > source.waterHeadY()) {
continue;
}
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
HydrologyCaveRejection rejection = inspectReachableNeighbor(
view,
source,
settings,
neighbor,
queue,
queued
);
if (rejection != HydrologyCaveRejection.NONE) {
return rejection;
}
}
}
return HydrologyCaveRejection.NONE;
}
HydrologyCaveRejection inspectReachableNeighbor(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
CavePosition position,
Queue<CavePosition> queue,
Set<CavePosition> queued
) {
if (position.y() > source.waterHeadY()) {
return inspectAboveHeadNeighbor(view, source, position);
}
if (!view.isInWorld(position)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
CaveVoxel voxel = planner.voxelAt(view, position);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return hazard;
}
if (!planner.isFluidReachable(voxel, settings)) {
return HydrologyCaveRejection.NONE;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
if (queued.add(position)) {
queue.add(position);
}
return HydrologyCaveRejection.NONE;
}
HydrologyCaveRejection inspectAboveHeadNeighbor(
CaveVoxelView view,
HydrologyCaveSource source,
CavePosition position
) {
if (isInletOpening(source, position) || !view.isInWorld(position)) {
return HydrologyCaveRejection.NONE;
}
CaveVoxel voxel = planner.voxelAt(view, position);
return switch (voxel) {
case LAVA -> HydrologyCaveRejection.LAVA_CONTACT;
case COMPATIBLE_FLUID -> HydrologyCaveRejection.EXISTING_FLUID;
case INCOMPATIBLE_FLUID -> HydrologyCaveRejection.INCOMPATIBLE_FLUID;
default -> HydrologyCaveRejection.NONE;
};
}
HydrologyCaveRejection validateReachablePosition(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
CavePosition position
) {
if (!view.isInWorld(position)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
if (view.isOpenToSurface(position)) {
return HydrologyCaveRejection.OPEN_SURFACE;
}
CaveVoxel voxel = planner.voxelAt(view, position);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return hazard;
}
return planner.isFluidReachable(voxel, settings)
? HydrologyCaveRejection.NONE
: HydrologyCaveRejection.NO_CAVE_TARGET;
}
CavePathResult buildThroat(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings
) {
CavePosition entry = source.entry();
CavePosition target = source.target();
int deltaX = target.x() - entry.x();
int deltaY = target.y() - entry.y();
int deltaZ = target.z() - entry.z();
int movesX = Math.abs(deltaX);
int movesY = Math.abs(deltaY);
int movesZ = Math.abs(deltaZ);
int length = movesX + movesY + movesZ;
if (length > settings.maxThroatLength()) {
return CavePathResult.rejected(HydrologyCaveRejection.THROAT_LIMIT);
}
int stepX = Integer.signum(deltaX);
int stepY = Integer.signum(deltaY);
int stepZ = Integer.signum(deltaZ);
int usedX = 0;
int usedY = 0;
int usedZ = 0;
CavePosition current = entry;
List<CavePosition> positions = new ArrayList<>(length + 1);
while (true) {
HydrologyCaveRejection positionRejection = validateThroatPosition(view, source, settings, current);
if (positionRejection != HydrologyCaveRejection.NONE) {
return CavePathResult.rejected(positionRejection);
}
positions.add(current);
if (current.equals(target)) {
return expandThroat(view, source, settings, positions);
}
int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ);
if (axis == 0) {
current = current.offset(stepX, 0, 0);
usedX++;
} else if (axis == 1) {
current = current.offset(0, stepY, 0);
usedY++;
} else {
current = current.offset(0, 0, stepZ);
usedZ++;
}
}
}
CavePathResult expandThroat(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
List<CavePosition> centerline
) {
int radius = settings.throatRadius();
int extent = radius - 1;
int radiusSquared = radius * radius;
Set<CavePosition> expanded = new LinkedHashSet<>();
for (CavePosition center : centerline) {
for (int dx = -extent; dx <= extent; dx++) {
for (int dy = -extent; dy <= extent; dy++) {
for (int dz = -extent; dz <= extent; dz++) {
if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) {
continue;
}
CavePosition position = center.offset(dx, dy, dz);
if (position.y() > source.entry().y()) {
continue;
}
HydrologyCaveRejection rejection = validateThroatPosition(view, source, settings, position);
if (rejection != HydrologyCaveRejection.NONE) {
return CavePathResult.rejected(rejection);
}
expanded.add(position);
if (expanded.size() > settings.maxFloodVolume()) {
return CavePathResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
}
}
}
}
return CavePathResult.accepted(List.copyOf(expanded));
}
int selectNextAxis(
long sourceId,
int movesX,
int movesY,
int movesZ,
int usedX,
int usedY,
int usedZ
) {
double scoreX = nextAxisScore(movesX, usedX);
double scoreY = nextAxisScore(movesY, usedY);
double scoreZ = nextAxisScore(movesZ, usedZ);
double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ));
int tieOffset = Math.floorMod(sourceId, 3);
for (int offset = 0; offset < 3; offset++) {
int axis = (tieOffset + offset) % 3;
double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ;
if (score == minimum) {
return axis;
}
}
throw new IllegalStateException("No remaining throat axis");
}
double nextAxisScore(int moves, int used) {
if (used >= moves) {
return Double.POSITIVE_INFINITY;
}
return ((2D * used) + 1D) / moves;
}
HydrologyCaveRejection validateThroatPosition(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
CavePosition position
) {
if (!view.isInWorld(position)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
return planner.rejectionForHazard(planner.voxelAt(view, position), settings);
}
CaveGrottoResult buildGrotto(HydrologyCaveSource source, HydrologyCavePlannerSettings settings) {
int horizontalRadius = settings.grottoHorizontalRadius();
int verticalRadius = settings.grottoVerticalRadius();
Set<CavePosition> candidates = new LinkedHashSet<>();
for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) {
for (int dy = -verticalRadius; dy <= verticalRadius; dy++) {
for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) {
if (settings.grottoShape().contains(source, settings, dx, dy, dz)) {
candidates.add(source.target().offset(dx, dy, dz));
if (candidates.size() > settings.maxFloodVolume()) {
return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
}
}
}
}
candidates.add(source.target());
for (int offset = 1; offset <= settings.dryHeadroom(); offset++) {
CavePosition headroom = new CavePosition(
source.target().x(), source.waterHeadY() + offset, source.target().z());
if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) {
return CaveGrottoResult.rejected(HydrologyCaveRejection.DRY_HEADROOM_LIMIT);
}
candidates.add(headroom);
if (candidates.size() > settings.maxFloodVolume()) {
return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
}
return CaveGrottoResult.accepted(connectedGrotto(source.target(), candidates));
}
Set<CavePosition> connectedGrotto(CavePosition target, Set<CavePosition> candidates) {
Queue<CavePosition> queue = new ArrayDeque<>();
Set<CavePosition> connected = new LinkedHashSet<>();
queue.add(target);
connected.add(target);
while (!queue.isEmpty()) {
CavePosition position = queue.remove();
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (candidates.contains(neighbor) && connected.add(neighbor)) {
queue.add(neighbor);
}
}
}
return connected;
}
HydrologyCaveRejection validateGeneratedCarve(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
Set<CavePosition> carve
) {
for (CavePosition position : carve) {
if (!view.isInWorld(position)) {
return HydrologyCaveRejection.WORLD_BOUNDARY;
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return boundsRejection;
}
CaveVoxel voxel = planner.voxelAt(view, position);
if (isGeneratedInletCarve(view, source, settings, position, voxel)) {
continue;
}
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return hazard;
}
if (voxel != CaveVoxel.SOLID) {
return HydrologyCaveRejection.GROTTO_INTERSECTION;
}
}
return HydrologyCaveRejection.NONE;
}
CaveBoundaryResult validateGeneratedBoundary(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
Set<CavePosition> carve
) {
Set<CavePosition> guards = new LinkedHashSet<>();
for (CavePosition position : carve) {
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (carve.contains(neighbor)) {
continue;
}
if (isInletOpening(source, neighbor)
|| isGeneratedInletOpening(view, source, settings, neighbor)) {
continue;
}
if (!view.isInWorld(neighbor)) {
return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY);
}
HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor);
if (boundsRejection != HydrologyCaveRejection.NONE) {
return CaveBoundaryResult.rejected(boundsRejection);
}
CaveVoxel voxel = planner.voxelAt(view, neighbor);
HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings);
if (hazard != HydrologyCaveRejection.NONE) {
return CaveBoundaryResult.rejected(hazard);
}
if (voxel != CaveVoxel.SOLID) {
return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN);
}
guards.add(neighbor);
}
}
return CaveBoundaryResult.accepted(guards);
}
void addChamberActions(
Map<CavePosition, HydrologyCaveAction> actions,
Collection<CavePosition> positions,
int waterHeadY
) {
for (CavePosition position : positions) {
HydrologyCaveAction action = position.y() <= waterHeadY
? HydrologyCaveAction.WET_SOURCE
: HydrologyCaveAction.DRY_AIR;
actions.put(position, action);
}
}
void addThroatActions(
Map<CavePosition, HydrologyCaveAction> actions,
Collection<CavePosition> throat,
HydrologyCaveSource source
) {
for (CavePosition position : throat) {
HydrologyCaveAction action;
if (position.y() <= source.waterHeadY()) {
action = HydrologyCaveAction.WET_SOURCE;
} else if (source.mode() == HydrologyCaveMode.WATERFALL_POOL
|| source.mode() == HydrologyCaveMode.GENERATED_GROTTO) {
action = HydrologyCaveAction.FALLING_FLUID;
} else {
action = HydrologyCaveAction.DRY_AIR;
}
actions.put(position, action);
}
}
void addSealGuards(
CaveVoxelView view,
HydrologyCaveSource source,
Map<CavePosition, HydrologyCaveAction> actions
) {
Set<CavePosition> guards = new LinkedHashSet<>();
for (CavePosition position : List.copyOf(actions.keySet())) {
for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) {
CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z());
if (actions.containsKey(neighbor)
|| isInletOpening(source, neighbor)
|| !view.isInWorld(neighbor)) {
continue;
}
if (planner.voxelAt(view, neighbor) == CaveVoxel.SOLID) {
guards.add(neighbor);
}
}
}
for (CavePosition guard : guards) {
actions.put(guard, HydrologyCaveAction.SEAL_GUARD);
}
}
boolean isInletOpening(HydrologyCaveSource source, CavePosition position) {
return position.equals(source.entry().offset(0, 1, 0));
}
boolean isGeneratedInletCarve(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
CavePosition position,
CaveVoxel voxel
) {
if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) {
return false;
}
int extent = Math.max(0, settings.throatRadius() - 1);
long deltaX = (long) position.x() - source.entry().x();
long deltaZ = (long) position.z() - source.entry().z();
return position.y() >= source.entry().y() - extent
&& position.y() <= source.entry().y()
&& deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent
&& view.isOpenToSurface(position);
}
boolean isGeneratedInletOpening(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
CavePosition position
) {
int radius = Math.max(1, settings.throatRadius());
long deltaX = (long) position.x() - source.entry().x();
long deltaZ = (long) position.z() - source.entry().z();
return position.y() >= source.entry().y()
&& position.y() <= source.entry().y() + 1
&& deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius
&& view.isOpenToSurface(position);
}
}
@@ -29,91 +29,6 @@ public final class HydrologyCaveContainmentPlanner {
static final Comparator<HydrologyCaveSource> SOURCE_PRIORITY =
HydrologyCaveConflictPolicy.sourcePriority();
final HydrologyCaveChamberPlanner chambers;
public HydrologyCaveContainmentPlanner() {
this.chambers = new HydrologyCaveChamberPlanner(this);
}
public HydrologyCavePlan plan(
CaveVoxelView view,
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings
) {
Objects.requireNonNull(view);
Objects.requireNonNull(source);
Objects.requireNonNull(settings);
HydrologyCaveRejection sourceRejection = validateSource(source);
if (sourceRejection != HydrologyCaveRejection.NONE) {
return rejected(source, sourceRejection);
}
CavePathResult throat = chambers.buildThroat(view, source, settings);
if (throat.rejection() != HydrologyCaveRejection.NONE) {
return rejected(source, throat.rejection());
}
return switch (source.mode()) {
case CLOSED_COMPONENT -> chambers.planClosedComponent(view, source, settings, throat.positions());
case GENERATED_GROTTO -> chambers.planGeneratedGrotto(view, source, settings, throat.positions());
case GROTTO_OR_CLOSED_COMPONENT -> chambers.planGrottoOrClosedComponent(
view,
source,
settings,
throat.positions()
);
case WATERFALL_POOL -> chambers.planWaterfallPool(
view,
source,
settings,
throat.positions()
);
case DEEP_POOL -> chambers.planDeepPool(view, source, settings, throat.positions());
};
}
public HydrologyCavePlanningResult planAll(
CaveVoxelView view,
Collection<HydrologyCaveSource> sources,
HydrologyCavePlannerSettings settings
) {
Objects.requireNonNull(view);
Objects.requireNonNull(sources);
Objects.requireNonNull(settings);
List<HydrologyCaveSource> orderedSources = new ArrayList<>(sources);
orderedSources.sort(SOURCE_PRIORITY);
List<HydrologyCavePlan> plans = new ArrayList<>(orderedSources.size());
Map<CavePosition, HydrologyCaveAction> combinedActions = new LinkedHashMap<>();
Map<CavePosition, HydrologyCaveSource> claimedBy = new HashMap<>();
Map<CavePosition, CaveVoxelPrecondition> combinedPreconditions = new LinkedHashMap<>();
for (HydrologyCaveSource source : orderedSources) {
HydrologyCavePlan candidate = plan(view, source, settings);
if (!candidate.accepted()) {
plans.add(candidate);
continue;
}
OptionalLong winnerSourceId = findWinningSourceId(
candidate.actions().keySet(),
claimedBy
);
if (winnerSourceId.isPresent()) {
plans.add(rejectedOverlap(source, winnerSourceId.getAsLong()));
} else {
plans.add(candidate);
combinedActions.putAll(candidate.actions());
combinedPreconditions.putAll(candidate.baselinePreconditions());
}
for (CavePosition position : candidate.actions().keySet()) {
claimedBy.putIfAbsent(position, source);
}
}
return new HydrologyCavePlanningResult(plans, combinedActions, combinedPreconditions);
}
public HydrologyCavePlanningResult validateAll(
CaveVoxelView view,
Collection<HydrologyCaveCandidate> candidates
@@ -698,14 +613,6 @@ public final class HydrologyCaveContainmentPlanner {
return HydrologyCaveRejection.NONE;
}
HydrologyCaveRejection rejectionForTarget(
CaveVoxel voxel,
HydrologyCavePlannerSettings settings
) {
HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings);
return hazard == HydrologyCaveRejection.NONE ? HydrologyCaveRejection.NO_CAVE_TARGET : hazard;
}
HydrologyCaveRejection rejectionForHazard(
CaveVoxel voxel,
HydrologyCavePlannerSettings settings
@@ -722,50 +629,10 @@ public final class HydrologyCaveContainmentPlanner {
};
}
boolean isFluidReachable(CaveVoxel voxel, HydrologyCavePlannerSettings settings) {
return voxel == CaveVoxel.CAVE_AIR
|| (voxel == CaveVoxel.COMPATIBLE_FLUID
&& settings.existingFluidPolicy() != HydrologyCaveFluidPolicy.REJECT_EXISTING)
|| (voxel == CaveVoxel.INCOMPATIBLE_FLUID
&& settings.existingFluidPolicy() == HydrologyCaveFluidPolicy.REPLACE_CONTAINED);
}
CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) {
return Objects.requireNonNull(view.voxelAt(position));
}
OptionalLong findWinningSourceId(
Set<CavePosition> positions,
Map<CavePosition, HydrologyCaveSource> claimedBy
) {
HydrologyCaveSource winner = null;
for (CavePosition position : positions) {
HydrologyCaveSource contender = claimedBy.get(position);
if (contender == null) {
continue;
}
if (winner == null || SOURCE_PRIORITY.compare(contender, winner) < 0) {
winner = contender;
}
}
return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId());
}
HydrologyCavePlan accepted(
CaveVoxelView view,
HydrologyCaveSource source,
Map<CavePosition, HydrologyCaveAction> actions
) {
Map<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>(actions.size());
for (CavePosition position : actions.keySet()) {
preconditions.put(
position,
new CaveVoxelPrecondition(voxelAt(view, position), view.isOpenToSurface(position))
);
}
return accepted(source, actions, preconditions);
}
HydrologyCavePlan accepted(
HydrologyCaveSource source,
Map<CavePosition, HydrologyCaveAction> actions,
@@ -1,21 +0,0 @@
package art.arcane.iris.generation.hydrology.cave;
@FunctionalInterface
public interface HydrologyCaveGrottoShape {
HydrologyCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> {
double horizontalRadius = settings.grottoHorizontalRadius();
double verticalRadius = settings.grottoVerticalRadius();
double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius))
+ ((double) dy * dy / (verticalRadius * verticalRadius))
+ ((double) dz * dz / (horizontalRadius * horizontalRadius));
return normalized <= 1D;
};
boolean contains(
HydrologyCaveSource source,
HydrologyCavePlannerSettings settings,
int offsetX,
int offsetY,
int offsetZ
);
}
@@ -6,72 +6,17 @@ public record HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
int maxThroatLength,
int throatRadius,
int grottoHorizontalRadius,
int grottoVerticalRadius,
int dryHeadroom,
HydrologyCaveFluidPolicy existingFluidPolicy,
HydrologyCaveGrottoShape grottoShape,
int maxClosedComponentHorizontalRadius,
int maxClosedComponentDepth
) {
public static final int MAXIMUM_PLANNED_MUTATIONS = 262_144;
public HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
int maxThroatLength,
int throatRadius,
int grottoHorizontalRadius,
int grottoVerticalRadius,
int dryHeadroom,
HydrologyCaveFluidPolicy existingFluidPolicy,
HydrologyCaveGrottoShape grottoShape
) {
this(
maxHorizontalRadius,
maxDepth,
maxFloodVolume,
maxThroatLength,
throatRadius,
grottoHorizontalRadius,
grottoVerticalRadius,
dryHeadroom,
existingFluidPolicy,
grottoShape,
maxHorizontalRadius,
maxDepth
);
}
public HydrologyCavePlannerSettings(
int maxHorizontalRadius,
int maxDepth,
int maxFloodVolume,
int maxThroatLength,
int grottoHorizontalRadius,
int grottoVerticalRadius,
HydrologyCaveFluidPolicy existingFluidPolicy
) {
this(
maxHorizontalRadius,
maxDepth,
maxFloodVolume,
maxThroatLength,
1,
grottoHorizontalRadius,
grottoVerticalRadius,
0,
existingFluidPolicy,
HydrologyCaveGrottoShape.ELLIPSOID
);
}
public HydrologyCavePlannerSettings {
Objects.requireNonNull(existingFluidPolicy);
Objects.requireNonNull(grottoShape);
if (maxHorizontalRadius < 1) {
throw new IllegalArgumentException("maxHorizontalRadius must be positive");
}
@@ -81,12 +26,6 @@ public record HydrologyCavePlannerSettings(
if (maxFloodVolume < 1) {
throw new IllegalArgumentException("maxFloodVolume must be positive");
}
if (maxThroatLength < 1) {
throw new IllegalArgumentException("maxThroatLength must be positive");
}
if (throatRadius < 1) {
throw new IllegalArgumentException("throatRadius must be positive");
}
if (grottoHorizontalRadius < 1) {
throw new IllegalArgumentException("grottoHorizontalRadius must be positive");
}
@@ -376,7 +376,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
private double naturalHeight(CacheStripe stripe, long packed, int blockX, int blockZ) {
synchronized (stripe) {
double cached = stripe.naturalHeights.getAndMoveToLast(packed);
if (!Double.isNaN(cached) || stripe.naturalHeights.containsKey(packed)) {
if (!Double.isNaN(cached)) {
return cached;
}
TerrainBasis cachedBasis = stripe.bases.getAndMoveToLast(packed);
@@ -392,7 +392,7 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
}
synchronized (stripe) {
double existing = stripe.naturalHeights.getAndMoveToLast(packed);
if (!Double.isNaN(existing) || stripe.naturalHeights.containsKey(packed)) {
if (!Double.isNaN(existing)) {
return existing;
}
stripe.naturalHeights.putAndMoveToLast(packed, sampled);
@@ -9,7 +9,8 @@ import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
@@ -149,8 +150,9 @@ public final class ErosionFieldCompiler {
boolean[] basin = prepared.basin();
double[] bendOffsets = prepared.bendOffsets();
double[][] blendWidth = prepared.blendWidth();
Long2IntOpenHashMap nearest = geometry.nearest;
Long2DoubleOpenHashMap distance = geometry.distance;
Long2IntOpenHashMap indices = geometry.indices;
IntArrayList stations = geometry.stations;
DoubleArrayList distance = geometry.distance;
if (!geometry.prepared) {
for (int station = 0; station < count; station++) {
int radius = prepared.radii()[station];
@@ -172,14 +174,22 @@ public final class ErosionFieldCompiler {
continue;
}
long key = RiverFootprint.pack(cellX, cellZ);
int existing = nearest.get(key);
int index = indices.get(key);
int existing = index < 0 ? -1 : stations.getInt(index);
double existingDistance = index < 0 ? 0D : distance.getDouble(index);
// A cell on a station's cross-section row ties between the two segments meeting
// there; the later station owns it so the row uses the head its own ring bounded.
if (existing < 0
|| cellDistance < distance.get(key) - EPSILON
|| Math.abs(cellDistance - distance.get(key)) <= EPSILON && station > existing) {
nearest.put(key, station);
distance.put(key, cellDistance);
|| cellDistance < existingDistance - EPSILON
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
if (index < 0) {
indices.put(key, stations.size());
stations.add(station);
distance.add(cellDistance);
} else {
stations.set(index, station);
distance.set(index, cellDistance);
}
}
}
}
@@ -189,17 +199,18 @@ public final class ErosionFieldCompiler {
int oceanStart = oceanStart(centerline, valley, terminal);
int inletStart = prepared.inletStart();
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(wetOnly
? Math.min(nearest.size(), Math.max(16, count * 8)) : nearest.size());
? Math.min(indices.size(), Math.max(16, count * 8)) : indices.size());
LongArrayList wetKeys = new LongArrayList();
LongIterator candidates = nearest.keySet().iterator();
long[] coordinates = new long[Math.min(nearest.size(), HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE)];
LongIterator candidates = indices.keySet().iterator();
long[] coordinates = new long[Math.min(indices.size(), HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE)];
double[] outlines = new double[coordinates.length];
while (candidates.hasNext()) {
int pageSize = 0;
while (candidates.hasNext() && pageSize < coordinates.length) {
long key = candidates.nextLong();
int station = nearest.get(key);
double cellDistance = distance.get(key);
int geometryIndex = indices.get(key);
int station = stations.getInt(geometryIndex);
double cellDistance = distance.getDouble(geometryIndex);
int cellX = RiverFootprint.unpackX(key);
int cellZ = RiverFootprint.unpackZ(key);
int head = valley.head()[station];
@@ -231,8 +242,9 @@ public final class ErosionFieldCompiler {
HydrologyTerrainSample[] samples = sampler.sampleBatch(coordinates, pageSize);
for (int index = 0; index < pageSize; index++) {
long key = coordinates[index];
int station = nearest.get(key);
double cellDistance = distance.get(key);
int geometryIndex = indices.get(key);
int station = stations.getInt(geometryIndex);
double cellDistance = distance.getDouble(geometryIndex);
int cellX = RiverFootprint.unpackX(key);
int cellZ = RiverFootprint.unpackZ(key);
int head = valley.head()[station];
@@ -898,12 +910,13 @@ public final class ErosionFieldCompiler {
}
private static final class RasterGeometry {
private final Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
private final Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
private final Long2IntOpenHashMap indices = new Long2IntOpenHashMap();
private final IntArrayList stations = new IntArrayList();
private final DoubleArrayList distance = new DoubleArrayList();
private boolean prepared;
private RasterGeometry() {
nearest.defaultReturnValue(-1);
indices.defaultReturnValue(-1);
}
}
@@ -22,36 +22,27 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.WorldMaintenance;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.iris.world.task.J;
import lombok.Data;
import lombok.AccessLevel;
@@ -61,9 +52,7 @@ import lombok.Setter;
import lombok.ToString;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B;
import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.function.Supplier;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
@@ -71,7 +60,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Map;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static art.arcane.iris.generation.mantle.EngineMantle.AIR;
@@ -215,122 +203,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
static Set<IrisPosition> getBallooned(Set<IrisPosition> vset, double radius) {
Set<IrisPosition> returnset = new HashSet<>();
int ceilrad = (int) Math.ceil(radius);
double r2 = Math.pow(radius, 2);
for (IrisPosition v : vset) {
int tipx = v.getX();
int tipy = v.getY();
int tipz = v.getZ();
for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) {
for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) {
for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) {
if (hypot(loopx - tipx, loopy - tipy, loopz - tipz) <= r2) {
returnset.add(new IrisPosition(loopx, loopy, loopz));
}
}
}
}
}
return returnset;
}
static Set<IrisPosition> getHollowed(Set<IrisPosition> vset) {
PositionLookup lookup = PositionLookup.of(vset);
Set<IrisPosition> returnset = new KSet<>();
for (IrisPosition v : vset) {
int x = v.getX();
int y = v.getY();
int z = v.getZ();
if (!(lookup.contains(x + 1, y, z)
&& lookup.contains(x - 1, y, z)
&& lookup.contains(x, y + 1, z)
&& lookup.contains(x, y - 1, z)
&& lookup.contains(x, y, z + 1)
&& lookup.contains(x, y, z - 1))) {
returnset.add(v);
}
}
return returnset;
}
private static double hypot(double x, double y, double z) {
return Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2);
}
record PositionLookup(LongOpenHashSet keys, Set<IrisPosition> positions, int originX, int originY, int originZ) {
private static final int AXIS_BITS = 21;
private static final long AXIS_LIMIT = 1L << AXIS_BITS;
static PositionLookup of(Set<IrisPosition> positions) {
if (positions.isEmpty()) {
return new PositionLookup(new LongOpenHashSet(), positions, 0, 0, 0);
}
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (IrisPosition position : positions) {
minX = Math.min(minX, position.getX());
minY = Math.min(minY, position.getY());
minZ = Math.min(minZ, position.getZ());
maxX = Math.max(maxX, position.getX());
maxY = Math.max(maxY, position.getY());
maxZ = Math.max(maxZ, position.getZ());
}
if ((long) maxX - minX >= AXIS_LIMIT - 2L
|| (long) maxY - minY >= AXIS_LIMIT - 2L
|| (long) maxZ - minZ >= AXIS_LIMIT - 2L) {
return new PositionLookup(null, positions, 0, 0, 0);
}
LongOpenHashSet keys = new LongOpenHashSet(positions.size());
for (IrisPosition position : positions) {
keys.add(key(position.getX() - minX, position.getY() - minY, position.getZ() - minZ));
}
return new PositionLookup(keys, positions, minX, minY, minZ);
}
boolean contains(int x, int y, int z) {
if (keys == null) {
return positions.contains(new IrisPosition(x, y, z));
}
long dx = (long) x - originX;
long dy = (long) y - originY;
long dz = (long) z - originZ;
if ((dx | dy | dz) < 0L || dx >= AXIS_LIMIT || dy >= AXIS_LIMIT || dz >= AXIS_LIMIT) {
return false;
}
return keys.contains(key(dx, dy, dz));
}
private static long key(long x, long y, long z) {
return (x << (AXIS_BITS * 2)) | (y << AXIS_BITS) | z;
}
}
private static double lengthSq(double x, double y, double z) {
return (x * x) + (y * y) + (z * z);
}
private static double lengthSq(double x, double z) {
return (x * x) + (z * z);
}
public <T> void setDataWarped(int x, int y, int z, T t, RNG rng, IrisData data, IrisGeneratorStyle style) {
setData((int) Math.round(style.warp(rng, data, x, x, y, -z)),
(int) Math.round(style.warp(rng, data, y, z, -x, y)),
(int) Math.round(style.warp(rng, data, z, -y, z, x)), t);
}
public ObjectPlacementCapture captureObjectPlacement(ObjectContinuationBundle.PlacementKey key) {
if (objectPlacementCapture.get() != null) {
throw new IllegalStateException("Object placement capture is already active");
@@ -376,6 +248,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return;
}
if (t instanceof NativeBlockState) {
clearTile(matter, x, y, z);
clearDeferredPlacement(matter, x, y, z);
}
Class<?> sliceType = t instanceof NativeBlockState ? NativeBlockState.class : matter.getClass(t);
@@ -671,69 +544,21 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return carvedColumn;
}
public boolean restorePrerequisiteData(int x, int y, int z, Class<?> type) {
void restoreData(int x, int y, int z, Class<?> type, Object value) {
Objects.requireNonNull(type, "type");
if (!allowsWrite(x, z)
|| y < 0
|| y >= mantle.getWorldHeight()
|| !isPreObjectType(type)) {
return false;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
int section = y >> 4;
if (chunk == null) {
return false;
}
synchronized (chunk) {
if (!chunk.exists(section)) {
return false;
}
Matter matter = chunk.get(section);
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
if (cell == null || !cell.captures(type)) {
return false;
}
restoreRaw(matter, x, y, z, type, cell.original(type));
recordObjectValue(x, y, z, type, cell.original(type));
return true;
}
}
public boolean restorePrerequisiteCell(int x, int y, int z) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return false;
return;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
int section = y >> 4;
if (chunk == null) {
return false;
throw new IllegalStateException("Object rollback exceeded its prepared Mantle radius at "
+ x + "," + y + "," + z);
}
synchronized (chunk) {
if (!chunk.exists(section)) {
return false;
Matter matter = chunk.get(y >> 4);
if (matter != null) {
restoreRaw(matter, x, y, z, type, value);
}
Matter matter = chunk.get(section);
PreObjectMatterCell cell = preObjectCell(matter, x, y, z);
if (cell == null) {
return false;
}
if (cell.blockCaptured()) {
restoreRaw(matter, x, y, z, NativeBlockState.class, cell.block());
recordObjectValue(x, y, z, NativeBlockState.class, cell.block());
}
if (cell.stringCaptured()) {
restoreRaw(matter, x, y, z, String.class, cell.string());
recordObjectValue(x, y, z, String.class, cell.string());
}
if (cell.cavernCaptured()) {
restoreRaw(matter, x, y, z, MatterCavern.class, cell.cavern());
recordObjectValue(x, y, z, MatterCavern.class, cell.cavern());
}
return true;
}
}
@@ -778,6 +603,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return hydrology != null && hydrology.protectsPlacement();
}
private static void clearTile(Matter matter, int x, int y, int z) {
MatterSlice<TileWrapper> slice = matter.getSlice(TileWrapper.class);
if (slice != null) {
slice.set(x & 15, y & 15, z & 15, null);
}
}
private static void clearDeferredPlacement(Matter matter, int x, int y, int z) {
MatterSlice<Identifier> slice = matter.getSlice(Identifier.class);
if (slice != null) {
@@ -803,6 +635,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
if (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z)) {
return;
}
clearTile(matter, x, y, z);
MatterSlice<NativeBlockState> blockSlice = matter.slice(NativeBlockState.class);
MatterSlice<Identifier> identifierSlice = matter.slice(Identifier.class);
capturePreObjectOriginal(matter, x, y, z, NativeBlockState.class);
@@ -1102,479 +935,6 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return getEngineMantle().getEngine();
}
/**
* Set a sphere into the mantle
*
* @param cx the center x
* @param cy the center y
* @param cz the center z
* @param radius the radius of this sphere
* @param fill should it be filled? or just the outer shell?
* @param data the data to set
* @param <T> the type of data to apply to the mantle
*/
public <T> void setSphere(int cx, int cy, int cz, double radius, boolean fill, T data) {
setElipsoid(cx, cy, cz, radius, radius, radius, fill, data);
}
public <T> void setElipsoid(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data) {
setElipsoidFunction(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data);
}
public <T> void setElipsoidWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, T data, RNG rng, IrisData idata, IrisGeneratorStyle style) {
setElipsoidFunctionWarped(cx, cy, cz, rx, ry, rz, fill, (a, b, c) -> data, rng, idata, style);
}
/**
* Set an elipsoid into the mantle
*
* @param cx the center x
* @param cy the center y
* @param cz the center z
* @param rx the x radius
* @param ry the y radius
* @param rz the z radius
* @param fill should it be filled or just the outer shell?
* @param data the data to set
* @param <T> the type of data to apply to the mantle
*/
public <T> void setElipsoidFunction(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3<Integer, Integer, Integer, T> data) {
rx += 0.5;
ry += 0.5;
rz += 0.5;
final double invRadiusX = 1 / rx;
final double invRadiusY = 1 / ry;
final double invRadiusZ = 1 / rz;
final int ceilRadiusX = (int) Math.ceil(rx);
final int ceilRadiusY = (int) Math.ceil(ry);
final int ceilRadiusZ = (int) Math.ceil(rz);
double nextXn = 0;
forX:
for (int x = 0; x <= ceilRadiusX; ++x) {
final double xn = nextXn;
nextXn = (x + 1) * invRadiusX;
double nextYn = 0;
forY:
for (int y = 0; y <= ceilRadiusY; ++y) {
final double yn = nextYn;
nextYn = (y + 1) * invRadiusY;
double nextZn = 0;
for (int z = 0; z <= ceilRadiusZ; ++z) {
final double zn = nextZn;
nextZn = (z + 1) * invRadiusZ;
double distanceSq = lengthSq(xn, yn, zn);
if (distanceSq > 1) {
if (z == 0) {
if (y == 0) {
break forX;
}
break forY;
}
break;
}
if (!fill) {
if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) {
continue;
}
}
setData(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz));
setData(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz));
setData(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz));
setData(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz));
setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz));
setData(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz));
setData(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz));
setData(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz));
setData(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz));
}
}
}
}
public <T> void setElipsoidFunctionWarped(int cx, int cy, int cz, double rx, double ry, double rz, boolean fill, Function3<Integer, Integer, Integer, T> data, RNG rng, IrisData idata, IrisGeneratorStyle style) {
rx += 0.5;
ry += 0.5;
rz += 0.5;
final double invRadiusX = 1 / rx;
final double invRadiusY = 1 / ry;
final double invRadiusZ = 1 / rz;
final int ceilRadiusX = (int) Math.ceil(rx);
final int ceilRadiusY = (int) Math.ceil(ry);
final int ceilRadiusZ = (int) Math.ceil(rz);
double nextXn = 0;
forX:
for (int x = 0; x <= ceilRadiusX; ++x) {
final double xn = nextXn;
nextXn = (x + 1) * invRadiusX;
double nextYn = 0;
forY:
for (int y = 0; y <= ceilRadiusY; ++y) {
final double yn = nextYn;
nextYn = (y + 1) * invRadiusY;
double nextZn = 0;
for (int z = 0; z <= ceilRadiusZ; ++z) {
final double zn = nextZn;
nextZn = (z + 1) * invRadiusZ;
double distanceSq = lengthSq(xn, yn, zn);
if (distanceSq > 1) {
if (z == 0) {
if (y == 0) {
break forX;
}
break forY;
}
break;
}
if (!fill) {
if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) {
continue;
}
}
setDataWarped(x + cx, y + cy, z + cz, data.apply(x + cx, y + cy, z + cz), rng, idata, style);
setDataWarped(-x + cx, y + cy, z + cz, data.apply(-x + cx, y + cy, z + cz), rng, idata, style);
setDataWarped(x + cx, -y + cy, z + cz, data.apply(x + cx, -y + cy, z + cz), rng, idata, style);
setDataWarped(x + cx, y + cy, -z + cz, data.apply(x + cx, y + cy, -z + cz), rng, idata, style);
setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style);
setDataWarped(-x + cx, -y + cy, z + cz, data.apply(-x + cx, -y + cy, z + cz), rng, idata, style);
setDataWarped(x + cx, -y + cy, -z + cz, data.apply(x + cx, -y + cy, -z + cz), rng, idata, style);
setDataWarped(-x + cx, y + cy, -z + cz, data.apply(-x + cx, y + cy, -z + cz), rng, idata, style);
setDataWarped(-x + cx, -y + cy, -z + cz, data.apply(-x + cx, -y + cy, -z + cz), rng, idata, style);
}
}
}
}
/**
* Set a cuboid of data in the mantle
*
* @param x1 the min x
* @param y1 the min y
* @param z1 the min z
* @param x2 the max x
* @param y2 the max y
* @param z2 the max z
* @param data the data to set
* @param <T> the type of data to apply to the mantle
*/
public <T> void setCuboid(int x1, int y1, int z1, int x2, int y2, int z2, T data) {
int j, k;
for (int i = x1; i <= x2; i++) {
for (j = x1; j <= x2; j++) {
for (k = x1; k <= x2; k++) {
setData(i, j, k, data);
}
}
}
}
/**
* Set a pyramid of data in the mantle
*
* @param cx the center x
* @param cy the base y
* @param cz the center z
* @param data the data to set
* @param size the size of the pyramid (width of base & height)
* @param filled should it be filled or hollow
* @param <T> the type of data to apply to the mantle
*/
@SuppressWarnings("ConstantConditions")
public <T> void setPyramid(int cx, int cy, int cz, T data, int size, boolean filled) {
int height = size;
for (int y = 0; y <= height; ++y) {
size--;
for (int x = 0; x <= size; ++x) {
for (int z = 0; z <= size; ++z) {
if ((filled && z <= size && x <= size) || z == size || x == size) {
setData(x + cx, y + cy, z + cz, data);
setData(-x + cx, y + cy, z + cz, data);
setData(x + cx, y + cy, -z + cz, data);
setData(-x + cx, y + cy, -z + cz, data);
}
}
}
}
}
/**
* Set a 3d line
*
* @param a the first point
* @param b the second point
* @param radius the radius
* @param filled hollow or filled?
* @param data the data
* @param <T> the type of data to apply to the mantle
*/
public <T> void setLine(IrisPosition a, IrisPosition b, double radius, boolean filled, T data) {
setLine(ImmutableList.of(a, b), radius, filled, data);
}
public <T> void setLine(List<IrisPosition> vectors, double radius, boolean filled, T data) {
setLineConsumer(vectors, radius, filled, (_x, _y, _z) -> data);
}
/**
* Set lines for points
*
* @param vectors the points
* @param radius the radius
* @param filled hollow or filled?
* @param data the data to set
* @param <T> the type of data to apply to the mantle
*/
public <T> void setLineConsumer(List<IrisPosition> vectors, double radius, boolean filled, Function3<Integer, Integer, Integer, T> data) {
Set<IrisPosition> vset = cleanup(vectors);
vset = getBallooned(vset, radius);
if (!filled) {
vset = getHollowed(vset);
}
setConsumer(vset, data);
}
/**
* Set lines for points
*
* @param vectors the points
* @param radius the radius
* @param filled hollow or filled?
* @param data the data to set
* @param <T> the type of data to apply to the mantle
*/
public <T> void setNoiseMasked(List<IrisPosition> vectors, double radius, double threshold, CNG shape, Set<IrisPosition> masks, boolean filled, Function3<Integer, Integer, Integer, T> data) {
Set<IrisPosition> vset = cleanup(vectors);
vset = masks == null ? getBallooned(vset, radius) : getMasked(vset, masks, radius);
vset.removeIf(p -> shape.noise(p.getX(), p.getY(), p.getZ()) < threshold);
if (!filled) {
vset = getHollowed(vset);
}
setConsumer(vset, data);
}
static Set<IrisPosition> getMasked(Set<IrisPosition> vectors, Set<IrisPosition> masks, double radius) {
Set<IrisPosition> vset = new KSet<>();
int ceil = (int) Math.ceil(radius);
double r2 = Math.pow(radius, 2);
PositionLookup maskLookup = PositionLookup.of(masks);
for (IrisPosition v : vectors) {
int tipX = v.getX();
int tipY = v.getY();
int tipZ = v.getZ();
for (int x = -ceil; x <= ceil; x++) {
for (int y = -ceil; y <= ceil; y++) {
for (int z = -ceil; z <= ceil; z++) {
if (hypot(x, y, z) > r2 || !maskLookup.contains(x, y, z))
continue;
vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z));
}
}
}
}
return vset;
}
private static Set<IrisPosition> cleanup(List<IrisPosition> vectors) {
Set<IrisPosition> vset = new KSet<>();
for (int i = 0; vectors.size() != 0 && i < vectors.size() - 1; i++) {
IrisPosition pos1 = vectors.get(i);
IrisPosition pos2 = vectors.get(i + 1);
int x1 = pos1.getX();
int y1 = pos1.getY();
int z1 = pos1.getZ();
int x2 = pos2.getX();
int y2 = pos2.getY();
int z2 = pos2.getZ();
int tipx = x1;
int tipy = y1;
int tipz = z1;
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int dz = Math.abs(z2 - z1);
if (dx + dy + dz == 0) {
vset.add(new IrisPosition(tipx, tipy, tipz));
continue;
}
int dMax = Math.max(Math.max(dx, dy), dz);
if (dMax == dx) {
for (int domstep = 0; domstep <= dx; domstep++) {
tipx = x1 + domstep * (x2 - x1 > 0 ? 1 : -1);
tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dx) * (y2 - y1 > 0 ? 1 : -1));
tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dx) * (z2 - z1 > 0 ? 1 : -1));
vset.add(new IrisPosition(tipx, tipy, tipz));
}
} else if (dMax == dy) {
for (int domstep = 0; domstep <= dy; domstep++) {
tipy = y1 + domstep * (y2 - y1 > 0 ? 1 : -1);
tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dy) * (x2 - x1 > 0 ? 1 : -1));
tipz = (int) Math.round(z1 + domstep * ((double) dz) / ((double) dy) * (z2 - z1 > 0 ? 1 : -1));
vset.add(new IrisPosition(tipx, tipy, tipz));
}
} else /* if (dMax == dz) */ {
for (int domstep = 0; domstep <= dz; domstep++) {
tipz = z1 + domstep * (z2 - z1 > 0 ? 1 : -1);
tipy = (int) Math.round(y1 + domstep * ((double) dy) / ((double) dz) * (y2 - y1 > 0 ? 1 : -1));
tipx = (int) Math.round(x1 + domstep * ((double) dx) / ((double) dz) * (x2 - x1 > 0 ? 1 : -1));
vset.add(new IrisPosition(tipx, tipy, tipz));
}
}
}
return vset;
}
/**
* Set a cylinder in the mantle
*
* @param cx the center x
* @param cy the base y
* @param cz the center z
* @param data the data to set
* @param radius the radius
* @param height the height of the cyl
* @param filled filled or not
*/
public <T> void setCylinder(int cx, int cy, int cz, T data, double radius, int height, boolean filled) {
setCylinder(cx, cy, cz, data, radius, radius, height, filled);
}
/**
* Set a cylinder in the mantle
*
* @param cx the center x
* @param cy the base y
* @param cz the center z
* @param data the data to set
* @param radiusX the x radius
* @param radiusZ the z radius
* @param height the height of this cyl
* @param filled filled or hollow?
*/
public <T> void setCylinder(int cx, int cy, int cz, T data, double radiusX, double radiusZ, int height, boolean filled) {
int affected = 0;
radiusX += 0.5;
radiusZ += 0.5;
if (height == 0) {
return;
} else if (height < 0) {
height = -height;
cy = cy - height;
}
if (cy < 0) {
cy = 0;
} else if (cy + height - 1 > getMantle().getWorldHeight()) {
height = getMantle().getWorldHeight() - cy + 1;
}
final double invRadiusX = 1 / radiusX;
final double invRadiusZ = 1 / radiusZ;
final int ceilRadiusX = (int) Math.ceil(radiusX);
final int ceilRadiusZ = (int) Math.ceil(radiusZ);
double nextXn = 0;
forX:
for (int x = 0; x <= ceilRadiusX; ++x) {
final double xn = nextXn;
nextXn = (x + 1) * invRadiusX;
double nextZn = 0;
for (int z = 0; z <= ceilRadiusZ; ++z) {
final double zn = nextZn;
nextZn = (z + 1) * invRadiusZ;
double distanceSq = lengthSq(xn, zn);
if (distanceSq > 1) {
if (z == 0) {
break forX;
}
break;
}
if (!filled) {
if (lengthSq(nextXn, zn) <= 1 && lengthSq(xn, nextZn) <= 1) {
continue;
}
}
for (int y = 0; y < height; ++y) {
setData(cx + x, cy + y, cz + z, data);
setData(cx + -x, cy + y, cz + z, data);
setData(cx + x, cy + y, cz + -z, data);
setData(cx + -x, cy + y, cz + -z, data);
}
}
}
}
public <T> void set(IrisPosition pos, T data) {
try {
setData(pos.getX(), pos.getY(), pos.getZ(), data);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
// Reached per position while an object writes past the edge of the writer window, which is
// how a large object is clipped. Nothing here is actionable from outside the engine.
IrisLogging.debug("No set? " + data.toString() + " for " + pos.toString());
}
}
public <T> void set(List<IrisPosition> positions, T data) {
for (IrisPosition i : positions) {
set(i, data);
}
}
public <T> void set(Set<IrisPosition> positions, T data) {
for (IrisPosition i : positions) {
set(i, data);
}
}
public <T> void setConsumer(Set<IrisPosition> positions, Function3<Integer, Integer, Integer, T> data) {
for (IrisPosition i : positions) {
set(i, data.apply(i.getX(), i.getY(), i.getZ()));
}
}
public boolean isWithin(Vector pos) {
return isWithin(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ());
}
public boolean isWithin(int x, int y, int z) {
int cx = x >> 4;
int cz = z >> 4;
if (y < 0 || y >= mantle.getWorldHeight()) {
return false;
}
return cx >= this.x - radius && cx <= this.x + radius
&& cz >= this.z - radius && cz <= this.z + radius;
}
@Override
public void close() {
for (int index = 0; index < window.length(); index++) {
@@ -1689,6 +1049,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
maximumX = Math.max(maximumX, x);
maximumZ = Math.max(maximumZ, z);
if (type == NativeBlockState.class) {
MatterSlice<TileWrapper> tiles = fragment.getSlice(TileWrapper.class);
if (tiles != null) {
tiles.set(x & 15, y, z & 15, null);
}
fragment.slice(Identifier.class).set(x & 15, y, z & 15, null);
fragment.slice(type).set(x & 15, y, z & 15, value == null ? AIR.get() : value);
} else {
@@ -8,6 +8,7 @@ import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.List;
import java.util.Map;
@@ -51,6 +52,10 @@ public final class ObjectContinuationPersistence {
public static void put(Matter payload, int x, int y, int z, Object value) {
if (value instanceof NativeBlockState state) {
MatterSlice<TileWrapper> tiles = payload.getSlice(TileWrapper.class);
if (tiles != null) {
tiles.set(x, y, z, null);
}
String customKey = state.deferredPlacementKey();
NativeBlockState base = state.placementBaseState();
boolean custom = state.isCustom() && customKey != null && base != null;
@@ -99,12 +99,19 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
captureOriginal(originals, mutation.key());
if (mutation.key().type() == NativeBlockState.class) {
captureOriginal(originals, new DataKey(
mutation.key().x(),
mutation.key().y(),
mutation.key().z(),
Identifier.class
));
TileWrapper tile = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
if (tile != null) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
}
Identifier identifier = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class);
if (identifier != null || mutation instanceof CustomBlockMutation) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class),
identifier == null ? CLEARED : identifier);
}
}
mutation.apply(writer);
}
@@ -148,6 +155,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
Identifier identifier = Identifier.fromString(placementKey);
OverlayCell cell = writableCell(x, y, z);
cell.block = baseState;
cell.tile = CLEARED;
cell.identifier = identifier;
mutations.add(replayed != null
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
@@ -243,6 +251,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
OverlayCell cell = writableCell(x, y, z);
if (data instanceof NativeBlockState) {
cell.block = data;
cell.tile = CLEARED;
cell.identifier = CLEARED;
} else {
cell.put(type, data);
@@ -325,15 +334,15 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return cell;
}
private Object prerequisiteOrCleared(int x, int y, int z, Class<?> type) {
Object value = writer.getPrerequisiteDataIfPresent(x, y, z, type);
private Object publishedOrCleared(int x, int y, int z, Class<?> type) {
Object value = writer.getDataIfPresent(x, y, z, type);
return value == null ? CLEARED : value;
}
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
originals.computeIfAbsent(
key,
candidate -> prerequisiteOrCleared(
candidate -> publishedOrCleared(
candidate.x(),
candidate.y(),
candidate.z(),
@@ -383,27 +392,16 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
entries.sort((first, second) -> Boolean.compare(
second.getKey().type() == NativeBlockState.class,
first.getKey().type() == NativeBlockState.class
));
for (Map.Entry<DataKey, Object> entry : entries) {
for (int index = entries.size() - 1; index >= 0; index--) {
Map.Entry<DataKey, Object> entry = entries.get(index);
DataKey key = entry.getKey();
try {
if (key.type() == NativeBlockState.class
&& writer.restorePrerequisiteCell(key.x(), key.y(), key.z())) {
continue;
}
if (writer.restorePrerequisiteData(key.x(), key.y(), key.z(), key.type())) {
continue;
}
writer.clearData(key.x(), key.y(), key.z(), key.type());
Object original = entry.getValue();
if (original != CLEARED) {
writer.setData(key.x(), key.y(), key.z(), original);
}
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
entry.getValue() == CLEARED ? null : entry.getValue());
} catch (Throwable rollbackFailure) {
failure.addSuppressed(rollbackFailure);
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
}
}
@@ -413,6 +411,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private Object hydrology;
private Object cavern;
private Object identifier;
private Object tile;
private Object string;
private Object treeMaterial;
private Map<Class<?>, Object> other;
@@ -430,6 +429,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (type == Identifier.class) {
return identifier;
}
if (type == TileWrapper.class) {
return tile;
}
if (type == String.class) {
return string;
}
@@ -446,6 +448,8 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cavern = value;
} else if (type == Identifier.class) {
identifier = value;
} else if (type == TileWrapper.class) {
tile = value;
} else if (type == String.class) {
string = value;
} else if (type == TreeBlockMaterial.class) {
@@ -19,9 +19,7 @@ final class StructureFoundationPlanner {
return;
}
long columnKey = pack(x, z);
if (!columns.containsKey(columnKey) || y < columns.get(columnKey)) {
columns.put(columnKey, y);
}
columns.mergeInt(columnKey, y, Math::min);
}
static int findGroundY(int foundationY, int maxDepth, int minimumY, IntPredicate solidAtY) {
@@ -4,6 +4,8 @@ import art.arcane.iris.structure.placement.PlacedStructurePiece;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.structure.StructureCarvingFootprint;
@@ -36,11 +38,12 @@ final class StructurePieceColumns {
if (object == null || rotation == null || object.getBlocks() == null) {
continue;
}
for (IrisBlockVector local : object.getBlocks().keys()) {
if (local == null || B.isAir(object.getBlocks().get(local))) {
VectorMap<NativeBlockState>.Cursor cursor = object.getBlocks().cursor();
while (cursor.next()) {
if (B.isAir(cursor.value())) {
continue;
}
IrisBlockVector rotated = rotation.rotate(local.clone());
IrisBlockVector rotated = rotation.rotate(cursor.key());
long worldX = (long) piece.getX() + rotated.getBlockX();
long worldY = (long) piece.getY() + rotated.getBlockY();
long worldZ = (long) piece.getZ() + rotated.getBlockZ();
@@ -22,13 +22,15 @@ public final class TerrainMatterView {
}
public static <T> T get(Mantle<Matter> mantle, int x, int y, int z, Class<T> type) {
if (!hasJournal(type)) {
return mantle.get(x, y, z, type);
Objects.requireNonNull(type, "Terrain matter type");
if (y < 0 || y >= mantle.getWorldHeight()) {
return null;
}
MantleChunk<Matter> chunk = mantle.getChunk(x >> 4, z >> 4);
synchronized (chunk) {
PreObjectMatterCell cell = mantle.get(x, y, z, PreObjectMatterCell.class);
return cell != null && cell.captures(type) ? cell.original(type) : mantle.get(x, y, z, type);
MantleChunk<Matter> chunk = mantle.useChunk(x >> 4, z >> 4);
try {
return get(chunk, x, y, z, type);
} finally {
chunk.release();
}
}
@@ -49,6 +49,9 @@ import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = false)
public class IrisExpression extends IrisRegistrant {
private static final int MAX_RETAINED_EVALUATION_DEPTH = 8;
private static final int MAX_RETAINED_ARGUMENTS = 256;
@ArrayType(type = IrisExpressionLoad.class, min = 1)
@Description("Variables to use in this expression")
private KList<IrisExpressionLoad> variables = new KList<>();
@@ -66,6 +69,10 @@ public class IrisExpression extends IrisRegistrant {
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, ProceduralStream<Double>> streams = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ThreadLocal<EvaluationScratch> evaluationScratch = ThreadLocal.withInitial(EvaluationScratch::new);
private Expression expression() {
return expressionCache.aquire(() -> {
Scope scope = new Scope(); // Create variable scope. This scope can hold both constants and invocation variables.
@@ -108,31 +115,37 @@ public class IrisExpression extends IrisRegistrant {
}
public double evaluate(RNG rng, double x, double z) {
double[] g = new double[3 + getVariables().size()];
int m = 0;
for (IrisExpressionLoad i : getVariables()) {
g[m++] = i.getValue(rng, getLoader(), x, z);
EvaluationScratch scratch = evaluationScratch.get();
double[] arguments = scratch.acquire(3 + getVariables().size());
try {
int index = 0;
for (IrisExpressionLoad variable : getVariables()) {
arguments[index++] = variable.getValue(rng, getLoader(), x, z);
}
arguments[index++] = x;
arguments[index++] = z;
arguments[index] = -1;
return expression().evaluate(new FunctionContext(rng), arguments);
} finally {
scratch.release();
}
g[m++] = x;
g[m++] = z;
g[m] = -1;
return expression().evaluate(new FunctionContext(rng), g);
}
public double evaluate(RNG rng, double x, double y, double z) {
double[] g = new double[3 + getVariables().size()];
int m = 0;
for (IrisExpressionLoad i : getVariables()) {
g[m++] = i.getValue(rng, getLoader(), x, y, z);
EvaluationScratch scratch = evaluationScratch.get();
double[] arguments = scratch.acquire(3 + getVariables().size());
try {
int index = 0;
for (IrisExpressionLoad variable : getVariables()) {
arguments[index++] = variable.getValue(rng, getLoader(), x, y, z);
}
arguments[index++] = x;
arguments[index++] = y;
arguments[index] = z;
return expression().evaluate(new FunctionContext(rng), arguments);
} finally {
scratch.release();
}
g[m++] = x;
g[m++] = y;
g[m] = z;
return expression().evaluate(new FunctionContext(rng), g);
}
@Override
@@ -144,4 +157,28 @@ public class IrisExpression extends IrisRegistrant {
public String getTypeName() {
return "Expression";
}
private static final class EvaluationScratch {
private final double[][] retained = new double[MAX_RETAINED_EVALUATION_DEPTH][];
private int depth;
private double[] acquire(int length) {
double[] arguments;
if (depth >= retained.length || length > MAX_RETAINED_ARGUMENTS) {
arguments = new double[length];
} else {
arguments = retained[depth];
if (arguments == null || arguments.length != length) {
arguments = new double[length];
retained[depth] = arguments;
}
}
depth++;
return arguments;
}
private void release() {
depth--;
}
}
}
@@ -258,7 +258,7 @@ public class IrisGeneratorStyle {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isFlat() {
return style == null || style.equals(NoiseStyle.FLAT);
return expression == null && imageMap == null && (style == null || style.equals(NoiseStyle.FLAT));
}
public double getMaxFractureDistance() {
@@ -210,8 +210,8 @@ public final class DimensionTerrainContext implements DataProvider {
if (key == null || key.isBlank()) {
continue;
}
maximum += biome.getGenLinkMax(key, engine);
minimum += biome.getGenLinkMin(key, engine);
maximum += biome.getGenLinkMax(key);
minimum += biome.getGenLinkMin(key);
}
interpolatorBounds.put(biome, new NoiseBounds(minimum, maximum));
}
@@ -329,8 +329,8 @@ public final class DimensionTerrainContext implements DataProvider {
if (key == null || key.isBlank()) {
continue;
}
maximum += sampledBiome.getGenLinkMax(key, engine);
minimum += sampledBiome.getGenLinkMin(key, engine);
maximum += sampledBiome.getGenLinkMax(key);
minimum += sampledBiome.getGenLinkMin(key);
}
return new NoiseBounds(minimum, maximum);
} catch (Throwable e) {
@@ -32,7 +32,14 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public final class GenerationCacheWarmer {
private GenerationCacheWarmer() {
@@ -43,6 +50,7 @@ public final class GenerationCacheWarmer {
IrisData data = engine.getData();
RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L);
int[] counter = {0};
List<ProceduralWarmTask> proceduralTasks = new ArrayList<>();
KList<IrisBiome> biomes = engine.getAllBiomes();
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
@@ -50,7 +58,7 @@ public final class GenerationCacheWarmer {
warmPlacements(biome.getObjects(), root, counter, data, engine);
warmDecorators(biome.getDecorators(), engine.getSeedManager().getComponent(), counter, data);
warmOres(biome.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(biome.getProceduralObjects(), root, counter, data);
collectProcedural(biome.getProceduralObjects(), root, counter, proceduralTasks);
}
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
@@ -58,10 +66,11 @@ public final class GenerationCacheWarmer {
for (IrisRegion region : regions) {
warmPlacements(region.getObjects(), root, counter, data, engine);
warmOres(region.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(region.getProceduralObjects(), root, counter, data);
collectProcedural(region.getProceduralObjects(), root, counter, proceduralTasks);
}
warmOres(engine.getDimension().getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(proceduralTasks, data);
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
}
@@ -110,15 +119,93 @@ public final class GenerationCacheWarmer {
}
}
private static void warmProcedural(IrisProceduralObjects procedural, RNG root, int[] counter, IrisData data) {
private static void collectProcedural(IrisProceduralObjects procedural, RNG root, int[] counter,
List<ProceduralWarmTask> tasks) {
if (procedural == null) {
return;
}
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
if (placement != null) {
tasks.add(new ProceduralWarmTask(placement, root.nextParallelRNG(counter[0]++)));
}
placement.getVariantObject(data, root.nextParallelRNG(counter[0]++), null);
}
}
static void warmProcedural(List<ProceduralWarmTask> tasks, IrisData data) {
if (tasks.isEmpty()) {
return;
}
IdentityHashMap<IrisProceduralPlacement, List<ProceduralWarmTask>> identities = new IdentityHashMap<>();
List<List<ProceduralWarmTask>> groups = new ArrayList<>();
for (ProceduralWarmTask task : tasks) {
List<ProceduralWarmTask> group = identities.get(task.placement());
if (group == null) {
group = new ArrayList<>();
identities.put(task.placement(), group);
groups.add(group);
}
group.add(task);
}
int workers = Math.min(groups.size(), Math.min(4, Runtime.getRuntime().availableProcessors()));
if (workers == 1) {
for (ProceduralWarmTask task : tasks) {
task.placement().getVariantObject(data, task.rng(), null);
}
return;
}
Throwable failure = null;
boolean interrupted = false;
try (ExecutorService executor = Executors.newFixedThreadPool(workers, runnable -> {
Thread thread = new Thread(runnable, "Iris-Procedural-Warm");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
})) {
List<Future<?>> pending = new ArrayList<>(groups.size());
for (List<ProceduralWarmTask> group : groups) {
pending.add(executor.submit(() -> warmGroup(group, data)));
}
for (Future<?> future : pending) {
boolean complete = false;
while (!complete) {
try {
future.get();
complete = true;
} catch (InterruptedException exception) {
interrupted = true;
} catch (ExecutionException exception) {
Throwable cause = exception.getCause();
if (failure == null) {
failure = cause;
} else if (failure != cause) {
failure.addSuppressed(cause);
}
complete = true;
}
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException exception) {
throw exception;
}
if (failure != null) {
throw new IllegalStateException("Procedural generation cache warming failed.", failure);
}
}
private static void warmGroup(List<ProceduralWarmTask> group, IrisData data) {
for (ProceduralWarmTask task : group) {
task.placement().getVariantObject(data, task.rng(), null);
}
}
record ProceduralWarmTask(IrisProceduralPlacement placement, RNG rng) {
}
}
@@ -1955,8 +1955,8 @@ public class IrisComplex implements DataProvider {
continue;
}
max += biome.getGenLinkMax(key, engine);
min += biome.getGenLinkMin(key, engine);
max += biome.getGenLinkMax(key);
min += biome.getGenLinkMin(key);
}
return new GeneratorBounds(min, max);
@@ -18,13 +18,9 @@
package art.arcane.iris.generation.runtime;
import art.arcane.volmlib.util.data.KCache;
public interface MeteredCache {
long getSize();
KCache<?, ?> getRawCache();
long getMaxSize();
default double getUsage() {
@@ -20,7 +20,6 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.data.KCache;
import art.arcane.volmlib.util.format.Form;
import art.arcane.iris.platform.bukkit.plugin.IrisService;
import art.arcane.volmlib.util.scheduling.Looper;
@@ -33,7 +32,6 @@ import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PreservationSVC implements IrisService, PreservationRegistry {
@@ -188,10 +186,6 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
caches.add(new WeakReference<>(cache));
}
public List<KCache<?, ?>> caches() {
return cacheStream().map(MeteredCache::getRawCache).collect(Collectors.toList());
}
@Unmodifiable
public List<MeteredCache> getCaches() {
return cacheStream().toList();
@@ -192,7 +192,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
IrisDimension dimension = terrainContext.getDimension();
IrisData data = terrainContext.getData();
IrisBiome biome = layer.biome();
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
int bedrockDepth = dimension.isBedrock() ? 1 : 0;
int surfaceDepth = Math.max(0, layer.normalTerrainHeight() + 1 - bedrockDepth);
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
KList<NativeBlockState> surfaceBlocks = biome == null || layer.terrainColumn() != null
? null
@@ -207,6 +208,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
terrainContext.getSlopeStream()
);
int paletteSurfaceY = layer.normalTerrainHeight();
KList<NativeBlockState> ceilingBlocks = null;
int terrainSpan = layer.terrainColumn() == null ? -1 : layer.terrainColumn().spanCount() - 1;
KList<NativeBlockState> seaBlocks = biome == null || fluidDepth == 0
? null
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
@@ -239,13 +242,32 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
if (sourceSurfaceY != paletteSurfaceY) {
paletteSurfaceY = sourceSurfaceY;
surfaceBlocks = null;
ceilingBlocks = null;
}
if (depth == 0 && layer.surfaceBlock() != null) {
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
} else {
if (terrainSpan >= 0) {
while (terrainSpan >= 0 && sourceY < layer.terrainColumn().ceiling(terrainSpan)) {
terrainSpan--;
}
int ceilingY = layer.terrainColumn().ceiling(terrainSpan);
int ceilingDepth = sourceY - ceilingY;
if (terrainSpan > 0 && depth >= 2 && ceilingDepth < 2) {
if (ceilingBlocks == null && biome != null) {
ceilingBlocks = biome.generateCeilingLayers(dimension, worldX, worldZ, rng,
2, ceilingY, data, null);
}
writeBlock(output, metadata, localX, y, localZ,
ceilingBlocks != null && ceilingBlocks.hasIndex(ceilingDepth)
? ceilingBlocks.get(ceilingDepth) : layer.rockBlock());
continue;
}
}
if (surfaceBlocks == null && biome != null) {
surfaceBlocks = biome.generateLayersWithSlope(dimension, worldX, worldZ, rng,
sourceSurfaceY, sourceSurfaceY, data, terrainContext.getSurfaceSlopeStream(sourceSurfaceY));
sourceSurfaceY + 1 - bedrockDepth, sourceSurfaceY, data,
terrainContext.getSurfaceSlopeStream(sourceSurfaceY));
}
writeBlock(output, metadata, localX, y, localZ,
surfaceBlocks != null && surfaceBlocks.hasIndex(depth)
@@ -178,15 +178,24 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
return block == null ? fallbackSolid : block;
}
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, int paletteDepth, IrisData data, IrisComplex complex) {
private PaletteContext createPaletteContext(IrisBiome parent, IrisFloatingChildBiomes entry, IrisDimension dimension, int wx, int wz, int paletteDepth, int surfaceY, int baseY, IrisData data, IrisComplex complex) {
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
RNG layerRng = paletteRng(rng, entry);
KList<NativeBlockState> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
boolean physicalDepth = usesPhysicalDepth(target, entry);
int topPaletteDepth = physicalDepth ? Math.max(paletteDepth, surfaceY - baseY + 1) : paletteDepth;
KList<NativeBlockState> topBlocks = target == null ? null : target.generateLayers(dimension, wx, wz, layerRng, topPaletteDepth, surfaceY, data, complex);
if (topBlocks == null || topBlocks.isEmpty()) {
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
physicalDepth = usesPhysicalDepth(parent, entry);
topPaletteDepth = physicalDepth ? Math.max(paletteDepth, surfaceY - baseY + 1) : paletteDepth;
topBlocks = parent.generateLayers(dimension, wx, wz, layerRng, topPaletteDepth, surfaceY, data, complex);
}
KList<NativeBlockState> bottomBlocks = generateBottomPaletteLayers(entry, dimension, wx, wz, layerRng, paletteDepth, data, complex);
return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone"));
return new PaletteContext(topBlocks, bottomBlocks, B.getState("minecraft:stone"), surfaceY, physicalDepth);
}
private static boolean usesPhysicalDepth(IrisBiome biome, IrisFloatingChildBiomes entry) {
return biome != null && biome.isLockLayers() && (entry == null || entry.getBottomPaletteMode() == null
|| entry.getBottomPaletteMode() == FloatingBottomPaletteMode.DEPTH);
}
static RNG paletteRng(RNG worldRng, IrisFloatingChildBiomes entry) {
@@ -198,11 +207,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
private final KList<NativeBlockState> topBlocks;
private final KList<NativeBlockState> bottomBlocks;
private final NativeBlockState fallbackSolid;
private final int surfaceY;
private final boolean physicalDepth;
private PaletteContext(KList<NativeBlockState> topBlocks, KList<NativeBlockState> bottomBlocks, NativeBlockState fallbackSolid) {
private PaletteContext(KList<NativeBlockState> topBlocks, KList<NativeBlockState> bottomBlocks, NativeBlockState fallbackSolid, int surfaceY, boolean physicalDepth) {
this.topBlocks = topBlocks;
this.bottomBlocks = bottomBlocks;
this.fallbackSolid = fallbackSolid;
this.surfaceY = surfaceY;
this.physicalDepth = physicalDepth;
}
}
@@ -251,12 +264,12 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Nativ
IrisFloatingChildBiomes entry = sample.entryAt(k);
PaletteContext paletteContext = paletteContexts.get(entry);
if (paletteContext == null) {
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, paletteDepth, data, complex);
paletteContext = createPaletteContext(parent, entry, dimension, wx, wz, paletteDepth, y, sample.islandBaseY, data, complex);
paletteContexts.put(entry, paletteContext);
}
int depth = topDepthByEntry.getOrDefault(entry, 0);
int bottomDepth = bottomDepths == null || bottomDepths[k] < 0 ? depth : bottomDepths[k];
NativeBlockState block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, depth, bottomDepth, paletteContext.fallbackSolid);
NativeBlockState block = selectPaletteBlock(entry, paletteContext.topBlocks, paletteContext.bottomBlocks, paletteContext.physicalDepth ? paletteContext.surfaceY - y : depth, bottomDepth, paletteContext.fallbackSolid);
if (block != null) {
output.set(xf, y, zf, block);
}
@@ -315,7 +315,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
cut = Math.max(0, naturalSurfaceY - layerSurfaceY);
}
blocks = biome.generateLayers(dimension, realX, realZ, rng,
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
layerSurfaceY + cut + 1 - bedrockFloor, layerSurfaceY + cut, data, complex);
}
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
@@ -436,7 +436,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
int depthFromFace = y - faceY;
if (upperBlocks == null && upperBiome != null) {
upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(),
realX, realZ, rng, sourceSurfaceY, sourceSurfaceY,
realX, realZ, rng, sourceSurfaceY + 1 - (bedrockEnabled ? 1 : 0), sourceSurfaceY,
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
}
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
@@ -7,7 +7,6 @@ import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.cache.WorldCache2DDouble;
import art.arcane.volmlib.util.data.KCache;
public class CachedDoubleStream2D extends BasicStream<Double> implements ProceduralStream<Double>, MeteredCache, ChunkFillableStream2D, ChunkFillableDoubleStream2D {
private final ProceduralStream<Double> stream;
@@ -56,11 +55,6 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
return cache.getSize();
}
@Override
public KCache<?, ?> getRawCache() {
return null;
}
@Override
public long getMaxSize() {
return cache.getMaxSize();
@@ -24,7 +24,6 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.volmlib.util.cache.ChunkCache2D;
import art.arcane.volmlib.util.cache.WorldCache2D;
import art.arcane.volmlib.util.data.KCache;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStream<T>, MeteredCache, ChunkFillableStream2D {
@@ -70,11 +69,6 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
return cache.getSize();
}
@Override
public KCache<?, ?> getRawCache() {
return null;
}
@Override
public long getMaxSize() {
return cache.getMaxSize();
@@ -64,11 +64,6 @@ public class CachedStream3D<T> extends BasicStream<T> implements ProceduralStrea
return cache.getSize();
}
@Override
public KCache<?, ?> getRawCache() {
return cache;
}
@Override
public long getMaxSize() {
return cache.getMaxSize();
@@ -6,17 +6,15 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.function.Function3;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
public class ContextInjectingStream<T> extends BasicStream<T> {
private final Engine engine;
private final Function3<ChunkContext, Integer, Integer, T> contextAccessor;
public ContextInjectingStream(ProceduralStream<T> stream, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
this(stream, null, contextAccessor);
}
public ContextInjectingStream(ProceduralStream<T> stream, Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
super(stream);
this.engine = engine;
this.engine = Objects.requireNonNull(engine);
this.contextAccessor = contextAccessor;
}
@@ -30,9 +28,9 @@ public class ContextInjectingStream<T> extends BasicStream<T> {
int blockZ = (int) z;
if (chunkContext != null
&& (engine == null || context.getEngine() == engine)
&& context.getEngine() == engine
&& context.getGenerationSessionId() == chunkContext.getGenerationSessionId()
&& (engine == null || context.getGenerationSessionId() == engine.getGenerationSessionId())
&& context.getGenerationSessionId() == engine.getGenerationSessionId()
&& blockX >> 4 == chunkContext.getX() >> 4
&& blockZ >> 4 == chunkContext.getZ() >> 4) {
T t = contextAccessor.apply(chunkContext, blockX & 15, blockZ & 15);
@@ -1,13 +1,9 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.function.Function3;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
public final class GenerationStreams {
private GenerationStreams() {
@@ -25,19 +21,7 @@ public final class GenerationStreams {
return new CachedStream3D<T>(name, engine, stream, maxSize);
}
public static <T> ProceduralStream<T> contextInjecting(ProceduralStream<T> stream, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
//return stream;
return new ContextInjectingStream<>(stream, contextAccessor);
}
public static <T> ProceduralStream<T> contextInjecting(ProceduralStream<T> stream, Engine engine, Function3<ChunkContext, Integer, Integer, T> contextAccessor) {
return new ContextInjectingStream<>(stream, engine, contextAccessor);
}
public static <T> ProceduralStream<Double> style(ProceduralStream<T> stream, RNG rng, IrisStyledRange range, IrisData data) {
return ProceduralStream.of((x, z) -> {
double d = stream.getDouble(x, z);
return range.get(rng, d, -d, data);
}, Interpolated.DOUBLE);
}
}
@@ -18,7 +18,7 @@ public final class ProvisionalSampling {
/** Records that the calling thread just answered from inputs that are not final. */
public static void mark() {
TRACKERS.get().provisional = true;
TRACKERS.get().revision++;
}
/**
@@ -27,15 +27,9 @@ public final class ProvisionalSampling {
*/
public static <T> T memoizable(ProceduralStream<T> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = stream.get(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
long revision = tracker.revision;
T value = stream.get(x, z);
if (tracker.revision != revision) {
throw new Unmemoizable(value);
}
return value;
@@ -43,15 +37,9 @@ public final class ProvisionalSampling {
public static double memoizableDouble(ProceduralStream<Double> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
double value;
boolean provisional;
try {
value = stream.getDouble(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
long revision = tracker.revision;
double value = stream.getDouble(x, z);
if (tracker.revision != revision) {
throw new Unmemoizable(value);
}
return value;
@@ -59,34 +47,16 @@ public final class ProvisionalSampling {
public static <T> T memoizable(Supplier<T> resolver) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = resolver.get();
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
long revision = tracker.revision;
T value = resolver.get();
if (tracker.revision != revision) {
throw new Unmemoizable(value);
}
return value;
}
private static final class Tracker {
private boolean provisional;
private boolean begin() {
boolean outer = provisional;
provisional = false;
return outer;
}
private boolean end(boolean outer) {
boolean inner = provisional;
provisional = outer || inner;
return inner;
}
private long revision;
}
/** Carries a provisional value past a memoizing cache so the cache stores nothing. */
@@ -145,25 +145,26 @@ public final class Terrain3DRuntime {
int count = 1;
boundaries[0] = 0;
boolean previousSolid = true;
for (int gridY = Math.floorDiv(minimum, STEP) * STEP; gridY <= maximum; gridY += STEP) {
NodeSample lowerNW = northWest.sample(gridY);
NodeSample lowerNE = northEast.sample(gridY);
NodeSample lowerSW = southWest.sample(gridY);
NodeSample lowerSE = southEast.sample(gridY);
int firstGridY = Math.floorDiv(minimum, STEP) * STEP;
NodeSample lowerNW = northWest.sample(firstGridY);
NodeSample lowerNE = northEast.sample(firstGridY);
NodeSample lowerSW = southWest.sample(firstGridY);
NodeSample lowerSE = southEast.sample(firstGridY);
double lowerDisplacement = interpolate(lowerNW.displacement, lowerNE.displacement,
lowerSW.displacement, lowerSE.displacement, dx, dz);
double lowerCrack = interpolate(lowerNW.crackDistance, lowerNE.crackDistance,
lowerSW.crackDistance, lowerSE.crackDistance, dx, dz);
double lowerDepth = interpolate(lowerNW.crackDepth, lowerNE.crackDepth,
lowerSW.crackDepth, lowerSE.crackDepth, dx, dz);
for (int gridY = firstGridY; gridY <= maximum; gridY += STEP) {
NodeSample upperNW = northWest.sample(gridY + STEP);
NodeSample upperNE = northEast.sample(gridY + STEP);
NodeSample upperSW = southWest.sample(gridY + STEP);
NodeSample upperSE = southEast.sample(gridY + STEP);
double lowerDisplacement = interpolate(lowerNW.displacement, lowerNE.displacement,
lowerSW.displacement, lowerSE.displacement, dx, dz);
double upperDisplacement = interpolate(upperNW.displacement, upperNE.displacement,
upperSW.displacement, upperSE.displacement, dx, dz);
double lowerCrack = interpolate(lowerNW.crackDistance, lowerNE.crackDistance,
lowerSW.crackDistance, lowerSE.crackDistance, dx, dz);
double upperCrack = interpolate(upperNW.crackDistance, upperNE.crackDistance,
upperSW.crackDistance, upperSE.crackDistance, dx, dz);
double lowerDepth = interpolate(lowerNW.crackDepth, lowerNE.crackDepth,
lowerSW.crackDepth, lowerSE.crackDepth, dx, dz);
double upperDepth = interpolate(upperNW.crackDepth, upperNE.crackDepth,
upperSW.crackDepth, upperSE.crackDepth, dx, dz);
int lastY = Math.min(maximum, gridY + STEP - 1);
@@ -185,6 +186,9 @@ public final class Terrain3DRuntime {
previousSolid = solid;
}
}
lowerDisplacement = upperDisplacement;
lowerCrack = upperCrack;
lowerDepth = upperDepth;
}
if (previousSolid) {
if (count == boundaries.length) {
@@ -2,16 +2,24 @@ package art.arcane.iris.pack;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
public final class AtomicDirectoryPublisher {
private static final int MAXIMUM_METADATA_DELETION_RETRIES = 3;
private static final Set<String> OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini");
private static final String APPLE_DOUBLE_PREFIX = "._";
private AtomicDirectoryPublisher() {
}
@@ -139,8 +147,52 @@ public final class AtomicDirectoryPublisher {
}
try (Stream<Path> stream = Files.walk(path)) {
for (Path entry : stream.sorted(Comparator.reverseOrder()).toList()) {
Files.deleteIfExists(entry);
deleteEntry(entry);
}
}
}
public static boolean isOperatingSystemMetadata(Path entry) {
if (!Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
Path name = entry.getFileName();
if (name == null) {
return false;
}
String fileName = name.toString();
return OS_METADATA_FILES.contains(fileName) || fileName.startsWith(APPLE_DOUBLE_PREFIX);
}
private static void deleteEntry(Path entry) throws IOException {
for (int attempt = 0; ; attempt++) {
try {
Files.deleteIfExists(entry);
return;
} catch (DirectoryNotEmptyException notEmpty) {
if (attempt >= MAXIMUM_METADATA_DELETION_RETRIES || !deleteResidualMetadata(entry)) {
throw notEmpty;
}
}
}
}
private static boolean deleteResidualMetadata(Path directory) throws IOException {
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
List<Path> entries;
try (Stream<Path> stream = Files.list(directory)) {
entries = stream.toList();
}
for (Path entry : entries) {
if (!isOperatingSystemMetadata(entry)) {
return false;
}
}
for (Path entry : entries) {
Files.deleteIfExists(entry);
}
return true;
}
}
@@ -1,384 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.pack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import lombok.Data;
import org.bukkit.World;
import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
/**
* Represents an Iris pack that exists
*/
@Data
public class IrisPack {
private final File folder;
private final IrisData data;
/**
* Create an iris pack backed by a data folder
* the data folder is assumed to be in the Iris/packs/NAME folder
*
* @param name the name
*/
public IrisPack(String name) {
this(packsPack(name));
}
/**
* Create an iris pack backed by a data folder
*
* @param folder the folder of the pack. Must be a directory
*/
public IrisPack(File folder) {
this.folder = folder;
if (!folder.exists()) {
throw new RuntimeException("Cannot open Pack " + folder.getPath() + " (directory doesnt exist)");
}
if (!folder.isDirectory()) {
throw new RuntimeException("Cannot open Pack " + folder.getPath() + " (not a directory)");
}
this.data = IrisData.get(folder);
}
/**
* Create a new pack from the input url
*
* @param sender the sender
* @param url the url, or name, or really anything see IrisPackRepository.from(String)
* @return the iris pack
* @throws IrisException fails
*/
public static Future<IrisPack> from(VolmitSender sender, String url) throws IrisException {
IrisPackRepository repo = IrisPackRepository.from(url);
if (repo == null) {
throw new IrisException("Null Repo");
}
try {
return from(sender, repo);
} catch (MalformedURLException e) {
throw new IrisException("Malformed URL " + e.getMessage());
}
}
/**
* Create a pack from a repo
*
* @param sender the sender
* @param repo the repo
* @return the pack
* @throws MalformedURLException shit happens
*/
public static Future<IrisPack> from(VolmitSender sender, IrisPackRepository repo) throws MalformedURLException {
CompletableFuture<IrisPack> pack = new CompletableFuture<>();
repo.install(sender, () -> {
pack.complete(new IrisPack(repo.getRepo()));
});
return pack;
}
/**
* Create a blank pack with a given name
*
* @param name the name of the pack
* @return the pack
* @throws IrisException if the pack already exists or another error
*/
public static IrisPack blank(String name) throws IrisException {
File f = packsPack(name);
if (f.exists()) {
throw new IrisException("Already exists");
}
File fd = new File(f, "dimensions/" + name + ".json");
fd.getParentFile().mkdirs();
try {
IO.writeAll(fd, "{\n" +
" \"name\": \"" + Form.capitalize(name) + "\",\n" +
" \"version\": 1\n" +
"}\n");
} catch (IOException e) {
throw new IrisException(e.getMessage(), e);
}
IrisPack pack = new IrisPack(f);
pack.updateWorkspace();
return pack;
}
/**
* Get a packs pack folder for a name. Such that overworld would resolve as Iris/packs/overworld
*
* @param name the name
* @return the file path
*/
public static File packsPack(String name) {
return IrisPlatforms.get().packsFolderNoCreate(name);
}
private static KList<File> collectFiles(File f, String fileExtension) {
KList<File> l = new KList<>();
if (f.isDirectory()) {
for (File i : f.listFiles()) {
l.addAll(collectFiles(i, fileExtension));
}
} else if (f.getName().endsWith("." + fileExtension)) {
l.add(f);
}
return l;
}
/**
* Delete this pack. This invalidates this pack and you should
* probably no longer use this instance after deleting this pack
*/
public void delete() {
IO.delete(folder);
folder.delete();
}
/**
* Get the name of this pack
*
* @return the pack name
*/
public String getName() {
return folder.getName();
}
/**
* Get the file path of the workspace file
*
* @return the workspace file path
*/
public File getWorkspaceFile() {
return new File(getFolder(), getName() + ".code-workspace");
}
/**
* Update the workspace file
*
* @return true if it was updated
*/
public boolean updateWorkspace() {
getFolder().mkdirs();
File ws = getWorkspaceFile();
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
IrisLogging.debug("Building Workspace: " + ws.getPath());
JSONObject j = generateWorkspaceConfig();
IO.writeAll(ws, j.toString(4));
p.end();
IrisLogging.debug("Building Workspace: " + ws.getPath() + " took " + Form.duration(p.getMilliseconds(), 2));
return true;
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.warn("Pack invalid: " + ws.getAbsolutePath() + " Re-creating. You may loose some vs-code workspace settings! But not your actual project!");
ws.delete();
try {
IO.writeAll(ws, generateWorkspaceConfig());
} catch (IOException e1) {
IrisLogging.reportError(e1);
}
}
return false;
}
/**
* Install this pack into a world
*
* @param world the world to install into (world/iris/pack)
* @return the installed pack
*/
public IrisPack install(World world) throws IrisException {
return install(new File(world.getWorldFolder(), "iris/pack"));
}
/**
* Install this pack into a world
*
* @param world the world to install into (world/iris/pack)
* @return the installed pack
*/
public IrisPack install(IrisWorld world) throws IrisException {
return install(new File(world.worldFolder(), "iris/pack"));
}
/**
* Install this pack into a world
*
* @param folder the folder to install this pack into
* @return the installed pack
*/
public IrisPack install(File folder) throws IrisException {
if (folder.exists()) {
throw new IrisException("Cannot install new pack because the folder " + folder.getName() + " already exists!");
}
folder.mkdirs();
try {
FileUtils.copyDirectory(getFolder(), folder);
} catch (IOException e) {
IrisLogging.reportError(e);
}
return new IrisPack(folder);
}
/**
* Create a new pack using this pack as a template. The new pack will be renamed & have a renamed dimension
* to match it.
*
* @param newName the new pack name
* @return the new IrisPack
*/
public IrisPack install(String newName) throws IrisException {
File newPack = packsPack(newName);
if (newPack.exists()) {
throw new IrisException("Cannot install new pack because the folder " + newName + " already exists!");
}
try {
FileUtils.copyDirectory(getFolder(), newPack);
} catch (IOException e) {
IrisLogging.reportError(e);
}
IrisData data = IrisData.get(newPack);
IrisDimension dim = data.getDimensionLoader().load(getDimensionKey());
data.dump();
File from = dim.getLoadFile();
File to = new File(from.getParentFile(), newName + ".json");
try {
FileUtils.moveFile(from, to);
new File(newPack, getWorkspaceFile().getName()).delete();
} catch (Throwable e) {
throw new IrisException(e);
}
IrisPack pack = new IrisPack(newPack);
pack.updateWorkspace();
return pack;
}
/**
* The dimension's assumed loadkey
*
* @return getName()
*/
public String getDimensionKey() {
return getName();
}
/**
* Get the main dimension object
*
* @return the dimension (folder name as dim key)
*/
public IrisDimension getDimension() {
return getData().getDimensionLoader().load(getDimensionKey());
}
/**
* Find all files in this pack with the given extension
*
* @param fileExtension the extension
* @return the list of files
*/
public KList<File> collectFiles(String fileExtension) {
return collectFiles(getFolder(), fileExtension);
}
private JSONObject generateWorkspaceConfig() {
JSONObject ws = new JSONObject();
JSONArray folders = new JSONArray();
JSONObject folder = new JSONObject();
folder.put("path", ".");
folders.put(folder);
ws.put("folders", folders);
JSONObject settings = new JSONObject();
settings.put("workbench.colorTheme", "Monokai");
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
settings.put("workbench.tips.enabled", false);
settings.put("workbench.tree.indent", 24);
settings.put("files.autoSave", "onFocusChange");
JSONObject jc = new JSONObject();
jc.put("editor.autoIndent", "brackets");
jc.put("editor.acceptSuggestionOnEnter", "smart");
jc.put("editor.cursorSmoothCaretAnimation", true);
jc.put("editor.dragAndDrop", false);
jc.put("files.trimTrailingWhitespace", true);
jc.put("diffEditor.ignoreTrimWhitespace", true);
jc.put("files.trimFinalNewlines", true);
jc.put("editor.suggest.showKeywords", false);
jc.put("editor.suggest.showSnippets", false);
jc.put("editor.suggest.showWords", false);
JSONObject st = new JSONObject();
st.put("strings", true);
jc.put("editor.quickSuggestions", st);
jc.put("editor.suggest.insertMode", "replace");
settings.put("[json]", jc);
settings.put("json.maxItemsComputed", 30000);
JSONArray schemas = new JSONArray();
IrisData dm = IrisData.get(getFolder());
for (ResourceLoader<?> r : dm.getLoaders().v()) {
if (r.supportsSchemas()) {
schemas.put(r.buildSchema());
}
}
settings.put("json.schemas", schemas);
ws.put("settings", settings);
return ws;
}
}
@@ -1,137 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.pack;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.volmlib.util.format.Form;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.jobs.DownloadJob;
import art.arcane.iris.world.task.jobs.JobCollection;
import art.arcane.iris.world.task.jobs.SingleJob;
import lombok.Builder;
import lombok.Data;
import org.zeroturnaround.zip.ZipUtil;
import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.UUID;
@Data
@Builder
public class IrisPackRepository {
private static final String DEFAULT_BRANCH = "stable";
@Builder.Default
private String user = "IrisDimensions";
@Builder.Default
private String repo = "overworld";
@Builder.Default
private String branch = DEFAULT_BRANCH;
@Builder.Default
private String tag = "";
/**
*
*/
public static IrisPackRepository from(String g) {
// https://github.com/IrisDimensions/overworld
if (g.startsWith("https://github.com/")) {
String sub = g.split("\\Qgithub.com/\\E")[1];
IrisPackRepository r = IrisPackRepository.builder()
.user(sub.split("\\Q/\\E")[0])
.repo(sub.split("\\Q/\\E")[1]).build();
if (g.contains("/tree/")) {
r.setBranch(g.split("/tree/")[1]);
}
return r;
} else if (g.contains("/")) {
String[] f = g.split("\\Q/\\E");
if (f.length == 1) {
return from(g);
} else if (f.length == 2) {
return IrisPackRepository.builder()
.user(f[0])
.repo(f[1])
.build();
} else if (f.length >= 3) {
IrisPackRepository r = IrisPackRepository.builder()
.user(f[0])
.repo(f[1])
.build();
if (f[2].startsWith("#")) {
r.setTag(f[2].substring(1));
} else {
r.setBranch(f[2]);
}
return r;
}
} else {
return IrisPackRepository.builder()
.user("IrisDimensions")
.repo(g)
.branch(DEFAULT_BRANCH)
.build();
}
return null;
}
public String toURL() {
if (!tag.trim().isEmpty()) {
return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/tags/" + tag;
}
return "https://codeload.github.com/" + user + "/" + repo + "/zip/refs/heads/" + branch;
}
public void install(VolmitSender sender, Runnable whenComplete) throws MalformedURLException {
File pack = IrisPlatforms.get().packsFolderNoCreate(getRepo());
if (!pack.exists()) {
File dl = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "dltk-" + UUID.randomUUID() + ".zip");
File work = new File(IrisPlatforms.get().dataFolder("cache", "temp"), "extk-" + UUID.randomUUID());
new JobCollection(Form.capitalize(getRepo()),
new DownloadJob(toURL(), pack),
new SingleJob(IrisLanguage.text(RuntimeUiMessages.JOB_EXTRACTING), () -> ZipUtil.unpack(dl, work)),
new SingleJob(IrisLanguage.text(RuntimeUiMessages.JOB_INSTALLING), () -> {
try {
FileUtils.copyDirectory(work.listFiles()[0], pack);
} catch (IOException e) {
IrisLogging.reportError("Failed to install Iris pack into " + pack + ".", e);
}
})).execute(sender, whenComplete);
} else {
sender.sendMessage(IrisLanguage.text(RuntimeUiMessages.PACK_ALREADY_EXISTS));
}
}
}
@@ -108,13 +108,21 @@ final class PackBiomeLayerValidator {
private static void validateLayerArray(JSONObject biome, String field, String biomeKey,
List<String> blockingErrors) {
if (!biome.has(field) || biome.isNull(field)) {
if (!biome.has(field)) {
return;
}
JSONArray array = biome.optJSONArray(field);
if (array == null) {
blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array.");
return;
}
for (int index = 0; index < array.length(); index++) {
Object layer = array.opt(index);
if (!(layer instanceof JSONObject) && !(layer instanceof String)) {
blockingErrors.add("Biome '" + biomeKey + "' " + field + "[" + index
+ "] must be an object or snippet reference.");
}
}
}
}
@@ -63,6 +63,7 @@ final class PackDimensionValidator {
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
validateObjectScaleFactor(dimensionKey, dimJson, blockingErrors);
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
validateUpperDimension(dimensionKey, dimJson, dimensionKeys, blockingErrors);
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
@@ -107,6 +108,21 @@ final class PackDimensionValidator {
}
}
private static void validateUpperDimension(String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
List<String> errors) {
if (!dimension.has("upperDimension") || dimension.isNull("upperDimension")) {
return;
}
Object value = dimension.opt("upperDimension");
if (!(value instanceof String key)) {
errors.add("Dimension '" + dimensionKey + "' upperDimension must be a dimension key.");
return;
}
if (!key.isEmpty() && !key.equalsIgnoreCase("none") && !dimensionKeys.contains(key)) {
errors.add("Dimension '" + dimensionKey + "' upperDimension references missing dimension '" + key + "'.");
}
}
static void validateObjectScaleFactor(String dimensionKey, JSONObject dimension, List<String> errors) {
validateFiniteNumber(dimension, "allObjectScaleFactor", IrisObjectScale.MINIMUM_FACTOR,
IrisObjectScale.MAXIMUM_FACTOR, "Dimension '" + dimensionKey + "'", errors);
@@ -385,15 +401,31 @@ final class PackDimensionValidator {
* initializers (min 16, max 32); logicalHeight absent means 256.
*/
static void validateDimensionHeights(File packFolder, String dimensionKey, JSONObject dimJson, List<String> blockingErrors) {
JSONObject range = resolveDimensionHeight(packFolder, dimJson);
if (range == null && dimJson.has("dimensionHeight") && !dimJson.isNull("dimensionHeight")) {
if (!(dimJson.opt("dimensionHeight") instanceof String)) {
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight must be an object or a range snippet reference.");
}
// Unresolvable snippet references are reported by the content-key machinery.
if (dimJson.has("dimensionHeight") && dimJson.isNull("dimensionHeight")) {
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight must be an object or a range snippet reference.");
return;
}
JSONObject range = resolveDimensionHeight(packFolder, dimJson);
if (range == null && dimJson.has("dimensionHeight")) {
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight must be an object or a resolvable range snippet reference.");
return;
}
if (range != null) {
boolean validMin = validateHeightNumber(range, "min", dimensionKey, blockingErrors);
boolean validMax = validateHeightNumber(range, "max", dimensionKey, blockingErrors);
if (!validMin || !validMax) {
return;
}
}
if (dimJson.has("logicalHeight") && !dimJson.isNull("logicalHeight")) {
double logicalHeightValue = dimJson.optDouble("logicalHeight", Double.NaN);
if (!Double.isFinite(logicalHeightValue) || logicalHeightValue != Math.rint(logicalHeightValue)
|| logicalHeightValue < Integer.MIN_VALUE || logicalHeightValue > Integer.MAX_VALUE) {
blockingErrors.add("Dimension '" + dimensionKey + "' logicalHeight must be an integer.");
return;
}
}
int minY;
int maxY;
if (range == null) {
@@ -404,7 +436,7 @@ final class PackDimensionValidator {
maxY = (int) range.optDouble("max", 32D);
}
int height = maxY - minY;
int logicalHeight = dimJson.optInt("logicalHeight", 256);
int logicalHeight = (int) dimJson.optDouble("logicalHeight", 256D);
if (height < IrisDimensionType.MIN_HEIGHT || height > IrisDimensionType.MAX_HEIGHT) {
blockingErrors.add("Dimension '" + dimensionKey + "' dimensionHeight span (max - min) is " + height
@@ -507,6 +539,31 @@ final class PackDimensionValidator {
return Double.toString(value);
}
private static boolean validateHeightNumber(JSONObject range, String field, String dimensionKey, List<String> errors) {
if (!range.has(field) || range.isNull(field)) {
return true;
}
Object value = range.opt(field);
double number;
try {
if (value instanceof Number numeric) {
number = numeric.doubleValue();
} else if (value instanceof String text) {
number = Double.parseDouble(text);
} else {
throw new NumberFormatException();
}
} catch (NumberFormatException exception) {
errors.add("Dimension '" + dimensionKey + "' dimensionHeight." + field + " must be a finite number.");
return false;
}
if (!Double.isFinite(number)) {
errors.add("Dimension '" + dimensionKey + "' dimensionHeight." + field + " must be a finite number.");
return false;
}
return true;
}
private static JSONObject resolveDimensionHeight(File packFolder, JSONObject dimJson) {
if (!dimJson.has("dimensionHeight") || dimJson.isNull("dimensionHeight")) {
return null;
@@ -59,6 +59,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipFile;
public final class PackDownloader {
private static final String DEFAULT_OVERWORLD_PACK = "overworld";
@@ -641,33 +642,36 @@ public final class PackDownloader {
);
return null;
}
if (cancellation != null) {
cancellation.beginPublication();
}
IrisData.getLoaded(new File(packsFolder, prepared.key())).ifPresent(IrisData::close);
IrisData.getLoaded(target.toFile()).ifPresent(IrisData::close);
Path retainedBackup = null;
if (forceOverwrite && Files.exists(target)) {
Path backupRoot = packsRoot.resolve(".backups");
if (Files.isSymbolicLink(backupRoot)) {
throw new IOException("Pack backup folder is an unsafe symbolic link: " + backupRoot);
try (PackValidationRegistry.RootMutation mutation = PackValidationRegistry.beginRootMutation(target)) {
PackValidationRegistry.remove(prepared.key());
if (cancellation != null) {
cancellation.beginPublication();
}
Files.createDirectories(backupRoot);
retainedBackup = backupRoot.resolve(prepared.key() + "-" + UUID.randomUUID());
}
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) {
if (retainedBackup != null) {
publication.retainBackup(retainedBackup);
IrisData.getLoaded(new File(packsFolder, prepared.key())).ifPresent(IrisData::close);
IrisData.getLoaded(target.toFile()).ifPresent(IrisData::close);
if (forceOverwrite && Files.exists(target)) {
Path backupRoot = packsRoot.resolve(".backups");
if (Files.isSymbolicLink(backupRoot)) {
throw new IOException("Pack backup folder is an unsafe symbolic link: " + backupRoot);
}
Files.createDirectories(backupRoot);
retainedBackup = backupRoot.resolve(prepared.key() + "-" + UUID.randomUUID());
}
publication.commit();
if (retainedBackup == null) {
try {
publication.cleanupBackup();
} catch (IOException exception) {
IrisLogging.reportError(
"Pack '" + prepared.key() + "' was published, but its transaction backup could not be cleaned.",
exception
);
try (AtomicDirectoryPublisher.Publication publication = AtomicDirectoryPublisher.publish(staging, target)) {
if (retainedBackup != null) {
publication.retainBackup(retainedBackup);
}
publication.commit();
if (retainedBackup == null) {
try {
publication.cleanupBackup();
} catch (IOException exception) {
IrisLogging.reportError(
"Pack '" + prepared.key() + "' was published, but its transaction backup could not be cleaned.",
exception
);
}
}
}
}
@@ -763,6 +767,11 @@ public final class PackDownloader {
if (Files.size(source) > safety.maxArchiveBytes()) {
throw new IOException("Pack archive exceeds the compressed size limit.");
}
try (ZipFile directory = new ZipFile(source.toFile())) {
if (directory.size() > safety.maxEntries()) {
throw new IOException("Pack archive contains too many entries.");
}
}
if (Files.exists(root, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Pack extraction target is not a directory: " + root);
}
@@ -0,0 +1,305 @@
package art.arcane.iris.pack;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
public final class PackFingerprints {
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
private PackFingerprints() {
}
public static String computePackMetadataDigest(File packsDir) {
Path root = resolveFingerprintRoot(packsDir);
if (root == null) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
List<FingerprintEntry> entries = collectFingerprintEntries(root.toRealPath());
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
for (FingerprintEntry entry : entries) {
byte[] relativePath = entry.relativePath().getBytes(StandardCharsets.UTF_8);
updateDigestInt(digest, relativePath.length);
digest.update(relativePath);
updateDigestLong(digest, entry.size());
updateDigestLong(digest, entry.lastModifiedMillis());
}
return HexFormat.of().formatHex(digest.digest());
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
public static String computePackFingerprint(File packsDir) {
return computePackContentSnapshot(packsDir).content();
}
public static PackContentSnapshot computePackContentSnapshot(File packsDir) {
Path root = resolveFingerprintRoot(packsDir);
if (root == null) {
return new PackContentSnapshot("", Map.of());
}
try {
Path resolvedRoot = root.toRealPath();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
Map<String, MessageDigest> packDigests = new LinkedHashMap<>();
List<FingerprintEntry> entries = collectFingerprintEntries(resolvedRoot);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES];
for (FingerprintEntry entry : entries) {
MessageDigest packDigest = entry.packName() == null
? null
: packDigests.computeIfAbsent(entry.packName(), ignored -> newSha256Digest());
updateFingerprintEntry(digest, entry.relativePath(), entry.size());
if (packDigest != null) {
updateFingerprintEntry(packDigest, entry.packRelativePath(), entry.size());
}
long readBytes = 0L;
try (InputStream input = Files.newInputStream(
entry.source(),
StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS)) {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
if (packDigest != null) {
packDigest.update(buffer, 0, read);
}
readBytes += read;
}
}
}
if (readBytes != entry.size()) {
throw new IOException("Iris pack changed while fingerprinting: " + entry.source());
}
}
Map<String, String> packContents = new LinkedHashMap<>();
for (Map.Entry<String, MessageDigest> entry : packDigests.entrySet()) {
packContents.put(entry.getKey(), HexFormat.of().formatHex(entry.getValue().digest()));
}
return new PackContentSnapshot(
HexFormat.of().formatHex(digest.digest()),
Map.copyOf(packContents));
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
public static String computePackTreeFingerprint(File packDir) {
Path root = resolveFingerprintRoot(packDir);
if (root == null) {
return "";
}
try {
List<FingerprintEntry> entries = new ArrayList<>();
collectFingerprintTree(root.toRealPath(), "", null, entries);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES];
for (FingerprintEntry entry : entries) {
updateFingerprintEntry(digest, entry.relativePath(), entry.size());
long readBytes = 0L;
try (InputStream input = Files.newInputStream(
entry.source(),
StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS)) {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
readBytes += read;
}
}
}
if (readBytes != entry.size()) {
throw new IOException("Iris pack changed while fingerprinting: " + entry.source());
}
}
return HexFormat.of().formatHex(digest.digest());
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris pack at " + root, exception);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 not available", exception);
}
}
private static MessageDigest newSha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 not available", exception);
}
}
private static void updateFingerprintEntry(MessageDigest digest, String relativePath, long size) {
byte[] relativeBytes = relativePath.getBytes(StandardCharsets.UTF_8);
updateDigestInt(digest, relativeBytes.length);
digest.update(relativeBytes);
updateDigestLong(digest, size);
}
private static Path resolveFingerprintRoot(File packsDir) {
if (packsDir == null) {
return null;
}
Path root = packsDir.toPath().toAbsolutePath().normalize();
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
return null;
}
if (!Files.isDirectory(root)) {
if (Files.isSymbolicLink(root)) {
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
}
return null;
}
return root;
}
private static List<FingerprintEntry> collectFingerprintEntries(Path root) throws IOException {
List<FingerprintEntry> entries = new ArrayList<>();
try (Stream<Path> children = Files.list(root)) {
for (Path child : children.toList()) {
String childName = child.getFileName().toString();
if (PackDirectoryResolver.isHiddenName(childName) || isNonContentPackFile(childName)) {
continue;
}
if (Files.isSymbolicLink(child)) {
if (!Files.isDirectory(child)) {
throw new IOException("Iris pack fingerprint rejected symbolic link: " + child);
}
PackDirectoryResolver.requireSafePackTree(child.toFile());
collectFingerprintTree(child.toRealPath(), childName, childName, entries);
} else if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
collectFingerprintTree(child, childName, childName, entries);
} else if (Files.isRegularFile(child, LinkOption.NOFOLLOW_LINKS)) {
BasicFileAttributes attributes = Files.readAttributes(
child, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
entries.add(new FingerprintEntry(
child,
childName,
null,
null,
attributes.size(),
attributes.lastModifiedTime().toMillis()));
} else {
throw new IOException("Iris pack fingerprint rejected unsupported entry: " + child);
}
}
}
return entries;
}
private static void collectFingerprintTree(
Path treeRoot,
String logicalRoot,
String packName,
List<FingerprintEntry> entries
) throws IOException {
Files.walkFileTree(treeRoot, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
Path directory,
BasicFileAttributes attributes
) {
if (!directory.equals(treeRoot)
&& treeRoot.relativize(directory).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
String fileName = file.getFileName().toString();
if ((treeRoot.relativize(file).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(fileName))
|| isNonContentPackFile(fileName)) {
return FileVisitResult.CONTINUE;
}
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
throw new IOException("Iris pack fingerprint rejected symbolic link: " + file);
}
if (!attributes.isRegularFile()) {
throw new IOException("Iris pack fingerprint rejected unsupported entry: " + file);
}
String relative = treeRoot.relativize(file).toString().replace(File.separatorChar, '/');
String logicalRelative = logicalRoot.isEmpty() ? relative : logicalRoot + "/" + relative;
entries.add(new FingerprintEntry(
file,
logicalRelative,
packName,
packName == null ? null : relative,
attributes.size(),
attributes.lastModifiedTime().toMillis()));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException {
throw new IOException("Unable to inspect Iris pack entry: " + file, failure);
}
});
}
private static boolean isNonContentPackFile(String name) {
return name != null && (name.equals(".DS_Store") || name.endsWith(CODE_WORKSPACE_SUFFIX));
}
private record FingerprintEntry(
Path source,
String relativePath,
String packName,
String packRelativePath,
long size,
long lastModifiedMillis
) {
}
private static void updateDigestInt(MessageDigest digest, int value) {
for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
digest.update((byte) (value >>> shift));
}
}
private static void updateDigestLong(MessageDigest digest, long value) {
for (int shift = Long.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
digest.update((byte) (value >>> shift));
}
}
public record PackContentSnapshot(String content, Map<String, String> packContents) {
public PackContentSnapshot {
content = Objects.requireNonNullElse(content, "");
packContents = Map.copyOf(Objects.requireNonNullElse(packContents, Map.of()));
}
}
}
@@ -75,11 +75,15 @@ public final class PackResourceCleanup {
synchronized (packLock(lockPath)) {
try {
Path packRoot = requirePackRoot(packFolder);
CleanupScan scan = scanCleanup(packRoot);
if (scan.paths().isEmpty()) {
return new ApplyResult(null, List.of(), null);
try (PackValidationRegistry.RootMutation mutation =
PackValidationRegistry.beginRootMutation(packRoot)) {
CleanupScan scan = scanCleanup(packRoot);
if (scan.paths().isEmpty()) {
return new ApplyResult(null, List.of(), null);
}
PackValidationRegistry.remove(packFolder.getName());
return quarantine(packRoot, scan.paths());
}
return quarantine(packRoot, scan.paths());
} catch (IOException | RuntimeException e) {
return new ApplyResult(null, List.of(), errorMessage("Unable to quarantine pack resources", e));
}
@@ -110,14 +114,18 @@ public final class PackResourceCleanup {
synchronized (packLock(lockPath)) {
try {
Path packRoot = requirePackRoot(packFolder);
RestoreScan scan = scanRestore(packRoot);
if (scan.dump() == null) {
return new RestoreResult(null, List.of(), List.of(), null);
try (PackValidationRegistry.RootMutation mutation =
PackValidationRegistry.beginRootMutation(packRoot)) {
RestoreScan scan = scanRestore(packRoot);
if (scan.dump() == null) {
return new RestoreResult(null, List.of(), List.of(), null);
}
if (!scan.conflicts().isEmpty()) {
return new RestoreResult(scan.dumpPath(), List.of(), scan.conflicts(), null);
}
PackValidationRegistry.remove(packFolder.getName());
return restore(packRoot, scan);
}
if (!scan.conflicts().isEmpty()) {
return new RestoreResult(scan.dumpPath(), List.of(), scan.conflicts(), null);
}
return restore(packRoot, scan);
} catch (IOException | RuntimeException e) {
return new RestoreResult(null, List.of(), List.of(), errorMessage("Unable to restore quarantined resources", e));
}
@@ -1,6 +1,5 @@
package art.arcane.iris.pack;
import art.arcane.iris.pack.datapack.ServerConfigurator;
import art.arcane.iris.pack.validation.CompatAction;
import art.arcane.iris.pack.validation.CompatFinding;
import art.arcane.iris.pack.validation.CompatRegistry;
@@ -41,7 +40,7 @@ public final class PackValidationCache {
}
public static String contentFingerprint(File packsRoot) {
return ServerConfigurator.computePackFingerprint(packsRoot);
return PackFingerprints.computePackFingerprint(packsRoot);
}
public static String contextFingerprint() {
@@ -115,6 +115,11 @@ public final class PackValidationRegistry {
}
public static boolean publishIfCurrent(ValidationTicket ticket, PackValidationResult result) {
return publishIfCurrent(ticket, result, "", "");
}
public static boolean publishIfCurrent(ValidationTicket ticket, PackValidationResult result,
String contentFingerprint, String contextFingerprint) {
if (ticket == null || result == null) {
return false;
}
@@ -129,7 +134,9 @@ public final class PackValidationRegistry {
return new RootState(
current.generation(),
false,
new RootValidation(result, "", ""));
new RootValidation(result,
Objects.requireNonNull(contentFingerprint, "Content fingerprint"),
Objects.requireNonNull(contextFingerprint, "Context fingerprint")));
});
return published.get();
}
@@ -321,6 +328,18 @@ public final class PackValidationRegistry {
"");
}
public synchronized void stageValidatedSnapshot(
PackValidationResult result,
String contentFingerprint,
String contextFingerprint
) {
requireOpen();
pendingValidation = new RootValidation(
Objects.requireNonNull(result, "Pack validation result"),
Objects.requireNonNull(contentFingerprint, "Content fingerprint"),
Objects.requireNonNull(contextFingerprint, "Context fingerprint"));
}
public synchronized void commit() {
requireOpen();
if (pendingValidation == null) {
@@ -41,6 +41,7 @@ import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@@ -205,12 +206,15 @@ public final class PackValidator {
return findings;
} catch (Throwable e) {
IrisLogging.reportError("Version-content gating failed for pack '" + packFolder.getName() + "'", e);
blockingErrors.add("Version-content gating failed with " + e.getClass().getSimpleName()
+ ": " + Objects.requireNonNullElse(e.getMessage(), "No failure detail."));
return List.of();
} finally {
if (data != null) {
try {
data.close();
} catch (Throwable ignored) {
} catch (Throwable closeFailure) {
IrisLogging.reportError("Failed to close validation data for pack '" + packFolder.getName() + "'", closeFailure);
}
}
}
@@ -37,6 +37,7 @@ import art.arcane.iris.pack.datapack.IDataFixer;
import art.arcane.iris.pack.AtomicDirectoryPublisher;
import art.arcane.iris.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
@@ -57,27 +58,16 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -89,14 +79,11 @@ import java.util.stream.Stream;
import art.arcane.iris.localization.BukkitRuntimeMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.volmlib.util.localization.MessageArgument;
public class ServerConfigurator {
private static final Object DATAPACK_INSTALL_LOCK = new Object();
private static final String CODE_WORKSPACE_SUFFIX = ".code-workspace";
private static final String COMPILER_INPUT_FINGERPRINT_CACHE = "datapack-compiler-input-fingerprint";
static final String POST_COMPILE_RESTART_WARNING = "Iris installed updated datapack registry entries; "
+ "restart the server before creating worlds or opening Studios.";
private static final int FINGERPRINT_BUFFER_BYTES = 64 * 1024;
private static volatile boolean loadedDatapackRuntimeReady;
private static volatile String loadedDatapackCompilerInputFingerprint = "";
private static volatile Map<String, String> loadedDatapackRegistryRequirements = Map.of();
@@ -667,166 +654,14 @@ public class ServerConfigurator {
}
static PackFingerprint resolvePackFingerprint(File packsDir, String cachedMetadata, String cachedContent) {
String metadata = computePackMetadataDigest(packsDir);
String metadata = PackFingerprints.computePackMetadataDigest(packsDir);
if (!metadata.isEmpty()
&& metadata.equals(cachedMetadata)
&& cachedContent != null
&& !cachedContent.isEmpty()) {
return new PackFingerprint(metadata, cachedContent);
}
return new PackFingerprint(metadata, computePackFingerprint(packsDir));
}
public static String computePackMetadataDigest(File packsDir) {
Path root = resolveFingerprintRoot(packsDir);
if (root == null) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
List<FingerprintEntry> entries = collectFingerprintEntries(root.toRealPath());
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
for (FingerprintEntry entry : entries) {
byte[] relativePath = entry.relativePath().getBytes(StandardCharsets.UTF_8);
updateDigestInt(digest, relativePath.length);
digest.update(relativePath);
updateDigestLong(digest, entry.size());
updateDigestLong(digest, entry.lastModifiedMillis());
}
return HexFormat.of().formatHex(digest.digest());
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
public static String computePackFingerprint(File packsDir) {
return computePackContentSnapshot(packsDir).content();
}
public static PackContentSnapshot computePackContentSnapshot(File packsDir) {
Path root = resolveFingerprintRoot(packsDir);
if (root == null) {
return new PackContentSnapshot("", Map.of());
}
try {
Path resolvedRoot = root.toRealPath();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
Map<String, MessageDigest> packDigests = new LinkedHashMap<>();
List<FingerprintEntry> entries = collectFingerprintEntries(resolvedRoot);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES];
for (FingerprintEntry entry : entries) {
MessageDigest packDigest = entry.packName() == null
? null
: packDigests.computeIfAbsent(entry.packName(), ignored -> newSha256Digest());
updateFingerprintEntry(digest, entry.relativePath(), entry.size());
if (packDigest != null) {
updateFingerprintEntry(packDigest, entry.packRelativePath(), entry.size());
}
long readBytes = 0L;
try (InputStream input = Files.newInputStream(
entry.source(),
StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS)) {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
if (packDigest != null) {
packDigest.update(buffer, 0, read);
}
readBytes += read;
}
}
}
if (readBytes != entry.size()) {
throw new IOException("Iris pack changed while fingerprinting: " + entry.source());
}
}
Map<String, String> packContents = new LinkedHashMap<>();
for (Map.Entry<String, MessageDigest> entry : packDigests.entrySet()) {
packContents.put(entry.getKey(), HexFormat.of().formatHex(entry.getValue().digest()));
}
return new PackContentSnapshot(
HexFormat.of().formatHex(digest.digest()),
Map.copyOf(packContents));
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris packs at " + root, exception);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 not available", e);
}
}
public static String computePackTreeFingerprint(File packDir) {
Path root = resolveFingerprintRoot(packDir);
if (root == null) {
return "";
}
try {
List<FingerprintEntry> entries = new ArrayList<>();
collectFingerprintTree(root.toRealPath(), "", null, entries);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[FINGERPRINT_BUFFER_BYTES];
for (FingerprintEntry entry : entries) {
updateFingerprintEntry(digest, entry.relativePath(), entry.size());
long readBytes = 0L;
try (InputStream input = Files.newInputStream(
entry.source(),
StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS)) {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
readBytes += read;
}
}
}
if (readBytes != entry.size()) {
throw new IOException("Iris pack changed while fingerprinting: " + entry.source());
}
}
return HexFormat.of().formatHex(digest.digest());
} catch (IOException exception) {
throw new UncheckedIOException("Unable to fingerprint Iris pack at " + root, exception);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 not available", exception);
}
}
private static MessageDigest newSha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 not available", exception);
}
}
private static void updateFingerprintEntry(MessageDigest digest, String relativePath, long size) {
byte[] relativeBytes = relativePath.getBytes(StandardCharsets.UTF_8);
updateDigestInt(digest, relativeBytes.length);
digest.update(relativeBytes);
updateDigestLong(digest, size);
}
private static Path resolveFingerprintRoot(File packsDir) {
if (packsDir == null) {
return null;
}
Path root = packsDir.toPath().toAbsolutePath().normalize();
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
return null;
}
if (!Files.isDirectory(root)) {
if (Files.isSymbolicLink(root)) {
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
}
return null;
}
return root;
return new PackFingerprint(metadata, PackFingerprints.computePackFingerprint(packsDir));
}
private static FingerprintCache readFingerprintCache(Path cacheFile) {
@@ -894,132 +729,12 @@ public class ServerConfigurator {
}
}
private static List<FingerprintEntry> collectFingerprintEntries(Path root) throws IOException {
List<FingerprintEntry> entries = new ArrayList<>();
try (Stream<Path> children = Files.list(root)) {
for (Path child : children.toList()) {
String childName = child.getFileName().toString();
if (PackDirectoryResolver.isHiddenName(childName) || isNonContentPackFile(childName)) {
continue;
}
if (Files.isSymbolicLink(child)) {
if (!Files.isDirectory(child)) {
throw new IOException("Iris pack fingerprint rejected symbolic link: " + child);
}
PackDirectoryResolver.requireSafePackTree(child.toFile());
collectFingerprintTree(child.toRealPath(), childName, childName, entries);
} else if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
collectFingerprintTree(child, childName, childName, entries);
} else if (Files.isRegularFile(child, LinkOption.NOFOLLOW_LINKS)) {
BasicFileAttributes attributes = Files.readAttributes(
child, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
entries.add(new FingerprintEntry(
child,
childName,
null,
null,
attributes.size(),
attributes.lastModifiedTime().toMillis()));
} else {
throw new IOException("Iris pack fingerprint rejected unsupported entry: " + child);
}
}
}
return entries;
}
private static void collectFingerprintTree(
Path treeRoot,
String logicalRoot,
String packName,
List<FingerprintEntry> entries
) throws IOException {
Files.walkFileTree(treeRoot, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
Path directory,
BasicFileAttributes attributes
) {
if (!directory.equals(treeRoot)
&& treeRoot.relativize(directory).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
String fileName = file.getFileName().toString();
if ((treeRoot.relativize(file).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(fileName))
|| isNonContentPackFile(fileName)) {
return FileVisitResult.CONTINUE;
}
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
throw new IOException("Iris pack fingerprint rejected symbolic link: " + file);
}
if (!attributes.isRegularFile()) {
throw new IOException("Iris pack fingerprint rejected unsupported entry: " + file);
}
String relative = treeRoot.relativize(file).toString().replace(File.separatorChar, '/');
String logicalRelative = logicalRoot.isEmpty() ? relative : logicalRoot + "/" + relative;
entries.add(new FingerprintEntry(
file,
logicalRelative,
packName,
packName == null ? null : relative,
attributes.size(),
attributes.lastModifiedTime().toMillis()));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException {
throw new IOException("Unable to inspect Iris pack entry: " + file, failure);
}
});
}
private static boolean isNonContentPackFile(String name) {
return name != null && (name.equals(".DS_Store") || name.endsWith(CODE_WORKSPACE_SUFFIX));
}
private record FingerprintEntry(
Path source,
String relativePath,
String packName,
String packRelativePath,
long size,
long lastModifiedMillis
) {
}
record PackFingerprint(String metadata, String content) {
}
public record PackContentSnapshot(String content, Map<String, String> packContents) {
public PackContentSnapshot {
content = Objects.requireNonNullElse(content, "");
packContents = Map.copyOf(Objects.requireNonNullElse(packContents, Map.of()));
}
}
private record FingerprintCache(String content, String metadata) {
}
private static void updateDigestInt(MessageDigest digest, int value) {
for (int shift = Integer.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
digest.update((byte) (value >>> shift));
}
}
private static void updateDigestLong(MessageDigest digest, long value) {
for (int shift = Long.SIZE - Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
digest.update((byte) (value >>> shift));
}
}
public static File resolveDatapacksFolder(File worldFolder) {
File rootFolder = resolveWorldRootFolder(worldFolder);
return new File(rootFolder, "datapacks");
@@ -142,7 +142,7 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".png")) {
m.add(prefix + file.getName().replace(".png", ""));
m.add(prefix + file.getName().substring(0, file.getName().length() - ".png".length()));
} else if (file.isDirectory()) {
getPNGFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
@@ -867,29 +867,18 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public String toLoadKey(File f) {
if (f.getPath().startsWith(getDataFolder().getPath())) {
String[] full = f.getPath().split("\\Q" + File.separator + "\\E");
String[] df = getDataFolder().getPath().split("\\Q" + File.separator + "\\E");
StringBuilder g = new StringBuilder();
boolean m = true;
for (int i = 0; i < full.length; i++) {
if (i >= df.length) {
if (m) {
m = false;
continue;
}
g.append("/").append(full[i]);
}
Path root = getDataFolder().toPath().toAbsolutePath().normalize();
Path resource = f.toPath().toAbsolutePath().normalize();
if (resource.startsWith(root)) {
Path relative = root.relativize(resource);
if (relative.getNameCount() > 1) {
String key = relative.subpath(1, relative.getNameCount()).toString().replace(File.separatorChar, '/');
int extension = key.lastIndexOf('.');
return extension > key.lastIndexOf('/') ? key.substring(0, extension) : key;
}
return g.substring(1).split("\\Q.\\E")[0];
} else {
IrisLogging.error("Forign file from loader " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")");
}
IrisLogging.error("Failed to load " + f.getPath() + " (loader realm: " + getDataFolder().getPath() + ")");
return null;
}
@@ -976,7 +965,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
.filter(s -> s.endsWith(".json"))
.map(s -> s.substring(absPath.length() + 1))
.map(s -> s.replace("\\", "/"))
.map(s -> s.split("\\Q.\\E")[0])
.map(s -> s.substring(0, s.length() - ".json".length()))
.forEach(s -> l.add("snippet/" + s));
} catch (Throwable e) {
IrisLogging.reportError("Failed to scan Iris snippets in " + snippetFolder + ".", e);
@@ -83,7 +83,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
for (File file : listedFiles) {
if (file.isFile() && file.getName().endsWith(".mat")) {
m.add(prefix + file.getName().replace(".mat", ""));
m.add(prefix + file.getName().substring(0, file.getName().length() - ".mat".length()));
} else if (file.isDirectory()) {
findMatFiles(file, prefix + file.getName() + "/", m, visitedDirectories);
}
@@ -106,7 +106,7 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
String name = skipDirName ? "" : dir.getName() + "/";
for (File f : listedFiles) {
if (f.isFile() && f.getName().endsWith(ext)) {
paths.add(name + f.getName().replaceAll("\\Q" + ext + "\\E", ""));
paths.add(name + f.getName().substring(0, f.getName().length() - ext.length()));
} else if (f.isDirectory()) {
getFiles(f, ext, false, visitedDirectories).forEach(e -> paths.add(name + e));
}
@@ -397,7 +397,8 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
HashSet<String> m = new HashSet<>();
for (File i : files) {
for (File j : matchAllFiles(i, (f) -> f.getName().endsWith(".json"))) {
m.add(i.toURI().relativize(j.toURI()).getPath().replace(".json", ""));
String path = i.toURI().relativize(j.toURI()).getPath();
m.add(path.substring(0, path.length() - ".json".length()));
}
}
@@ -767,11 +768,6 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return loadCache.getSize();
}
@Override
public KCache<?, ?> getRawCache() {
return loadCache;
}
@Override
public long getMaxSize() {
return loadCache.getMaxSize();
@@ -831,7 +831,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
protected long loop() {
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine
if (initialEntryPending || activeEngine instanceof IrisEngine irisEngine
&& irisEngine.isGenerationCacheWarmPending()) {
return HOTLOAD_LOOP_DELAY_MS;
}
@@ -995,8 +995,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public synchronized void beginInitialEntry(boolean playerEntry) {
if (studio || closing || engine != null) {
throw new IllegalStateException("Initial world entry must begin before a normal generator initializes.");
if (closing || engine != null) {
throw new IllegalStateException("Initial world entry must begin before the generator initializes.");
}
initialEntryPending = true;
initialPlayerEntryPending = playerEntry;
@@ -1071,7 +1071,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void hotload() {
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
return;
}
@@ -1079,18 +1079,18 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private void hotloadFromWatcher() {
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
return;
}
CompletableFuture<Void> pending = new CompletableFuture<>();
watcherHotload.set(pending);
try {
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
if (initialEntryPending || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)
|| Thread.currentThread().isInterrupted()) {
pending.cancel(true);
} else {
J.a(() -> completeExclusiveControlFuture(loadLock, () -> {
if (shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
if (!initialEntryPending && shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
getEngine().hotload();
}
}, pending));
@@ -60,7 +60,7 @@ public class IrisAxisRotationClamp {
@DependsOn({"min", "max"})
@MinNumber(0)
@MaxNumber(360)
@Description("Iris spins the axis but not freely. For example an interval of 90 would mean 4 possible angles (right angles) degrees. \nSetting this to 0 means totally free rotation.\n\nNote that a lot of structures can have issues with non 90 degree intervals because the minecraft block resolution is so low.")
@Description("Iris spins the axis but not freely. For example an interval of 90 would mean 4 possible angles (right angles) degrees. \nA zero interval resolves to a one-degree rotation step. Unlimited rotation also resolves positive intervals below 1 to one degree.\n\nNote that a lot of structures can have issues with non 90 degree intervals because the minecraft block resolution is so low.")
private double interval = 0;
public void minMax(double fd) {
@@ -79,7 +79,7 @@ public class IrisAxisRotationClamp {
public double getRadians(int rng) {
if (forceLock) {
return Math.toRadians(Math.ceil(Math.abs((max % 360D))));
return Math.toRadians(max);
}
if (isUnlimited()) {
@@ -91,6 +91,7 @@ public class IrisAxisRotationClamp {
return Math.toRadians(max);
}
return Math.toRadians(M.clip((interval * (Math.ceil(Math.abs((rng % 360D) / interval)))) % 360D, Math.min(min, max), Math.max(min, max)));
double resolvedInterval = interval <= 0 ? 1 : interval;
return Math.toRadians(M.clip((resolvedInterval * (Math.ceil(Math.abs((rng % 360D) / resolvedInterval)))) % 360D, Math.min(min, max), Math.max(min, max)));
}
}
@@ -28,7 +28,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.jobs.Job;
import art.arcane.volmlib.util.collection.KList;
@@ -42,12 +41,22 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -128,79 +137,108 @@ public final class IrisObjectIO {
}
static void readLegacy(IrisObject self, InputStream in) throws IOException {
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
DataInputStream din = new DataInputStream(in);
self.w = din.readInt();
self.h = din.readInt();
self.d = din.readInt();
self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2);
int s = din.readInt();
for (int i = 0; i < s; i++) {
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
NativeBlockState data = resolvePaletteState(self, din.readUTF());
if (isExcludedObjectBlock(data)) {
continue;
DataInputStream input = new DataInputStream(in);
IrisObject parsed = readDimensions(input);
int blocks = requireNonnegativeCount(input.readInt(), "block");
for (int index = 0; index < blocks; index++) {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
NativeBlockState state = resolvePaletteState(self, input.readUTF());
if (!isExcludedObjectBlock(state)) {
parsed.blocks.put(position, state);
}
self.blocks.put(pos, data);
}
if (din.available() == 0)
return;
try {
int size = din.readInt();
for (int i = 0; i < size; i++) {
readTile(self, din);
int firstByte = input.read();
if (firstByte != -1) {
int tiles = requireNonnegativeCount((firstByte << 24)
| (input.readUnsignedByte() << 16)
| (input.readUnsignedByte() << 8)
| input.readUnsignedByte(), "tile");
for (int index = 0; index < tiles; index++) {
readTile(parsed, input);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
publishRead(self, parsed);
}
static void read(IrisObject self, InputStream in) throws Throwable {
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
DataInputStream din = new DataInputStream(in);
self.w = din.readInt();
self.h = din.readInt();
self.d = din.readInt();
if (!din.readUTF().equals("Iris V2 IOB;")) {
throw new HeaderException();
static void read(IrisObject self, InputStream in) throws IOException {
DataInputStream input = new DataInputStream(in);
IrisObject parsed = readDimensions(input);
if (!V2_HEADER.equals(input.readUTF())) {
throw new IOException("Invalid object header");
}
self.center = new Vector3i(self.w / 2, self.h / 2, self.d / 2);
int s = din.readShort();
int i;
int paletteSize = requireNonnegativeCount(input.readShort(), "palette");
KList<String> palette = new KList<>();
for (i = 0; i < s; i++) {
palette.add(din.readUTF());
for (int index = 0; index < paletteSize; index++) {
palette.add(input.readUTF());
}
// Resolve the palette once: B.getState per BLOCK was a registry lookup times the
// block count (tens of thousands) instead of times the palette size (hundreds).
NativeBlockState[] resolved = new NativeBlockState[palette.size()];
for (i = 0; i < resolved.length; i++) {
resolved[i] = resolvePaletteState(self, palette.get(i));
for (int index = 0; index < resolved.length; index++) {
resolved[index] = resolvePaletteState(self, palette.get(index));
}
s = din.readInt();
for (i = 0; i < s; i++) {
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
NativeBlockState data = resolved[din.readShort()];
if (isExcludedObjectBlock(data)) {
continue;
int blocks = requireNonnegativeCount(input.readInt(), "block");
for (int index = 0; index < blocks; index++) {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
int paletteIndex = input.readShort();
if (paletteIndex < 0 || paletteIndex >= resolved.length) {
throw new IOException("Invalid object palette index " + paletteIndex);
}
NativeBlockState state = resolved[paletteIndex];
if (!isExcludedObjectBlock(state)) {
parsed.blocks.put(position, state);
}
self.blocks.put(pos, data);
}
s = din.readInt();
int tiles = requireNonnegativeCount(input.readInt(), "tile");
for (int index = 0; index < tiles; index++) {
readTile(parsed, input);
}
publishRead(self, parsed);
}
for (i = 0; i < s; i++) {
readTile(self, din);
private static IrisObject readDimensions(DataInputStream input) throws IOException {
int width = input.readInt();
int height = input.readInt();
int depth = input.readInt();
requireValidDimensions(width, height, depth);
return new IrisObject(width, height, depth);
}
private static void requireValidDimensions(int width, int height, int depth) throws IOException {
if (width < 1 || height < 1 || depth < 1) {
throw new IOException("Invalid object dimensions " + width + "x" + height + "x" + depth);
}
}
private static int requireNonnegativeCount(int count, String kind) throws IOException {
if (count < 0) {
throw new IOException("Invalid object " + kind + " count " + count);
}
return count;
}
private static void publishRead(IrisObject self, IrisObject parsed) {
// A mid-file failure must not leave parsed entries merged into the previous object contents.
self.writeLock.lock();
try {
self.w = parsed.w;
self.h = parsed.h;
self.d = parsed.d;
self.center = parsed.center;
self.shrinkOffset = parsed.shrinkOffset;
self.blocks = parsed.blocks;
self.states = parsed.states;
self.smartBored = false;
self.smartBoreVariant = null;
self.aabb.reset();
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
} finally {
self.writeLock.unlock();
}
}
@@ -213,21 +251,30 @@ public final class IrisObjectIO {
}
static void read(IrisObject self, File file) throws IOException {
try (var fin = new BufferedInputStream(new FileInputStream(file))) {
read(self, fin);
} catch (Throwable e) {
if (!(e instanceof HeaderException))
IrisLogging.reportError(e);
// The V2 parse populates blocks/states incrementally; a mid-file failure must not
// leave those entries to be merged with the legacy parse of the same file.
self.blocks.clear();
self.states.clear();
try (var fin = new BufferedInputStream(new FileInputStream(file))) {
readLegacy(self, fin);
try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(file))) {
if (hasV2Header(input)) {
read(self, input);
} else {
readLegacy(self, input);
}
}
}
private static boolean hasV2Header(BufferedInputStream input) throws IOException {
input.mark(3 * Integer.BYTES + Short.BYTES + V2_HEADER.length());
try {
DataInputStream header = new DataInputStream(input);
header.readInt();
header.readInt();
header.readInt();
int length = header.readUnsignedShort();
return length == V2_HEADER.length()
&& V2_HEADER.equals(new String(header.readNBytes(length), StandardCharsets.US_ASCII));
} finally {
input.reset();
}
}
/**
* Palette key to state. A key the server does not have goes through the pack's dimension {@code blockFallbacks}
* before the plain lookup, so a declared fallback actually reaches the world instead of becoming air. Present
@@ -276,15 +323,16 @@ public final class IrisObjectIO {
* write path now rejects them with a descriptive error before any byte is written.
*/
static void validateWritable(IrisObject self) throws IOException {
requireValidDimensions(self.w, self.h, self.d);
Palette palette = buildPalette(self);
if (palette.size() > MAX_PALETTE_ENTRIES) {
throw new IOException("Object '" + self.getLoadKey() + "' has " + palette.size()
+ " distinct block states; the .iob format supports at most " + MAX_PALETTE_ENTRIES + ".");
}
for (var entry : self.blocks) {
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
requireShortCoordinates(self, "block", entry.getKey());
}
for (var entry : self.states) {
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
requireShortCoordinates(self, "tile", entry.getKey());
}
}
@@ -345,9 +393,8 @@ public final class IrisObjectIO {
}
private static void writeValidated(IrisObject self, OutputStream o, VolmitSender sender) throws IOException {
AtomicReference<IOException> ref = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
new Job() {
AtomicReference<Throwable> ref = new AtomicReference<>();
CompletableFuture<Void> completion = new Job() {
private volatile int total = self.blocks.size() * 3 + self.states.size();
private volatile int c = 0;
@@ -385,10 +432,8 @@ public final class IrisObjectIO {
writeState(dos, entry);
++c;
}
} catch (IOException e) {
ref.set(e);
} finally {
latch.countDown();
} catch (Throwable failure) {
ref.set(failure);
}
}
@@ -407,13 +452,26 @@ public final class IrisObjectIO {
}.execute(sender, true, () -> {});
try {
latch.await();
completion.get();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while writing object", interrupted);
} catch (ExecutionException rejected) {
ref.compareAndSet(null, rejected.getCause());
}
Throwable failure = ref.get();
if (failure instanceof IOException ioFailure) {
throw ioFailure;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
if (failure instanceof Error error) {
throw error;
}
if (failure != null) {
throw new IOException("Object save work failed", failure);
}
if (ref.get() != null)
throw ref.get();
}
private static void writeHeader(IrisObject self, DataOutputStream output) throws IOException {
@@ -446,12 +504,9 @@ public final class IrisObjectIO {
return;
}
// Validate before opening the stream: FileOutputStream truncates, and a rejected
// object must leave the existing .iob untouched.
// Validate before staging output: a rejected object must leave the existing .iob untouched.
validateWritable(self);
try (FileOutputStream out = new FileOutputStream(file)) {
writeValidated(self, out);
}
writeAtomically(file, out -> writeValidated(self, out));
}
static void write(IrisObject self, File file, VolmitSender sender) throws IOException {
@@ -460,15 +515,49 @@ public final class IrisObjectIO {
}
validateWritable(self);
try (FileOutputStream out = new FileOutputStream(file)) {
writeValidated(self, out, sender);
writeAtomically(file, out -> writeValidated(self, out, sender));
}
private static void writeAtomically(File file, ObjectWriter writer) throws IOException {
Path target = resolveWriteTarget(file);
Path parent = target.getParent();
boolean posix = Files.getFileStore(parent).supportsFileAttributeView(PosixFileAttributeView.class);
Set<PosixFilePermission> permissions = posix && Files.exists(target)
? Files.getPosixFilePermissions(target) : null;
Path temporary = posix
? Files.createTempFile(parent, ".iris-object-", ".tmp",
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-rw-")))
: Files.createTempFile(parent, ".iris-object-", ".tmp");
try {
if (permissions != null) {
Files.setPosixFilePermissions(temporary, permissions);
}
try (FileOutputStream output = new FileOutputStream(temporary.toFile())) {
writer.write(output);
}
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(temporary);
}
}
private static class HeaderException extends IOException {
public HeaderException() {
super("Invalid Header");
private static Path resolveWriteTarget(File file) throws IOException {
Path target = file.toPath().toAbsolutePath();
target = target.getParent().toRealPath().resolve(target.getFileName());
Set<Path> visited = new HashSet<>();
while (Files.isSymbolicLink(target)) {
if (!visited.add(target)) {
throw new IOException("Object destination contains a symbolic-link cycle: " + file);
}
Path link = Files.readSymbolicLink(target);
target = link.isAbsolute() ? link : target.getParent().resolve(link);
target = target.getParent().toRealPath().resolve(target.getFileName());
}
return target;
}
private interface ObjectWriter {
void write(OutputStream output) throws IOException;
}
private static final class Palette {
@@ -622,7 +622,7 @@ final class IrisObjectPlacementRunner {
continue;
}
data = IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate);
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz);
@@ -822,7 +822,7 @@ final class IrisObjectPlacementRunner {
}
}
}
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
}
} else {
int scan = 0;
@@ -844,7 +844,7 @@ final class IrisObjectPlacementRunner {
}
}
}
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
}
}
continue;
@@ -892,7 +892,7 @@ final class IrisObjectPlacementRunner {
if (B.isVineBlock(d)) {
d = attachVineFaces(placer, d, xx, j, zz);
}
placer.set(xx, j, zz, IrisProceduralBlocks.normalizeWaterlogging(d, placer.get(xx, j, zz), waterlogCandidate));
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
}
}
@@ -930,6 +930,14 @@ final class IrisObjectPlacementRunner {
return !wouldReplace && (rawStructurePiece || !air);
}
private static NativeBlockState normalizePlacedWaterlogging(NativeBlockState state, IObjectPlacer placer,
int x, int y, int z, boolean enabled) {
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
return state;
}
return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled);
}
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
CNG warp, double half, int x, int y, int z) {
KList<IrisBlockVector> offsets = new KList<>();
@@ -33,12 +33,10 @@ import art.arcane.volmlib.util.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
@@ -57,22 +55,20 @@ final class IrisObjectTransforms {
self.writeLock.lock();
try {
VectorMap<NativeBlockState> d = new VectorMap<>();
Set<IrisBlockVector> omitted = new HashSet<>();
VectorMap<TileData> dx = new VectorMap<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
if (rotated == null) {
omitted.add(entry.getKey());
continue;
}
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), rotated);
}
VectorMap<TileData> dx = new VectorMap<>();
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
if (!omitted.contains(entry.getKey())) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
d.put(destination, rotated);
TileData tile = self.states.get(entry.getKey());
if (tile == null) {
dx.remove(destination);
} else {
dx.put(destination, tile);
}
}
@@ -91,9 +91,10 @@ public final class StructureCaveAnchorResolver {
}
List<Integer> anchors = new ArrayList<>();
int step = Math.max(1, scanStep);
IntPredicate carved = y -> isCarved(engine, blockX, y, blockZ);
for (int worldY = minimumY; worldY <= maximumY; worldY += step) {
int mantleY = toMantleY(worldY, engine.getMinHeight());
if (!matchesGeometry(engine, mode, blockX, mantleY, blockZ, clearance)) {
if (!matchesGeometry(carved, mode, mantleY, clearance)) {
continue;
}
if (!matchesAnchorFluid(engine, placement, blockX, mantleY, blockZ)) {
@@ -172,22 +173,6 @@ public final class StructureCaveAnchorResolver {
}
}
static boolean matchesGeometry(
Engine engine,
IrisStructureAnchorMode mode,
int blockX,
int mantleY,
int blockZ,
int clearance
) {
IntPredicate carved = y -> isCarved(engine, blockX, y, blockZ);
if (mode == IrisStructureAnchorMode.CAVE_CENTER) {
return matchesCenterGeometry(
carved, mantleY, Math.max(1, clearance), 0, engine.getHeight() - 1);
}
return matchesGeometry(carved, mode, mantleY, clearance);
}
static boolean matchesGeometry(
IntPredicate carved,
IrisStructureAnchorMode mode,
@@ -289,7 +274,11 @@ public final class StructureCaveAnchorResolver {
int blockZ
) {
HydrologyCaveCell hydrology = hydrologyAt(engine, blockX, mantleY, blockZ);
MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ);
if (hydrology != null && hydrology.protectsPlacement()) {
return false;
}
MatterCavern cavern = engine.getMantle().getMantle()
.get(blockX, mantleY, blockZ, MatterCavern.class);
return acceptsAnchorFluid(
placement.isUnderwater(),
cavern,
@@ -18,6 +18,8 @@
package art.arcane.iris.studio;
import art.arcane.iris.pack.PackFingerprints;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.spi.IrisLogging;
@@ -37,6 +39,7 @@ import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.pack.PackDownloadExecution;
import art.arcane.iris.pack.PackDownloader;
import art.arcane.iris.pack.PackValidationRegistry;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.studio.workspace.IrisProject;
@@ -400,7 +403,7 @@ public class StudioSVC implements IrisService {
}
static IrisDimension loadInstalledDimension(Path packRoot, Path validatedSource, String dimensionKey) throws IOException {
String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(packRoot.toFile());
String copiedFingerprint = PackFingerprints.computePackTreeFingerprint(packRoot.toFile());
PackValidationResult validation = validatePublishedPack(packRoot, validatedSource, copiedFingerprint);
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
@@ -468,7 +471,7 @@ public class StudioSVC implements IrisService {
requireSafePublicationTarget(target, replaceExisting);
publication = AtomicDirectoryPublisher.publish(stage, target);
stage = null;
String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(target.toFile());
String copiedFingerprint = PackFingerprints.computePackTreeFingerprint(target.toFile());
PackValidationResult publishedValidation =
validatePublishedPack(target, source, copiedFingerprint, validationMutation);
if (!publishedValidation.isLoadable()) {
@@ -559,17 +562,40 @@ public class StudioSVC implements IrisService {
static PackValidationResult validatePublishedPack(Path packRoot) {
try (PackValidationRegistry.RootMutation mutation =
PackValidationRegistry.beginRootMutation(packRoot)) {
PackValidationResult result = PackValidator.validate(packRoot.toFile());
mutation.stage(result);
mutation.commit();
return PackValidationRegistry.requireLoadable(packRoot);
return validateAndPublishSnapshot(packRoot,
PackFingerprints.computePackTreeFingerprint(packRoot.toFile()), mutation);
}
}
private static PackValidationResult validatePublishedPack(Path packRoot, String contentFingerprint) {
try (PackValidationRegistry.RootMutation mutation =
PackValidationRegistry.beginRootMutation(packRoot)) {
return validateAndPublishSnapshot(packRoot, contentFingerprint, mutation);
}
}
private static PackValidationResult validateAndPublishSnapshot(
Path packRoot,
String contentFingerprint,
PackValidationRegistry.RootMutation mutation
) {
String contextFingerprint = PackValidationCache.contextFingerprint();
PackValidationResult result = PackValidator.validate(packRoot.toFile());
if (!contentFingerprint.equals(PackFingerprints.computePackTreeFingerprint(packRoot.toFile()))) {
throw new IllegalStateException("Iris pack changed during semantic validation: " + packRoot);
}
if (!contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
throw new IllegalStateException("Iris validation registry context changed during semantic validation: " + packRoot);
}
mutation.stageValidatedSnapshot(result, contentFingerprint, contextFingerprint);
mutation.commit();
return PackValidationRegistry.requireLoadable(packRoot);
}
static PackValidationResult validateGenerationCandidate(Path packRoot, String contentFingerprint) {
PackValidationResult validation = PackValidationRegistry.getMatching(packRoot, contentFingerprint);
if (validation == null) {
return validatePublishedPack(packRoot);
return validatePublishedPack(packRoot, contentFingerprint);
}
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
@@ -247,8 +247,18 @@ public final class StudioOpenCoordinator {
IrisLogging.debug("Studio open: " + world.getName() + " ready in "
+ elapsedMillis(openStart) + "ms");
provider.completeInitialEntry();
future.complete(new StudioOpenResult(world, entryLocation));
} catch (Throwable e) {
if (provider != null) {
try {
provider.completeInitialEntry();
} catch (Throwable completionFailure) {
if (completionFailure != e) {
e.addSuppressed(completionFailure);
}
}
}
if (entryBootstrap != null) {
entryBootstrap.handle((ignored, failure) -> null).join();
}
@@ -440,12 +450,18 @@ public final class StudioOpenCoordinator {
));
}
return closeFuture.whenComplete((result, throwable) -> {
if (throwable == null && result.failureCause() == null) {
Throwable failure = throwable != null ? unwrapFailure(throwable)
: result == null ? new IllegalStateException("Studio cleanup completed without a result.")
: result.failureCause();
if (failure == null) {
lease.close();
} else {
IrisLogging.error("Studio cleanup remains incomplete for \"" + operationTarget
+ "\". Further Iris world operations are blocked. Resolve the reported failure "
+ "and restart the server manually before opening another world.");
IrisLogging.reportError("Studio cleanup remains incomplete for \"" + operationTarget
+ "\" (unloadCompletedLive=" + (result != null && result.unloadCompletedLive())
+ ", folderDeletionCompletedLive=" + (result != null && result.folderDeletionCompletedLive())
+ ", startupCleanupQueued=" + (result != null && result.startupCleanupQueued())
+ "). Further Iris world operations are blocked. Resolve the reported failure "
+ "and restart the server manually before opening another world.", failure);
}
}).copy();
}
@@ -71,6 +71,7 @@ import java.nio.file.Files;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
@@ -78,6 +79,7 @@ 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.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
@@ -244,6 +246,7 @@ public class IrisCreator {
World world = null;
boolean bukkitRegistered = false;
PlatformChunkGenerator stagedGenerator = null;
boolean studioEntryTransferred = false;
try {
reportStudioProgress(0.08D, "resolve_dimension");
reportStudioProgress(0.16D, "prepare_world_pack");
@@ -309,7 +312,7 @@ public class IrisCreator {
throw new IrisException("Access is null. Something bad happened.");
}
stagedGenerator = access;
if (!studio && !benchmark) {
if (!benchmark) {
access.beginInitialEntry(sender != null && sender.isPlayer());
}
AtomicInteger createProgressTask = startCreateProgressReporter(access, done, creationReporter);
@@ -386,7 +389,9 @@ public class IrisCreator {
}
reportCreationProgress(creationReporter, 0.92D, "teleport_player");
awaitSenderTeleport(world);
access.completeInitialEntry();
if (!studio) {
access.completeInitialEntry();
}
if (pregen != null) {
CompletableFuture<Boolean> ff = new CompletableFuture<>();
@@ -415,6 +420,7 @@ public class IrisCreator {
}
}
reportCreationProgress(creationReporter, 0.99D, "finalize");
studioEntryTransferred = studio;
return world;
} catch (Throwable failure) {
rollbackWorldCreation(worldKey, world, stagedGenerator, storageRoot, bukkitRegistered, failure);
@@ -423,9 +429,13 @@ public class IrisCreator {
}
throw new IrisException("Failed to create world \"" + name + "\".", failure);
} finally {
if (stagedGenerator != null) {
stagedGenerator.completeInitialEntry();
}
completeCreationEntry(stagedGenerator, studioEntryTransferred);
}
}
static void completeCreationEntry(PlatformChunkGenerator generator, boolean studioEntryTransferred) {
if (generator != null && !studioEntryTransferred) {
generator.completeInitialEntry();
}
}
@@ -452,15 +462,40 @@ public class IrisCreator {
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
return requiredRuntime.requestChunkAsync(requiredWorld, chunkX, chunkZ, true)
.thenCompose(chunk -> requiredRuntime.resolveSafeEntry(requiredWorld, entryAnchor))
CompletableFuture<Boolean> result = new CompletableFuture<>();
AtomicReference<CompletableFuture<Boolean>> activeTeleport = new AtomicReference<>();
result.whenComplete((ignored, failure) -> {
CompletableFuture<Boolean> teleport = activeTeleport.get();
if (result.isCancelled() && teleport != null) {
teleport.cancel(false);
}
});
requiredRuntime.requestChunkAsync(requiredWorld, chunkX, chunkZ, true)
.thenCompose(chunk -> result.isCancelled()
? CompletableFuture.failedFuture(new CancellationException())
: requiredRuntime.resolveSafeEntry(requiredWorld, entryAnchor))
.thenCompose(safeEntry -> {
if (result.isCancelled()) {
return CompletableFuture.failedFuture(new CancellationException());
}
if (safeEntry == null) {
return CompletableFuture.failedFuture(new IllegalStateException(
"Unable to resolve a safe entry for world \"" + requiredWorld.getName() + "\"."));
}
return requiredRuntime.teleport(requiredPlayer, safeEntry);
CompletableFuture<Boolean> teleport = requiredRuntime.teleport(requiredPlayer, safeEntry);
activeTeleport.set(teleport);
if (result.isCancelled()) {
teleport.cancel(false);
}
return teleport;
}).whenComplete((teleported, failure) -> {
if (failure == null) {
result.complete(teleported);
} else {
result.completeExceptionally(unwrapFailure(failure));
}
});
return result;
}
private void awaitSenderTeleport(World world) {
@@ -752,7 +787,12 @@ public class IrisCreator {
try {
stagedGenerator.closeAsync().get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (Throwable rollbackFailure) {
failure.addSuppressed(unwrapFailure(rollbackFailure));
Throwable cause = unwrapFailure(rollbackFailure);
failure.addSuppressed(cause);
safeToDelete = false;
if (cause instanceof TimeoutException) {
ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\".");
}
}
}
if (activeWorld != null) {
@@ -524,8 +524,8 @@ public class IrisToolbelt {
}
};
try {
if (!J.runEntity(player, teleportTask)) {
teleportTask.run();
if (!J.runEntity(player, teleportTask, 0, () -> evacuation.complete(false))) {
evacuation.complete(false);
}
} catch (Throwable failure) {
evacuation.completeExceptionally(failure);
@@ -1,6 +1,7 @@
package art.arcane.iris.world;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.pack.AtomicDirectoryPublisher;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@@ -18,14 +19,11 @@ import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream;
public final class IrisWorldStorage {
private static final String IRIS_NAMESPACE = "iris";
private static final String DEFAULT_LEVEL_NAME = "world";
private static final Set<String> OS_METADATA_FILES = Set.of(".DS_Store", "Thumbs.db", "desktop.ini");
private static final String APPLE_DOUBLE_PREFIX = "._";
/**
* Server#getLevelDirectory is Paper-API-only. Once a call throws NoSuchMethodError (plain
@@ -352,23 +350,12 @@ public final class IrisWorldStorage {
return true;
}
try (Stream<Path> entries = Files.list(irisRoot)) {
return entries.anyMatch(entry -> !isOperatingSystemMetadata(entry));
return entries.anyMatch(entry -> !AtomicDirectoryPublisher.isOperatingSystemMetadata(entry));
} catch (IOException unreadable) {
return true;
}
}
private static boolean isOperatingSystemMetadata(Path entry) {
if (!Files.isRegularFile(entry, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
Path name = entry.getFileName();
if (name == null) {
return false;
}
String fileName = name.toString();
return OS_METADATA_FILES.contains(fileName) || fileName.startsWith(APPLE_DOUBLE_PREFIX);
}
/**
* True when the level root holds at least one Iris-managed world directory. Used by the failure paths
@@ -4,6 +4,7 @@ import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public final class WorldUnloadBoundaryRegistry {
private static final ConcurrentHashMap<String, Boundary> ACTIVE = new ConcurrentHashMap<>();
@@ -13,7 +14,7 @@ public final class WorldUnloadBoundaryRegistry {
static Boundary begin(String worldIdentity) {
String requiredIdentity = Objects.requireNonNull(worldIdentity, "world identity");
Boundary boundary = new Boundary(requiredIdentity, new CompletableFuture<>());
Boundary boundary = new Boundary(requiredIdentity, new CompletableFuture<>(), new AtomicBoolean());
Boundary existing = ACTIVE.putIfAbsent(requiredIdentity, boundary);
if (existing != null) {
throw new IllegalStateException("World unload is already active for " + requiredIdentity + ".");
@@ -22,8 +23,8 @@ public final class WorldUnloadBoundaryRegistry {
}
public static CompletionStage<Boolean> claim(String worldIdentity) {
Boundary boundary = ACTIVE.remove(Objects.requireNonNull(worldIdentity, "world identity"));
return boundary == null ? null : boundary.completion();
Boundary boundary = ACTIVE.get(Objects.requireNonNull(worldIdentity, "world identity"));
return boundary == null || !boundary.claimed().compareAndSet(false, true) ? null : boundary.completion();
}
static void complete(Boundary boundary, Boolean unloaded, Throwable failure) {
@@ -36,6 +37,6 @@ public final class WorldUnloadBoundaryRegistry {
boundary.completion().completeExceptionally(WorldLifecycleSupport.unwrap(failure));
}
record Boundary(String worldIdentity, CompletableFuture<Boolean> completion) {
record Boundary(String worldIdentity, CompletableFuture<Boolean> completion, AtomicBoolean claimed) {
}
}
@@ -88,7 +88,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final String chunkAccessMode;
private final ChunkRequestExecutor executor;
private final Executor slowRequestExecutor;
private final Executor chunkIoExecutor;
private final PregenChunkFlush chunkIoFlush;
private final PregenSerialWorker chunkFlush;
private final PregenSerialWorker mantleCleanup;
private final PregenAdmissionGate admission;
@@ -179,7 +179,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
// Moonrise flush parks until the level's pending IO settles. Filling the pool with parked
// flushes left generation waiting on plate loads that could never run.
this.chunkFlush = new PregenSerialWorker("Iris Pregen Chunk Flush", world.getName());
this.chunkIoExecutor = chunkFlush.executor();
this.chunkIoFlush = new PregenChunkFlush(chunkFlush.executor(), () -> INMS.get().flushChunkIO(world));
this.mantleCleanup = new PregenSerialWorker("Iris Pregen Mantle Cleanup", world.getName());
this.slowRequestWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.urgent = false;
@@ -427,8 +427,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
private CompletableFuture<Void> flushChunkIOAsync() {
INMSBinding binding = INMS.get();
return CompletableFuture.runAsync(() -> binding.flushChunkIO(world), chunkIoExecutor);
return chunkIoFlush.request();
}
private CompletableFuture<Void> trackEviction(CompletableFuture<Void> eviction) {
@@ -0,0 +1,62 @@
package art.arcane.iris.world.pregen;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
final class PregenChunkFlush {
private final Executor executor;
private final Runnable flush;
private CompletableFuture<Void> pending;
private boolean scheduled;
PregenChunkFlush(Executor executor, Runnable flush) {
this.executor = Objects.requireNonNull(executor);
this.flush = Objects.requireNonNull(flush);
}
CompletableFuture<Void> request() {
CompletableFuture<Void> requested;
boolean submit;
synchronized (this) {
if (pending == null) {
pending = new CompletableFuture<>();
}
requested = pending;
submit = !scheduled;
scheduled = true;
}
if (submit) {
try {
executor.execute(this::drain);
} catch (RuntimeException | Error failure) {
synchronized (this) {
pending = null;
scheduled = false;
}
requested.completeExceptionally(failure);
}
}
return requested.copy();
}
private void drain() {
while (true) {
CompletableFuture<Void> batch;
synchronized (this) {
batch = pending;
if (batch == null) {
scheduled = false;
return;
}
pending = null;
}
try {
flush.run();
batch.complete(null);
} catch (Throwable failure) {
batch.completeExceptionally(failure);
}
}
}
}
@@ -303,12 +303,15 @@ public final class WorldRuntimeControlService {
return CompletableFuture.completedFuture(null);
}
int chunkX = source.getBlockX() >> 4;
int chunkZ = source.getBlockZ() >> 4;
Location destination = source.clone();
int chunkX = destination.getBlockX() >> 4;
int chunkZ = destination.getBlockZ() >> 4;
CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
future.complete(findTopSafeLocationWithTicket(world, source, chunkX, chunkZ));
if (!future.isDone()) {
future.complete(findTopSafeLocationWithTicket(world, destination, chunkX, chunkZ));
}
} catch (Throwable t) {
future.completeExceptionally(t);
}
@@ -369,6 +372,7 @@ public final class WorldRuntimeControlService {
return CompletableFuture.completedFuture(false);
}
Objects.requireNonNull(teleporter, "teleporter");
Location destination = location.clone();
CompletableFuture<Boolean> future = new CompletableFuture<>();
GameModeRestore modeRestore = new GameModeRestore(player);
@@ -395,7 +399,7 @@ public final class WorldRuntimeControlService {
return;
}
}
CompletableFuture<Boolean> teleportFuture = teleporter.teleport(player, location);
CompletableFuture<Boolean> teleportFuture = teleporter.teleport(player, destination);
if (teleportFuture == null) {
future.complete(false);
return;
@@ -425,7 +429,7 @@ public final class WorldRuntimeControlService {
} catch (Throwable t) {
future.completeExceptionally(t);
}
});
}, 0, () -> future.complete(false));
if (!scheduled) {
future.completeExceptionally(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
}
@@ -54,19 +54,19 @@ public interface Job {
}
default void execute(VolmitSender sender) {
execute(sender, () -> {
default CompletableFuture<Void> execute(VolmitSender sender) {
return execute(sender, () -> {
});
}
default void execute(VolmitSender sender, Runnable whenComplete) {
execute(sender, false, whenComplete);
default CompletableFuture<Void> execute(VolmitSender sender, Runnable whenComplete) {
return execute(sender, false, whenComplete);
}
default void execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) {
default CompletableFuture<Void> execute(VolmitSender sender, boolean silentMsg, Runnable whenComplete) {
PrecisionStopwatch p = PrecisionStopwatch.start();
CompletableFuture<?> f = J.afut(this::execute);
CompletableFuture<Void> f = J.afut(this::execute);
int c = J.ar(() -> {
if (sender.isPlayer()) {
sender.sendProgress(getProgress(), getName());
@@ -90,5 +90,6 @@ public interface Job {
}
whenComplete.run();
});
return f;
}
}