mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
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];
|
||||
|
||||
+64
-28
@@ -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) {
|
||||
|
||||
+1
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
-809
@@ -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);
|
||||
}
|
||||
}
|
||||
-133
@@ -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,
|
||||
|
||||
-21
@@ -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
|
||||
);
|
||||
}
|
||||
-61
@@ -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");
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+31
-18
@@ -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 {
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+31
-27
@@ -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) {
|
||||
|
||||
+1
-3
@@ -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();
|
||||
|
||||
+24
-2
@@ -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)
|
||||
|
||||
+20
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user