This commit is contained in:
Brian Neumann-Fopiano
2026-07-21 12:00:17 -04:00
parent d76dadec30
commit f6f06c1ab9
141 changed files with 10717 additions and 1725 deletions
@@ -45,6 +45,7 @@ public class IrisSettings {
private IrisSettingsPerformance performance = new IrisSettingsPerformance();
private IrisSettingsPregen pregen = new IrisSettingsPregen();
private IrisSettingsSentry sentry = new IrisSettingsSentry();
private IrisSettingsTreeFeller treeFeller = new IrisSettingsTreeFeller();
public static int getThreadCount(int c) {
return Math.max(switch (c) {
@@ -229,16 +230,8 @@ public class IrisSettings {
public int noiseCacheSize = 1_024;
public int resourceLoaderCacheSize = 1_024;
public int objectLoaderCacheSize = 4_096;
public int tectonicPlateSize = -1;
public int mantleCleanupDelay = 200;
public boolean simdKernels = true;
public int getTectonicPlateSize() {
if (tectonicPlateSize > 0)
return tectonicPlateSize;
return (int) (getHardware.getProcessMemory() / 512L);
}
}
@Data
@@ -286,6 +279,16 @@ public class IrisSettings {
public boolean preventLeafDecay = true;
}
@Data
public static class IrisSettingsTreeFeller {
public boolean enabled = false;
public int durabilityPreservationChance = 0;
public int getDurabilityPreservationChance() {
return Math.max(0, Math.min(durabilityPreservationChance, 100));
}
}
@Data
public static class IrisSettingsStudio {
public boolean studio = true;
@@ -300,9 +303,22 @@ public class IrisSettings {
public boolean useVirtualThreads = true;
public boolean forceMulticoreWrite = false;
public int priority = Thread.NORM_PRIORITY;
public int parallelism = -1;
public int getPriority() {
return Math.max(Math.min(priority, Thread.MAX_PRIORITY), Thread.MIN_PRIORITY);
}
public int getParallelism() {
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
if (parallelism > 0) {
int maximumParallelism = processors > Integer.MAX_VALUE / 2
? Integer.MAX_VALUE
: processors * 2;
return Math.min(parallelism, maximumParallelism);
}
return Math.max(1, (int) Math.ceil(Math.sqrt(processors)));
}
}
}
@@ -45,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class PregeneratorJob implements PregenListener, PregenRenderSource {
private static final long WORLD_SHUTDOWN_TIMEOUT_MILLIS = 15_000L;
private static final Color COLOR_EXISTS = parseColor("#4d7d5b");
private static final Color COLOR_BLACK = parseColor("#4d7d5b");
private static final Color COLOR_MANTLE = parseColor("#3c2773");
@@ -130,18 +131,36 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return true;
}
public static boolean shutdownInstanceForWorld(String worldIdentity) {
PregeneratorJob inst = instance.get();
if (inst == null || !inst.targetsWorldIdentity(worldIdentity)) {
return false;
}
return shutdownAndWait(inst, WORLD_SHUTDOWN_TIMEOUT_MILLIS);
}
public static boolean shutdownAndWait(long timeoutMs) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
return shutdownAndWait(inst, timeoutMs);
}
private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs) {
inst.pregenerator.close();
inst.worker.interrupt();
try {
inst.worker.join(timeoutMs);
inst.worker.join(Math.max(1L, timeoutMs));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while stopping the Iris pregenerator.", e);
}
if (inst.worker.isAlive()) {
throw new IllegalStateException("Timed out while stopping the Iris pregenerator after "
+ Math.max(1L, timeoutMs) + "ms.");
}
instance.compareAndSet(inst, null);
return true;
@@ -184,7 +203,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
}
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName) {
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName, String worldIdentity) {
}
public static PregenProgress progressSnapshot() {
@@ -205,7 +224,8 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
inst.lastMethod,
inst.paused(),
inst.pregenerator.getFailedChunks(),
inst.worldName());
inst.worldName(),
inst.worldIdentity());
}
public String worldName() {
@@ -216,6 +236,13 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return engine.getWorld().name();
}
public String worldIdentity() {
if (engine == null || engine.getWorld() == null) {
return null;
}
return engine.getWorld().identity();
}
public boolean targetsWorldIdentity(String worldIdentity) {
if (worldIdentity == null || engine == null || engine.getWorld() == null) {
return false;
@@ -65,7 +65,11 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.reflect.KeyedType;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.context.IrisContext;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -73,15 +77,20 @@ import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Data
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private static final KMap<File, IrisData> dataLoaders = new KMap<>();
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
private final File dataFolder;
private final int id;
private final boolean datapackCompiler;
@@ -108,14 +117,15 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private Gson snippetLoader;
private GsonBuilder builder;
private KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
private Engine engine;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>();
private IrisData(File dataFolder) {
this(dataFolder, false);
}
private IrisData(File dataFolder, boolean datapackCompiler) {
this.engine = null;
this.dataFolder = dataFolder;
this.id = RNG.r.imax();
this.datapackCompiler = datapackCompiler;
@@ -130,6 +140,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return dataLoaders.computeIfAbsent(dataFolder, IrisData::new);
}
public static IrisData openRuntime(File dataFolder) {
return new IrisData(dataFolder);
}
public static IrisData openDatapackCompiler(File dataFolder) {
return new IrisData(dataFolder, true);
}
@@ -275,9 +289,72 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
public void cleanupEngine() {
if (engine != null && engine.isClosed()) {
engine = null;
IrisLogging.debug("Dereferenced Data<Engine> " + getId() + " " + getDataFolder());
int removed;
synchronized (engines) {
int previousSize = engines.size();
removeClosedEngines();
removed = previousSize - engines.size();
}
if (removed > 0) {
IrisLogging.debug("Dereferenced " + removed + " Data<Engine> registration(s) " + getId() + " " + getDataFolder());
}
}
public Engine getEngine() {
IrisContext context = IrisContext.get();
if (context != null) {
Engine contextEngine = context.getEngine();
if (!contextEngine.isClosed() && contextEngine.getData() == this) {
return contextEngine;
}
}
synchronized (engines) {
removeClosedEngines();
return engines.size() == 1 ? engines.get(0) : null;
}
}
public List<Engine> getEngines() {
synchronized (engines) {
removeClosedEngines();
return List.copyOf(engines);
}
}
public void registerEngine(Engine engine) {
Objects.requireNonNull(engine, "engine");
synchronized (engines) {
for (Engine registeredEngine : engines) {
if (registeredEngine == engine) {
return;
}
}
engines.add(engine);
}
}
public void unregisterEngine(Engine engine) {
if (engine == null) {
return;
}
synchronized (engines) {
Iterator<Engine> iterator = engines.iterator();
while (iterator.hasNext()) {
if (iterator.next() == engine) {
iterator.remove();
return;
}
}
}
}
private void removeClosedEngines() {
Iterator<Engine> iterator = engines.iterator();
while (iterator.hasNext()) {
if (iterator.next().isClosed()) {
iterator.remove();
}
}
}
@@ -287,6 +364,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
public void close() {
closed = true;
dump();
synchronized (engines) {
engines.clear();
}
if (dataLoaders.get(dataFolder) == this) {
dataLoaders.remove(dataFolder);
}
@@ -365,7 +445,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
gson = builder.create();
if (engine != null) {
for (Engine engine : getEngines()) {
engine.hotload();
}
}
@@ -405,7 +485,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
invalidateLoader(structureLoader);
invalidateLoader(jigsawPoolLoader);
invalidateLoader(jigsawPieceLoader);
if (engine != null) {
for (Engine engine : getEngines()) {
IrisStructureLocator.invalidate(engine);
}
}
@@ -65,6 +65,16 @@ public final class MantleHeapPressure {
return false;
}
public static double reclaimUrgency(double fraction) {
if (!Double.isFinite(fraction) || fraction <= LOW_WATER) {
return 0D;
}
if (fraction >= HIGH_WATER) {
return 1D;
}
return (fraction - LOW_WATER) / (HIGH_WATER - LOW_WATER);
}
public static boolean overPanicWater() {
return usedFraction() >= PANIC_WATER;
}
@@ -0,0 +1,94 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
import art.arcane.iris.core.runtime.GoldenHashEngine;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public final class EngineMaintenance {
private EngineMaintenance() {
}
public static boolean isAvailable(Engine engine) {
return engine != null
&& !engine.isClosing()
&& !engine.isClosed()
&& !engine.getMantle().getMantle().isClosed();
}
public static boolean pregeneratorTargets(Engine engine) {
if (engine == null || engine.getWorld() == null) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(engine.getWorld().identity());
}
public static boolean shouldRun(Engine engine) {
if (engine.isStudio()
&& !IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
return false;
}
if (GoldenHashEngine.isActive()) {
return false;
}
return engine.getWorld() == null
|| !WorldMaintenance.isWorldMaintenanceActive(engine.getWorld().identity());
}
public static int workerParallelism() {
return IrisSettings.get().getPerformance().getEngineSVC().getParallelism();
}
public static Outcome run(Engine engine) {
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
double heapUsage = MantleHeapPressure.usedFraction();
Plan plan = plan(settings.getMantleKeepAlive(), heapUsage, settings.getEngineSVC().isForceMulticoreWrite());
engine.getMantle().trim(plan.idleDurationMillis());
long unloadStart = System.nanoTime();
int unloadedTectonicPlates = engine.getMantle().unloadTectonicPlate(
plan.multicoreUnload() ? 0 : Integer.MAX_VALUE);
if (plan.heapPressure() && MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
return new Outcome(
unloadedTectonicPlates,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - unloadStart));
}
public static boolean isMantleClosed(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String message = current.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
return true;
}
current = current.getCause();
}
return false;
}
static Plan plan(int mantleKeepAliveSeconds, double heapUsage, boolean forceMulticoreWrite) {
long baseIdleDurationMillis = TimeUnit.SECONDS.toMillis(Math.max(0, mantleKeepAliveSeconds));
double reclaimUrgency = MantleHeapPressure.reclaimUrgency(heapUsage);
long idleDurationMillis = Math.round(baseIdleDurationMillis * (1D - reclaimUrgency));
boolean heapPressure = reclaimUrgency >= 1D;
return new Plan(idleDurationMillis, heapPressure || forceMulticoreWrite, heapPressure);
}
public record Outcome(int unloadedTectonicPlates, long unloadDurationMillis) {
}
record Plan(long idleDurationMillis, boolean multicoreUnload, boolean heapPressure) {
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
@@ -59,6 +60,7 @@ import org.bukkit.event.world.StructureGrowEvent;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
public class TreeSVC implements IrisService {
@@ -138,6 +140,7 @@ public class TreeSVC implements IrisService {
saplingPlane.forEach(block -> block.setType(Material.AIR));
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r));
String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
List<BlockState> blockStateList = new KList<>();
KMap<Location, BlockData> dataCache = new KMap<>();
// TODO: REAL CLASSES!!!!
@@ -234,7 +237,7 @@ public class TreeSVC implements IrisService {
event.setCancelled(true);
J.s(() -> {
Runnable growTask = () -> {
StructureGrowEvent iGrow = new StructureGrowEvent(event.getLocation(), event.getSpecies(), event.isFromBonemeal(), event.getPlayer(), blockStateList);
block = true;
@@ -253,9 +256,20 @@ public class TreeSVC implements IrisService {
block.setBlockData(data.getBase(), false);
IrisServices.get(ExternalDataSVC.class).processUpdate(engine, block, data.getCustom());
} else block.setBlockData(d, false);
int mantleY = block.getY() - event.getWorld().getMinHeight();
engine.getMantle().getMantle().set(block.getX(), mantleY, block.getZ(), treeMarker);
engine.getMantle().getMantle().set(
block.getX(),
mantleY,
block.getZ(),
TreeBlockMaterial.of(block.getBlockData().getAsString())
);
}
}
});
};
if (!J.runAt(event.getLocation(), growTask) && !J.isFolia()) {
J.s(growTask);
}
}
/**
@@ -0,0 +1,6 @@
package art.arcane.iris.core.service.tree;
@FunctionalInterface
public interface BlockDropRouter {
boolean routeDrop(Object drop);
}
@@ -0,0 +1,97 @@
package art.arcane.iris.core.service.tree;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import java.util.HashSet;
import java.util.Set;
public final class TreeDefinitionIndex {
private static final String PROCEDURAL_TREE_PREFIX = "procedural/tree/";
private final Set<String> explicitObjectKeys;
private final Set<String> proceduralTreeKeys;
private TreeDefinitionIndex(Set<String> explicitObjectKeys, Set<String> proceduralTreeKeys) {
this.explicitObjectKeys = Set.copyOf(explicitObjectKeys);
this.proceduralTreeKeys = Set.copyOf(proceduralTreeKeys);
}
public static TreeDefinitionIndex build(Engine engine) {
Set<String> explicitObjectKeys = new HashSet<>();
Set<String> proceduralTreeKeys = new HashSet<>();
for (IrisRegion region : engine.getDimension().getAllRegions(engine)) {
if (region == null) {
continue;
}
collectPlacements(region.getObjects(), explicitObjectKeys);
collectProceduralTrees(region.getProceduralObjects(), proceduralTreeKeys);
}
for (IrisBiome biome : engine.getDimension().getReachableBiomes(engine)) {
if (biome == null) {
continue;
}
collectPlacements(biome.getObjects(), explicitObjectKeys);
collectProceduralTrees(biome.getProceduralObjects(), proceduralTreeKeys);
}
return new TreeDefinitionIndex(explicitObjectKeys, proceduralTreeKeys);
}
public boolean isTreeMarker(String marker) {
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded == null || decoded.structureAware()) {
return false;
}
String objectKey = decoded.objectKey();
if (objectKey.startsWith(PROCEDURAL_TREE_PREFIX)) {
return proceduralTreeKeys.contains(objectKey);
}
if (objectKey.startsWith("procedural/")) {
return false;
}
return objectKey.startsWith("trees/") || explicitObjectKeys.contains(objectKey);
}
private static void collectPlacements(Iterable<IrisObjectPlacement> placements, Set<String> objectKeys) {
if (placements == null) {
return;
}
for (IrisObjectPlacement placement : placements) {
if (placement == null
|| placement.getTrees() == null
|| placement.getTrees().isEmpty()
|| placement.getPlace() == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
if (objectKey != null && !objectKey.isBlank()) {
objectKeys.add(objectKey);
}
}
}
}
private static void collectProceduralTrees(IrisProceduralObjects proceduralObjects, Set<String> objectKeys) {
if (proceduralObjects == null || proceduralObjects.getTrees() == null) {
return;
}
for (IrisProceduralTree tree : proceduralObjects.getTrees()) {
if (tree == null || tree.getName() == null || tree.getName().isBlank()) {
continue;
}
int variants = Math.max(1, tree.getVariants());
for (int index = 0; index < variants; index++) {
objectKeys.add(tree.getVariantLoadKey(index));
}
}
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.core.service.tree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class TreeMarkerTraversal {
public static final int MAX_MEMBERS = 131_072;
public static final int MAX_VISITED = 1_000_000;
public static final int MAX_AXIS_DISTANCE = 256;
private TreeMarkerTraversal() {
}
public static Discovery discover(Position trigger, String marker, int minimumY, int maximumY, MarkerLookup lookup) {
ArrayDeque<Position> pending = new ArrayDeque<>();
Set<Position> visited = new HashSet<>();
List<Position> members = new ArrayList<>();
pending.add(trigger);
visited.add(trigger);
while (!pending.isEmpty()) {
Position current = pending.removeFirst();
if (!marker.equals(lookup.markerAt(current.x(), current.y(), current.z()))) {
continue;
}
if (members.size() >= MAX_MEMBERS) {
return new Discovery(List.copyOf(members), false);
}
members.add(current);
for (int xOffset = -1; xOffset <= 1; xOffset++) {
for (int yOffset = -1; yOffset <= 1; yOffset++) {
for (int zOffset = -1; zOffset <= 1; zOffset++) {
if (xOffset == 0 && yOffset == 0 && zOffset == 0) {
continue;
}
Position next = current.offset(xOffset, yOffset, zOffset);
if (next.y() < minimumY || next.y() >= maximumY) {
continue;
}
if (!withinAxisBounds(trigger, next)) {
if (marker.equals(lookup.markerAt(next.x(), next.y(), next.z()))) {
return new Discovery(List.copyOf(members), false);
}
continue;
}
if (!visited.add(next)) {
continue;
}
if (visited.size() > MAX_VISITED) {
return new Discovery(List.copyOf(members), false);
}
pending.addLast(next);
}
}
}
}
return new Discovery(List.copyOf(members), true);
}
private static boolean withinAxisBounds(Position trigger, Position position) {
return Math.abs(position.x() - trigger.x()) <= MAX_AXIS_DISTANCE
&& Math.abs(position.y() - trigger.y()) <= MAX_AXIS_DISTANCE
&& Math.abs(position.z() - trigger.z()) <= MAX_AXIS_DISTANCE;
}
@FunctionalInterface
public interface MarkerLookup {
String markerAt(int x, int y, int z);
}
public record Position(int x, int y, int z) {
public Position offset(int xOffset, int yOffset, int zOffset) {
return new Position(x + xOffset, y + yOffset, z + zOffset);
}
}
public record Discovery(List<Position> members, boolean complete) {
}
}
@@ -19,30 +19,87 @@
package art.arcane.iris.engine;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KMap;
public class EnginePanic {
private static final KMap<String, String> stuff = new KMap<>();
private static KMap<String, String> last = new KMap<>();
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public final class EnginePanic {
private static final Diagnostics GLOBAL = scoped("global");
private EnginePanic() {
}
public static Diagnostics scoped(String scope) {
return new Diagnostics(scope);
}
public static void add(String key, String value) {
stuff.put(key, value);
GLOBAL.add(key, value);
}
public static void saveLast() {
last = stuff.copy();
GLOBAL.saveLast();
}
public static void lastPanic() {
for (String i : last.keySet()) {
IrisLogging.error("Last Panic " + i + ": " + stuff.get(i));
}
GLOBAL.lastPanic();
}
public static void panic() {
lastPanic();
for (String i : stuff.keySet()) {
IrisLogging.error("Engine Panic " + i + ": " + stuff.get(i));
GLOBAL.panic();
}
public static final class Diagnostics {
private final String scope;
private final ThreadLocal<LinkedHashMap<String, String>> current = ThreadLocal.withInitial(LinkedHashMap::new);
private final AtomicReference<Map<String, String>> last = new AtomicReference<>(Map.of());
private Diagnostics(String scope) {
this.scope = scope == null || scope.isBlank() ? "unknown" : scope;
}
public void add(String key, String value) {
current.get().put(String.valueOf(key), String.valueOf(value));
}
public void saveLast() {
last.set(currentSnapshot());
current.remove();
}
public void lastPanic() {
log("Last Panic", last.get());
}
public void panic() {
Map<String, String> currentSnapshot = currentSnapshot();
lastPanic();
log("Engine Panic", currentSnapshot);
current.remove();
}
Map<String, String> currentSnapshot() {
return immutableSnapshot(current.get());
}
Map<String, String> lastSnapshot() {
return last.get();
}
private void log(String prefix, Map<String, String> snapshot) {
for (Map.Entry<String, String> entry : snapshot.entrySet()) {
IrisLogging.error(prefix + " [" + scope + "] " + entry.getKey() + ": " + entry.getValue());
}
}
private static Map<String, String> immutableSnapshot(Map<String, String> values) {
if (values.isEmpty()) {
return Map.of();
}
return Collections.unmodifiableMap(new LinkedHashMap<>(values));
}
}
}
@@ -48,7 +48,7 @@ public final class GenerationCacheWarmer {
KList<IrisBiome> biomes = engine.getAllBiomes();
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
for (IrisBiome biome : biomes) {
warmPlacements(biome.getObjects(), root, counter, data);
warmPlacements(biome.getObjects(), root, counter, data, engine);
warmDecorators(biome.getDecorators(), root, counter, data);
warmOres(biome.getOres(), root, counter, data);
warmProcedural(biome.getProceduralObjects(), root, counter, data);
@@ -57,7 +57,7 @@ public final class GenerationCacheWarmer {
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
for (IrisRegion region : regions) {
warmPlacements(region.getObjects(), root, counter, data);
warmPlacements(region.getObjects(), root, counter, data, engine);
warmOres(region.getOres(), root, counter, data);
warmProcedural(region.getProceduralObjects(), root, counter, data);
}
@@ -71,7 +71,8 @@ public final class GenerationCacheWarmer {
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
}
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter, IrisData data) {
private static void warmPlacements(KList<IrisObjectPlacement> placements, RNG root, int[] counter,
IrisData data, Engine engine) {
if (placements == null) {
return;
}
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
continue;
}
RNG rng = root.nextParallelRNG(counter[0]++);
placement.getSurfaceWarp(rng, data);
placement.getSurfaceWarp(rng, data, engine);
placement.getDensity(rng, 0, 0, data);
}
}
@@ -111,7 +111,7 @@ public class IrisComplex implements DataProvider {
public IrisComplex(Engine engine, boolean simple) {
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
IrisBiome emptyBiome = new IrisBiome();
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
this.rng = new RNG(engine.getSeedManager().getComplex());
this.data = engine.getData();
@@ -124,19 +124,23 @@ public class IrisComplex implements DataProvider {
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
if (focusBiome != null) {
focusBiome.setInferredType(InferredType.LAND);
focusBiome = focusBiome.withInferredType(InferredType.LAND);
focusRegion = findRegion(focusBiome, engine);
}
//@builder
if (focusRegion != null) {
prepareInferredBiomes(focusRegion);
focusRegion.getAllBiomes(this).forEach(this::registerGenerators);
} else {
engine.getDimension()
.getRegions()
.forEach(i -> data.getRegionLoader().load(i)
.getAllBiomes(this)
.forEach(this::registerGenerators));
engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
return;
}
prepareInferredBiomes(region);
region.getAllBiomes(this).forEach(this::registerGenerators);
});
}
generatorBounds = buildGeneratorBounds(engine);
KList<IrisShapedGeneratorStyle> overlayNoise = engine.getDimension().getOverlayNoise();
@@ -171,7 +175,7 @@ public class IrisComplex implements DataProvider {
-> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getCaveBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes()))
.selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE))
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
@@ -181,7 +185,7 @@ public class IrisComplex implements DataProvider {
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)))
.selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND))
).convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
inferredStreams.put(InferredType.LAND, landBiomeStream);
@@ -191,7 +195,7 @@ public class IrisComplex implements DataProvider {
.zoom(engine.getDimension().getBiomeZoom())
.zoom(engine.getDimension().getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)))
.selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA))
).convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
inferredStreams.put(InferredType.SEA, seaBiomeStream);
@@ -200,7 +204,7 @@ public class IrisComplex implements DataProvider {
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)))
.selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE))
).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream");
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
@@ -312,22 +316,49 @@ public class IrisComplex implements DataProvider {
}
private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) {
IrisBiome resolved = resolveSurfaceBiome(
height,
biome,
region,
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream);
return resolved == biome ? biome : implode(resolved, x, z);
}
static IrisBiome resolveSurfaceBiome(
double height,
IrisBiome biome,
IrisRegion region,
double x,
double z,
double fluidHeight,
ProceduralStream<IrisBiome> landBiomes,
ProceduralStream<IrisBiome> seaBiomes,
ProceduralStream<IrisBiome> shoreBiomes
) {
if (biome == null || region == null) {
return biome;
}
double sh = region.getShoreHeight(x, z);
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
return shoreBiomeStream.get(x, z);
return shoreBiomes.get(x, z);
}
if (height > fluidHeight + sh && !biome.isLand()) {
return landBiomeStream.get(x, z);
return landBiomes.get(x, z);
}
if (height < fluidHeight && !biome.isAquatic()) {
return seaBiomeStream.get(x, z);
return seaBiomes.get(x, z);
}
if (height == fluidHeight && !biome.isShore()) {
return shoreBiomeStream.get(x, z);
return shoreBiomes.get(x, z);
}
return biome;
@@ -446,6 +477,21 @@ public class IrisComplex implements DataProvider {
return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0);
}
private void prepareInferredBiomes(IrisRegion region) {
loadInferredBiomes(region.getLandBiomes(), InferredType.LAND);
loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE);
loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA);
loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE);
}
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
inferred.add(biome.withInferredType(type));
}
return inferred;
}
private void registerGenerators(IrisBiome biome) {
generatorBiomes.add(biome);
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
@@ -532,8 +578,7 @@ public class IrisComplex implements DataProvider {
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
IrisBiome biome = childSelectionPlan.select(childCell, x, z);
biome.setInferredType(b.getInferredType());
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
return implode(biome, x, z, max - 1);
}
@@ -640,7 +685,7 @@ public class IrisComplex implements DataProvider {
}
}
private static class ChildSelectionPlan {
static final class ChildSelectionPlan {
private final IrisBiome[] mappedBiomes;
private final int maxIndex;
@@ -649,7 +694,7 @@ public class IrisComplex implements DataProvider {
this.maxIndex = mappedBiomes.length - 1;
}
private static ChildSelectionPlan create(KList<IrisBiome> options) {
static ChildSelectionPlan create(KList<IrisBiome> options) {
if (options.isEmpty()) {
return new ChildSelectionPlan(new IrisBiome[0]);
}
@@ -690,7 +735,7 @@ public class IrisComplex implements DataProvider {
return new ChildSelectionPlan(mappedBiomes);
}
private IrisBiome select(CNG childCell, double x, double z) {
IrisBiome select(CNG childCell, double x, double z) {
if (mappedBiomes.length == 0) {
return null;
}
File diff suppressed because it is too large Load Diff
@@ -36,19 +36,23 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class IrisEngineEffects extends EngineAssignedComponent implements EngineEffects {
private static final long EFFECT_BUDGET_NANOS = 1_500_000L;
private static final long EMPTY_PLAYER_REFRESH_NANOS = 1_000_000_000L;
private final ConcurrentHashMap<UUID, EnginePlayer> players;
private final Semaphore limit;
private final AtomicBoolean playerMapUpdateQueued;
private final AtomicLong nextEmptyRefresh;
public IrisEngineEffects(Engine engine) {
super(engine, "FX");
players = new ConcurrentHashMap<>();
limit = new Semaphore(1);
playerMapUpdateQueued = new AtomicBoolean(false);
nextEmptyRefresh = new AtomicLong(0L);
}
@Override
@@ -93,7 +97,15 @@ public class IrisEngineEffects extends EngineAssignedComponent implements Engine
return;
}
try {
if (players.isEmpty() || M.r(0.02)) {
if (players.isEmpty()) {
long now = System.nanoTime();
long next = nextEmptyRefresh.get();
if (now >= next && nextEmptyRefresh.compareAndSet(next, now + EMPTY_PLAYER_REFRESH_NANOS)) {
updatePlayerMap();
}
return;
}
if (M.r(0.02)) {
updatePlayerMap();
return;
}
@@ -22,7 +22,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
@@ -66,7 +65,9 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
@Data
@EqualsAndHashCode(exclude = "engine")
@@ -182,8 +183,8 @@ public class IrisEngineMantle implements EngineMantle {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), "mantle");
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createRuntimeDataAdapter(engine.getData());
MantleHooks hooks = createRuntimeHooks();
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
createRegionIO(dataFolder, worldHeight, adapter, hooks);
return new Mantle<>(
@@ -199,14 +200,14 @@ public class IrisEngineMantle implements EngineMantle {
}
public static MantleDataAdapter<Matter> createRuntimeDataAdapter(IrisData data) {
return createDataAdapter(data);
return createDataAdapter(() -> data);
}
public static MantleHooks createRuntimeHooks() {
return createHooks();
return createHooks(EnginePanic.scoped("runtime mantle"));
}
private static MantleDataAdapter<Matter> createDataAdapter(IrisData data) {
private static MantleDataAdapter<Matter> createDataAdapter(Supplier<IrisData> dataSupplier) {
return new MantleDataAdapter<>() {
@Override
public Matter createSection() {
@@ -215,6 +216,7 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public Matter readSection(art.arcane.volmlib.util.io.CountingDataInputStream din) throws IOException {
IrisData data = Objects.requireNonNull(dataSupplier.get(), "Iris mantle data is unavailable.");
try (IrisMatterContext.Scope scope = IrisMatterContext.open(data)) {
return Matter.readDin(din);
}
@@ -285,11 +287,11 @@ public class IrisEngineMantle implements EngineMantle {
};
}
private static MantleHooks createHooks() {
private static MantleHooks createHooks(EnginePanic.Diagnostics panic) {
return new MantleHooks() {
@Override
public void onBeforeReadSection(int index) {
EnginePanic.add("read.section", "Section[" + index + "]");
panic.add("read.section", "Section[" + index + "]");
}
@Override
@@ -299,22 +301,22 @@ public class IrisEngineMantle implements EngineMantle {
art.arcane.volmlib.util.io.CountingDataInputStream din,
IOException error) {
IrisLogging.error("Failed to read chunk section, skipping it.");
EnginePanic.add("read.byte.range", start + " " + end);
EnginePanic.add("read.byte.current", din.count() + "");
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
EnginePanic.panic();
panic.panic();
TectonicPlate.addError();
}
@Override
public void onBeforeReadChunk(int index) {
EnginePanic.add("read-chunk", "Chunk[" + index + "]");
panic.add("read-chunk", "Chunk[" + index + "]");
}
@Override
public void onAfterReadChunk(int index) {
EnginePanic.saveLast();
panic.saveLast();
}
@Override
@@ -324,11 +326,11 @@ public class IrisEngineMantle implements EngineMantle {
art.arcane.volmlib.util.io.CountingDataInputStream din,
Throwable error) {
IrisLogging.error("Failed to read chunk, creating a new chunk instead.");
EnginePanic.add("read.byte.range", start + " " + end);
EnginePanic.add("read.byte.current", din.count() + "");
panic.add("read.byte.range", start + " " + end);
panic.add("read.byte.current", din.count() + "");
IrisLogging.reportError(error);
error.printStackTrace();
EnginePanic.panic();
panic.panic();
}
@Override
@@ -22,6 +22,7 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.service.tree.BlockDropRouter;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
@@ -82,9 +83,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
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.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -95,7 +99,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private static final int MAX_FORCED_CHUNK_UPDATES = 128;
private final Looper looper;
private final int id;
private final KList<Runnable> updateQueue = new KList<>();
private final ChronoLatch cl;
private final ChronoLatch clw;
@@ -111,10 +114,15 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private final Set<Long> chunkUpdateQueue = ConcurrentHashMap.newKeySet();
private final AtomicBoolean chunkUpdateScanScheduled = new AtomicBoolean();
private final AtomicBoolean chunkDiscoveryScanScheduled = new AtomicBoolean();
private int entityCount = 0;
private int actuallySpawned = 0;
private final AtomicBoolean entityCountWarningReported = new AtomicBoolean();
private final AtomicBoolean entityCountErrorReported = new AtomicBoolean();
private boolean looperStopped;
private boolean cleanupServiceStopped;
private volatile int entityCount = 0;
private final AtomicInteger actuallySpawned = new AtomicInteger();
private int cooldown = 0;
private int forcedChunkUpdateCursor = 0;
private volatile boolean entityCountValid = false;
private volatile boolean playersPresent = false;
private KSet<Position2> injectBiomes = new KSet<>();
private volatile int loadedChunkCount = 0;
@@ -128,7 +136,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
chunkUpdater = null;
chunkDiscovery = null;
cleanupService = null;
id = -1;
}
public IrisWorldManager(Engine engine) {
@@ -143,55 +150,82 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
id = engine.getCacheID();
looper = new Looper() {
@Override
protected long loop() {
if (getEngine().isClosed() || getEngine().getCacheID() != id) {
interrupt();
if (!isManagerStarted()) {
return -1L;
}
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
J.runGlobal(() -> BukkitWorldBinding.tryBind(getEngine().getWorld()));
}
if (getEngine().getWorld().hasPlatformWorld()) {
if (chunkUpdater.flip()) {
updateChunks();
}
if (!playersPresent) {
return 5000;
}
if (chunkDiscovery.flip()) {
discoverChunks();
}
if (cln.flip()) {
engine.getEngineData().cleanup(getEngine());
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem() && !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
return 3000;
}
onAsyncTick();
}
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
return callManagerTask(
"bukkit_world_manager_loop",
IrisWorldManager.this::runLoop,
250L);
}
};
looper.setPriority(Thread.MIN_PRIORITY);
looper.setName("Iris World Manager " + getTarget().getWorld().name());
}
public void startManager() {
private long runLoop() {
if (getEngine().isClosed()) {
looper.interrupt();
return -1L;
}
if (!getEngine().getWorld().hasPlatformWorld() && clw.flip()) {
J.runGlobal(() -> runManagerTask(
"bukkit_world_manager_bind",
() -> BukkitWorldBinding.tryBind(getEngine().getWorld())));
}
if (getEngine().getWorld().hasPlatformWorld()) {
if (chunkUpdater.flip()) {
updateChunks();
}
if (!playersPresent) {
return 5000L;
}
if (chunkDiscovery.flip()) {
discoverChunks();
}
if (cln.flip()) {
getEngine().getEngineData().cleanup(getEngine());
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()
&& !IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
return 3000L;
}
onAsyncTick();
}
return IrisSettings.get().getWorld().getAsyncTickIntervalMS();
}
@Override
public void start() {
super.start();
if (!looper.isAlive()) {
looper.start();
}
}
private Runnable managedTask(String operation, Runnable task) {
return () -> runManagerTask(operation, task);
}
private Runnable managedTask(String operation, Runnable task, Runnable unavailable) {
return () -> {
if (!runManagerTask(operation, task)) {
unavailable.run();
}
};
}
private void discoverChunks() {
World world = BukkitWorldBinding.world(getEngine().getWorld());
if (world == null) {
@@ -206,7 +240,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
boolean scheduled = J.runGlobal(() -> {
boolean scheduled = J.runGlobal(managedTask("bukkit_world_manager_discover_chunks", () -> {
try {
if (getEngine().isClosed() || !world.equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
return;
@@ -217,7 +251,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
continue;
}
J.runEntity(player, () -> {
J.runEntity(player, managedTask("bukkit_world_manager_discover_player", () -> {
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
@@ -232,14 +266,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
raiseDiscoveredChunkFlag(world, chunkX, chunkZ);
}
}
});
}));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
chunkDiscoveryScanScheduled.set(false);
}
});
}, () -> chunkDiscoveryScanScheduled.set(false)));
if (!scheduled) {
chunkDiscoveryScanScheduled.set(false);
}
@@ -260,7 +294,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_discovered_flag", () -> {
try {
Mantle<Matter> mantle = getMantle();
if (!mantle.hasFlag(chunkX, chunkZ, MantleFlag.DISCOVERED)) {
@@ -271,7 +305,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
} finally {
discoveredFlagQueue.remove(key);
}
});
}, () -> discoveredFlagQueue.remove(key)));
}
private void updateChunks() {
@@ -288,7 +322,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
boolean scheduled = J.runGlobal(() -> updateChunksOnGlobal(world));
boolean scheduled = J.runGlobal(managedTask(
"bukkit_world_manager_update_chunks",
() -> updateChunksOnGlobal(world),
() -> chunkUpdateScanScheduled.set(false)));
if (!scheduled) {
chunkUpdateScanScheduled.set(false);
}
@@ -308,7 +345,9 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
continue;
}
J.runEntity(player, () -> schedulePlayerChunkUpdates(world, player));
J.runEntity(player, managedTask(
"bukkit_world_manager_player_chunk_updates",
() -> schedulePlayerChunkUpdates(world, player)));
}
scheduleForcedChunkUpdates(world);
@@ -363,13 +402,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
try {
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
boolean scheduled = J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_chunk_update", () -> {
try {
updateChunkRegion(world, chunkX, chunkZ);
} finally {
chunkUpdateQueue.remove(key);
}
});
}, () -> chunkUpdateQueue.remove(key)));
if (!scheduled) {
chunkUpdateQueue.remove(key);
}
@@ -409,12 +448,12 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
int delay = RNG.r.i(5, 200);
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), true);
}, delay);
}), delay);
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
forEachMarkerSpawner(markerChunk, (block, spawners) -> {
@@ -442,7 +481,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_spawn_marker_flag", () -> {
boolean raised = false;
try {
Mantle<Matter> mantle = getMantle();
@@ -460,13 +499,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_spawn_marker_callback", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
onFirstRaise.run();
});
});
}));
}, () -> markerFlagQueue.remove(key)));
}
private void warmupMantleChunkAsync(int chunkX, int chunkZ) {
@@ -475,7 +514,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_mantle_warmup", () -> {
try {
getMantle().getChunk(chunkX, chunkZ);
} catch (Throwable e) {
@@ -483,11 +522,11 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
} finally {
mantleWarmupQueue.remove(key);
}
});
}, () -> mantleWarmupQueue.remove(key)));
}
private boolean onAsyncTick() {
if (getEngine().isClosed()) {
if (getEngine().isClosing() || getEngine().isClosed()) {
return false;
}
@@ -496,7 +535,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return false;
}
actuallySpawned = 0;
actuallySpawned.set(0);
if (!getEngine().getWorld().hasPlatformWorld()) {
IrisLogging.debug("Can't spawn. No real world");
@@ -509,8 +548,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
World realWorld = BukkitWorldBinding.world(getEngine().getWorld());
if (realWorld == null) {
entityCount = 0;
entityCountValid = false;
} else if (J.isFolia()) {
entityCount = getFoliaLivingEntityCount(realWorld);
Integer count = getFoliaLivingEntityCount(realWorld);
if (count != null) {
entityCount = count;
entityCountValid = true;
resetEntityCountFailures();
} else {
entityCountValid = false;
}
} else {
CompletableFuture<Integer> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
@@ -526,14 +573,32 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
future.completeExceptionally(ex);
}
});
entityCount = scheduled ? future.get(2, TimeUnit.SECONDS) : 0;
if (scheduled) {
entityCount = future.get(2, TimeUnit.SECONDS);
entityCountValid = true;
resetEntityCountFailures();
} else {
reportEntityCountFailure("Unable to schedule the global entity count; pausing Iris entity spawning until a complete count is available.", null);
}
}
} catch (InterruptedException e) {
entityCountValid = false;
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while counting entities; pausing Iris entity spawning until a complete count is available.", null);
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
} catch (Throwable e) {
IrisLogging.reportError(e);
close();
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
}
}
if (!entityCountValid) {
return false;
}
double epx = getEntitySaturation();
if (epx > IrisSettings.get().getWorld().getTargetSpawnEntitiesPerChunk()) {
IrisLogging.debug("Can't spawn. The entity per chunk ratio is at " + Form.pc(epx, 2) + " > 100% (total entities " + entityCount + ")");
@@ -549,16 +614,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
Position2[] cc = getLoadedChunkPositionsSnapshot(world);
while (spawnBuffer-- > 0) {
if (getEngine().isClosing() || getEngine().isClosed()) {
return actuallySpawned.get() > 0;
}
if (cc.length == 0) {
IrisLogging.debug("Can't spawn. No chunks!");
return false;
}
Position2 c = cc[RNG.r.nextInt(cc.length)];
spawnChunkSafely(world, c.getX(), c.getZ(), false);
if (!spawnChunkSafely(world, c.getX(), c.getZ(), false)) {
return actuallySpawned.get() > 0;
}
}
return actuallySpawned > 0;
return actuallySpawned.get() > 0;
}
private boolean isPregenActiveForThisWorld() {
@@ -604,13 +675,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
try {
return future.get(2, TimeUnit.SECONDS);
} catch (Throwable e) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new Position2[0];
} catch (ExecutionException | TimeoutException e) {
IrisLogging.reportError(e);
return new Position2[0];
}
}
private int getFoliaLivingEntityCount(World world) {
private Integer getFoliaLivingEntityCount(World world) {
CompletableFuture<List<Player>> playerFuture = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
@@ -620,18 +694,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
});
if (!scheduled) {
return 0;
reportEntityCountFailure("Unable to schedule the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
}
List<Player> players;
try {
players = playerFuture.get(2, TimeUnit.SECONDS);
} catch (Throwable e) {
IrisLogging.reportError(e);
return 0;
} catch (InterruptedException e) {
entityCountValid = false;
Thread.currentThread().interrupt();
return null;
} catch (TimeoutException e) {
reportEntityCountFailure("Timed out while reading the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", null);
return null;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportEntityCountFailure("Failed to read the Folia player snapshot; pausing Iris entity spawning until a complete count is available.", cause);
return null;
}
Map<String, Entity> candidates = new ConcurrentHashMap<>();
AtomicBoolean incomplete = new AtomicBoolean();
AtomicReference<Throwable> failure = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(players.size());
for (Player player : players) {
@@ -651,21 +736,28 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
candidates.put(nearby.getUniqueId().toString(), nearby);
}
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
latch.countDown();
}
})) {
incomplete.set(true);
latch.countDown();
}
}
try {
latch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!awaitEntityTasks(latch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity candidate scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
AtomicInteger count = new AtomicInteger();
incomplete.set(false);
failure.set(null);
CountDownLatch entityLatch = new CountDownLatch(candidates.size());
for (Entity entity : candidates.values()) {
if (!J.runEntity(entity, () -> {
@@ -673,47 +765,117 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
if (entity instanceof LivingEntity && world.equals(entity.getWorld()) && !entity.isDead()) {
count.incrementAndGet();
}
} catch (Throwable e) {
incomplete.set(true);
failure.compareAndSet(null, e);
} finally {
entityLatch.countDown();
}
})) {
incomplete.set(true);
entityLatch.countDown();
}
}
try {
entityLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!awaitEntityTasks(entityLatch, 2, TimeUnit.SECONDS) || incomplete.get()) {
if (!Thread.currentThread().isInterrupted()) {
reportEntityCountFailure("The Folia entity validation scan was incomplete; pausing Iris entity spawning until a complete count is available.", failure.get());
}
return null;
}
return count.get();
}
private void spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
static boolean awaitEntityTasks(CountDownLatch latch, long timeout, TimeUnit unit) {
try {
return latch.await(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private boolean spawnChunkSafely(World world, int chunkX, int chunkZ, boolean initial) {
if (world == null) {
return;
return false;
}
CompletableFuture<Void> future = new CompletableFuture<>();
J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
} finally {
future.complete(null);
AtomicBoolean failureReported = new AtomicBoolean();
future.whenComplete((ignored, failure) -> {
if (failure != null) {
reportSpawnFailure(chunkX, chunkZ, failure, failureReported);
}
});
boolean scheduled;
try {
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
future.complete(null);
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
future.complete(null);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
} catch (Throwable e) {
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
return false;
}
if (!scheduled) {
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
return false;
}
try {
future.get(5, TimeUnit.SECONDS);
} catch (Throwable e) {
IrisLogging.reportError(e);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (TimeoutException e) {
IrisLogging.warn("Timed out waiting for Iris entity spawning in chunk %d,%d; deferring the remaining spawn buffer.", chunkX, chunkZ);
return false;
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
return false;
}
}
private void reportEntityCountFailure(String message, Throwable error) {
entityCountValid = false;
if (error != null) {
if (entityCountErrorReported.compareAndSet(false, true)) {
IrisLogging.reportError(message, error);
}
return;
}
if (entityCountWarningReported.compareAndSet(false, true)) {
IrisLogging.warn(message);
}
}
private void resetEntityCountFailures() {
entityCountWarningReported.set(false);
entityCountErrorReported.set(false);
}
private void reportSpawnFailure(int chunkX, int chunkZ, Throwable failure, AtomicBoolean failureReported) {
if (!failureReported.compareAndSet(false, true)) {
return;
}
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
IrisLogging.reportError("Failed to spawn Iris entities in chunk " + chunkX + "," + chunkZ + ".", cause);
}
private void spawnIn(Chunk c, boolean initial) {
if (getEngine().isClosed()) {
return;
@@ -736,8 +898,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
spawn(block, s, false);
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true)));
J.runRegion(c.getWorld(), c.getX(), c.getZ(), managedTask(
"bukkit_world_manager_marker_spawn_followup",
() -> raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true))));
});
}
@@ -775,17 +939,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
if (v == null || v.getReferenceSpawner() == null)
return;
try {
spawn(c, v);
} catch (Throwable e) {
J.runRegion(c.getWorld(), c.getX(), c.getZ(), () -> spawn(c, v));
}
spawn(c, v);
}
private void spawn(Chunk c, IrisEntitySpawn i) {
IrisSpawner ref = i.getReferenceSpawner();
int s = i.spawn(getEngine(), c, RNG.r);
actuallySpawned += s;
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(getEngine(), c.getX(), c.getZ());
}
@@ -797,7 +957,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
int s = i.spawn(getEngine(), pos, RNG.r);
actuallySpawned += s;
actuallySpawned.addAndGet(s);
if (s > 0) {
ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
}
@@ -857,10 +1017,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
int cX = e.getX(), cZ = e.getZ();
Long key = Cache.key(cX, cZ);
cleanup.put(key, cleanupService.schedule(() -> {
cleanup.put(key, cleanupService.schedule(managedTask("bukkit_world_manager_chunk_cleanup", () -> {
cleanup.remove(key);
getEngine().cleanupMantleChunk(cX, cZ);
}, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
}, () -> cleanup.remove(key)), Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
}
@Override
@@ -898,15 +1058,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
@Override
public void teleportAsync(PlayerTeleportEvent e) {
e.setCancelled(true);
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), () -> {
warmupAreaAsync(e.getPlayer(), e.getTo(), () -> J.runEntity(e.getPlayer(), managedTask(
"bukkit_world_manager_teleport",
() -> {
ignoreTP.set(true);
e.getPlayer().teleport(e.getTo(), e.getCause());
ignoreTP.set(false);
}));
})));
}
private void warmupAreaAsync(Player player, Location to, Runnable r) {
J.a(() -> {
J.a(managedTask("bukkit_world_manager_teleport_warmup", () -> {
int viewDistance = 2;
KList<Future<Chunk>> futures = new KList<>();
for (int i = -viewDistance; i <= viewDistance; i++) {
@@ -945,7 +1107,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return "Loading Chunks";
}
}.queue(futures).execute(new VolmitSender(player), true, r);
});
}));
}
public Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
@@ -1024,14 +1186,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
J.a(() -> {
J.a(managedTask("bukkit_world_manager_marker_scan", () -> {
try {
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
if (markerData.isEmpty()) {
return;
}
J.runRegion(world, chunkX, chunkZ, () -> {
J.runRegion(world, chunkX, chunkZ, managedTask("bukkit_world_manager_marker_scan_region", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
@@ -1050,13 +1212,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
});
});
}));
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
markerScanQueue.remove(key);
}
});
}, () -> markerScanQueue.remove(key)));
}
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
@@ -1107,13 +1269,13 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
private void removeMarkerAsync(IrisPosition marker) {
J.a(() -> {
J.a(managedTask("bukkit_world_manager_remove_marker", () -> {
try {
getMantle().remove(marker.getX(), marker.getY(), marker.getZ(), MatterMarker.class);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
});
}));
}
private static final class MarkerSpawnData {
@@ -1127,21 +1289,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
int blockX = e.getBlock().getX();
int mantleY = toMantleY(e.getBlock().getY(), getEngine().getWorld().minHeight());
int blockZ = e.getBlock().getZ();
J.a(() -> {
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker != null) {
if (marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}
});
KList<ItemStack> d = new KList<>();
IrisBiome b = EngineBukkitOps.getBiome(getEngine(), e.getBlock().getLocation());
@@ -1159,15 +1306,29 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
e.setDropItems(false);
}
if (d.isNotEmpty()) {
World w = e.getBlock().getWorld();
Location dropLocation = e.getBlock().getLocation().clone().add(.5, .5, .5);
Runnable dropTask = () -> d.forEach(item -> w.dropItemNaturally(dropLocation, item));
if (!J.runAt(dropLocation, dropTask)) {
if (!J.isFolia()) {
J.s(dropTask);
}
World w = e.getBlock().getWorld();
Location blockLocation = e.getBlock().getLocation();
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
Runnable finalizedBreak = managedTask("bukkit_world_manager_block_break_finalize", () -> {
if (e.isCancelled()) {
return;
}
J.a(managedTask("bukkit_world_manager_block_break_marker", () -> {
MatterMarker marker = getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = getData().getMarkerLoader().load(marker.getTag());
if (mark == null || mark.isRemoveOnChange()) {
getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
}
}));
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
});
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
J.s(finalizedBreak, 1);
}
}
}
@@ -1176,6 +1337,22 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return worldY - minHeight;
}
static <T> void routeDrops(Iterable<T> drops, BlockDropRouter router, Consumer<T> fallback) {
for (T drop : drops) {
boolean routed = false;
if (router != null) {
try {
routed = router.routeDrop(drop);
} catch (Throwable error) {
IrisLogging.reportError("Failed to route a deferred Iris block drop.", error);
}
}
if (!routed) {
fallback.accept(drop);
}
}
}
static int toWorldY(int mantleY, int minHeight) {
return mantleY + minHeight;
}
@@ -1190,11 +1367,35 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
@Override
public void close() {
super.close();
looper.interrupt();
if (cleanupService != null) {
cleanupService.shutdownNow();
public synchronized void close() {
Throwable failure = null;
try {
super.close();
} catch (Throwable e) {
failure = e;
}
if (!looperStopped) {
try {
if (looper != null) {
looper.interrupt();
}
looperStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (!cleanupServiceStopped) {
try {
if (cleanupService != null) {
cleanupService.shutdownNow();
}
cleanupServiceStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (failure != null) {
throw new IllegalStateException("Failed to completely stop the Bukkit Iris world manager.", failure);
}
}
@@ -1212,6 +1413,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return (double) entityCount / (loadedChunkCount + 1) * 1.28;
}
private static Throwable appendCloseFailure(Throwable failure, Throwable next) {
if (failure == null) {
return next;
}
if (failure != next) {
failure.addSuppressed(next);
}
return failure;
}
@Data
private static class ChunkCounter implements Predicate<IrisSpawner> {
private final Entity[] entities;
@@ -69,7 +69,7 @@ public class UpperDimensionContext implements DataProvider {
engine.getData(),
chunkHeight,
complex.getHeightStream(),
complex.getBaseBiomeStream(),
complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
true
@@ -89,6 +89,8 @@ public class UpperDimensionContext implements DataProvider {
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
Collections.synchronizedMap(new IdentityHashMap<>());
upperDim.getRegions().forEach(regionKey -> {
IrisRegion region = upperData.getRegionLoader().load(regionKey);
if (region != null) {
@@ -135,8 +137,7 @@ public class UpperDimensionContext implements DataProvider {
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getLandZoom())
.zoom(r.getLandBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getLandBiomes(),
t -> t.setInferredType(InferredType.LAND))))
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
.convert(r -> upperDim.getSeaBiomeStyle()
@@ -144,16 +145,14 @@ public class UpperDimensionContext implements DataProvider {
.zoom(upperDim.getBiomeZoom())
.zoom(upperDim.getSeaZoom())
.zoom(r.getSeaBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getSeaBiomes(),
t -> t.setInferredType(InferredType.SEA))))
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
.convertAware2D(ProceduralStream::get);
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
.convert(r -> upperDim.getShoreBiomeStyle()
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
.zoom(upperDim.getBiomeZoom())
.zoom(r.getShoreBiomeZoom())
.selectRarity(upperData.getBiomeLoader().loadAll(r.getShoreBiomes(),
t -> t.setInferredType(InferredType.SHORE))))
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
.convertAware2D(ProceduralStream::get);
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
@@ -170,7 +169,9 @@ public class UpperDimensionContext implements DataProvider {
.convertAware2D((t, x, z) -> {
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
});
})
.convertAware2D((biome, x, z) -> implode(
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
@@ -235,6 +236,23 @@ public class UpperDimensionContext implements DataProvider {
return Math.max(Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), 0);
}, Interpolated.DOUBLE);
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
IrisBiome baseBiome = baseBiomeStream.get(x, z);
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
height,
baseBiome,
regionStream.get(x, z),
x,
z,
fluidHeight,
landBiomeStream,
seaBiomeStream,
shoreBiomeStream);
return resolved == baseBiome
? baseBiome
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
});
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
.select(upperDim.getRockPalette().getBlockData(upperData));
@@ -244,13 +262,67 @@ public class UpperDimensionContext implements DataProvider {
upperData,
chunkHeight,
heightStream,
baseBiomeStream,
finalBiomeStream,
regionStream,
rockStream,
false
);
}
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
inferred.add(biome.withInferredType(type));
}
return inferred;
}
private static IrisBiome implode(
IrisBiome biome,
double x,
double z,
RNG rng,
DataProvider dataProvider,
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
int remainingDepth
) {
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
return biome;
}
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
synchronized (childSelectionPlans) {
selectionPlan = childSelectionPlans.get(biome);
if (selectionPlan == null) {
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
if (child != null) {
options.add(child);
}
}
options.add(biome);
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
childSelectionPlans.put(biome, selectionPlan);
}
}
}
IrisBiome selected = selectionPlan.select(
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
if (selected == null) {
return biome;
}
return implode(
selected.withInferredType(biome.getInferredType()),
x,
z,
rng,
dataProvider,
childSelectionPlans,
remainingDepth - 1);
}
public int getUpperSurfaceY(int x, int z) {
double rawHeight = heightStream.get((double) x, (double) z);
return chunkHeight - 1 - (int) Math.round(rawHeight);
@@ -48,6 +48,7 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -127,6 +128,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return GenerationSessionLease.noop();
}
IrisContext context = IrisContext.get();
if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) {
return generationSessions.continueSession(operation, context.getGenerationSessionId());
}
if (isClosing() || isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", isClosed());
}
return generationSessions.acquire(operation);
}
@@ -33,82 +33,194 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
public abstract class EngineAssignedWorldManager extends EngineAssignedComponent implements EngineWorldManager, BukkitEngineWorldManager, Listener {
private final int taskId;
private final Object managerLifecycleLock;
private final AtomicBoolean started;
private boolean listenerRegistered;
private boolean closeRequested;
private int taskId;
protected AtomicBoolean ignoreTP = new AtomicBoolean(false);
public EngineAssignedWorldManager() {
super(null, null);
managerLifecycleLock = new Object();
started = new AtomicBoolean(false);
taskId = -1;
}
public EngineAssignedWorldManager(Engine engine) {
super(engine, "World");
BukkitPlatform.volmitPlugin().registerListener(this);
taskId = J.sr(this::onTick, 1);
managerLifecycleLock = new Object();
started = new AtomicBoolean(false);
taskId = -1;
}
@Override
public void start() {
synchronized (managerLifecycleLock) {
if (started.get()) {
return;
}
if (closeRequested) {
throw new IllegalStateException("Cannot restart a closed Iris world manager.");
}
started.set(true);
try {
listenerRegistered = true;
registerManagerListener();
taskId = scheduleManagerTick(() -> runManagerTask("bukkit_world_manager_tick", this::onTick));
} catch (Throwable e) {
started.set(false);
closeRequested = true;
Throwable cleanupFailure = closeManagerResources();
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
}
throw propagate(e);
}
}
}
@EventHandler
public void on(IrisEngineHotloadEvent e) {
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
VolmitSender s = new VolmitSender(i);
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
}
runManagerTask("bukkit_world_manager_hotload_event", () -> {
for (Player i : BukkitWorldBinding.players(e.getEngine().getWorld())) {
i.playSound(i.getLocation(), Sound.BLOCK_AMETHYST_BLOCK_BREAK, 1f, 1.8f);
VolmitSender s = new VolmitSender(i);
s.sendTitle(C.IRIS + "Engine " + C.AQUA + "<font:minecraft:uniform>Hotloaded", 70, 60, 410);
}
});
}
@EventHandler
public void on(WorldSaveEvent e) {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().save();
}
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().close();
}
runManagerTask("bukkit_world_manager_save", () -> {
if (e.getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
getEngine().save();
}
});
}
@EventHandler
public void on(BlockBreakEvent e) {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockBreak(e);
}
runManagerTask("bukkit_world_manager_block_break", () -> {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockBreak(e);
}
});
}
@EventHandler
public void on(BlockPlaceEvent e) {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockPlace(e);
}
runManagerTask("bukkit_world_manager_block_place", () -> {
if (e.getPlayer().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onBlockPlace(e);
}
});
}
@EventHandler
public void on(ChunkLoadEvent e) {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkLoad(e.getChunk(), e.isNewChunk());
}
runManagerTask("bukkit_world_manager_chunk_load", () -> {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkLoad(e.getChunk(), e.isNewChunk());
}
});
}
@EventHandler
public void on(ChunkUnloadEvent e) {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkUnload(e.getChunk());
}
runManagerTask("bukkit_world_manager_chunk_unload", () -> {
if (e.getChunk().getWorld().equals(BukkitWorldBinding.world(getTarget().getWorld()))) {
onChunkUnload(e.getChunk());
}
});
}
@Override
public void close() {
super.close();
BukkitPlatform.volmitPlugin().unregisterListener(this);
if (taskId != -1) {
J.csr(taskId);
Throwable failure;
synchronized (managerLifecycleLock) {
closeRequested = true;
started.set(false);
failure = closeManagerResources();
}
if (failure != null) {
throw new IllegalStateException("Failed to completely stop the Iris world manager.", failure);
}
}
protected void registerManagerListener() {
BukkitPlatform.volmitPlugin().registerListener(this);
}
protected int scheduleManagerTick(Runnable tick) {
return J.sr(tick, 1);
}
protected void unregisterManagerListener() {
BukkitPlatform.volmitPlugin().unregisterListener(this);
}
protected void cancelManagerTick(int scheduledTaskId) {
J.csr(scheduledTaskId);
}
private Throwable closeManagerResources() {
Throwable failure = null;
if (listenerRegistered) {
try {
unregisterManagerListener();
listenerRegistered = false;
} catch (Throwable e) {
failure = e;
}
}
if (taskId != -1) {
try {
cancelManagerTick(taskId);
taskId = -1;
} catch (Throwable e) {
if (failure == null) {
failure = e;
} else if (failure != e) {
failure.addSuppressed(e);
}
}
}
return failure;
}
private RuntimeException propagate(Throwable failure) {
if (failure instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (failure instanceof Error error) {
throw error;
}
return new IllegalStateException(failure);
}
protected boolean runManagerTask(String operation, Runnable task) {
if (!started.get()) {
return false;
}
return EngineLifecycleTasks.run(getEngine(), operation, task);
}
protected <T> T callManagerTask(String operation, Supplier<T> task, T unavailable) {
if (!started.get()) {
return unavailable;
}
return EngineLifecycleTasks.call(getEngine(), operation, task, unavailable);
}
protected boolean isManagerStarted() {
return started.get();
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Objects;
import java.util.function.Supplier;
public final class EngineLifecycleTasks {
private EngineLifecycleTasks() {
}
public static boolean run(Engine engine, String operation, Runnable task) {
Objects.requireNonNull(engine);
Objects.requireNonNull(operation);
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
task.run();
return true;
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return false;
}
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return task.get();
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return unavailable;
}
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
}
@@ -22,6 +22,9 @@ import art.arcane.volmlib.util.atomics.AtomicRollingSequence;
import art.arcane.volmlib.util.collection.KMap;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
public class EngineMetrics {
private final AtomicRollingSequence total;
@@ -111,4 +114,24 @@ public class EngineMetrics {
return v;
}
public Map<String, Double> telemetryAverages() {
Map<String, Double> averages = new LinkedHashMap<>();
averages.put("total", total.getAverage());
averages.put("updates", updates.getAverage());
averages.put("terrain", terrain.getAverage());
averages.put("biome", biome.getAverage());
averages.put("post", post.getAverage());
averages.put("perfection", perfection.getAverage());
averages.put("decoration", decoration.getAverage());
averages.put("cave", cave.getAverage());
averages.put("deposit", deposit.getAverage());
averages.put("carve.resolve", carveResolve.getAverage());
averages.put("carve.apply", carveApply.getAverage());
averages.put("context.prefill", contextPrefill.getAverage());
averages.put("pregen.wait.permit", pregenWaitPermit.getAverage());
averages.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage());
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
return Map.copyOf(averages);
}
}
@@ -32,8 +32,8 @@ import lombok.ToString;
public class EngineTarget {
private final MultiBurst burster;
private final IrisData data;
private IrisDimension dimension;
private IrisWorld world;
private final IrisDimension dimension;
private final IrisWorld world;
public EngineTarget(IrisWorld world, IrisDimension dimension, IrisData data) {
this.world = world;
@@ -0,0 +1,222 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisEngineStatistics;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record EngineTelemetrySnapshot(
long sampledAtMs,
String worldIdentity,
String worldName,
String dimensionKey,
boolean active,
boolean studio,
boolean closing,
boolean failed,
long loadedChunks,
long loadedEntities,
double entitySaturation,
long generatedSession,
long generatedTotal,
double chunksPerSecond,
long blockUpdatesPerSecond,
int parallelism,
int activeGenerationLeases,
long hotloadsTotal,
long mantleResidentPlates,
long mantleQueuedPlates,
double mantleIdleMs,
Map<String, Double> generationTimingsMs
) {
public EngineTelemetrySnapshot {
if (worldIdentity == null || worldIdentity.isBlank()) {
throw new IllegalArgumentException("Engine telemetry requires a world identity");
}
worldIdentity = worldIdentity.trim();
worldName = worldName == null || worldName.isBlank() ? worldIdentity : worldName.trim();
dimensionKey = dimensionKey == null ? "" : dimensionKey.trim();
loadedChunks = Math.max(0L, loadedChunks);
loadedEntities = Math.max(0L, loadedEntities);
entitySaturation = finiteNonNegative(entitySaturation);
generatedSession = Math.max(0L, generatedSession);
generatedTotal = Math.max(0L, generatedTotal);
chunksPerSecond = finiteNonNegative(chunksPerSecond);
blockUpdatesPerSecond = Math.max(0L, blockUpdatesPerSecond);
parallelism = Math.max(0, parallelism);
activeGenerationLeases = Math.max(0, activeGenerationLeases);
hotloadsTotal = Math.max(0L, hotloadsTotal);
mantleResidentPlates = Math.max(0L, mantleResidentPlates);
mantleQueuedPlates = Math.max(0L, mantleQueuedPlates);
mantleIdleMs = finiteNonNegative(mantleIdleMs);
generationTimingsMs = sanitizeTimings(generationTimingsMs);
}
public static EngineTelemetrySnapshot capture(Engine engine, double chunksPerSecond, long sampledAtMs) {
if (engine == null) {
throw new IllegalArgumentException("Engine cannot be null");
}
EngineWorldManager worldManager = engine.getWorldManager();
GenerationSessionManager sessions = engine.getGenerationSessions();
IrisEngineStatistics statistics = engine.getEngineData().getStatistics();
boolean closing = engine.isClosing();
boolean closed = engine.isClosed();
boolean failed = engine.hasFailed();
return new EngineTelemetrySnapshot(
sampledAtMs,
engine.getWorld().identity(),
engine.getWorld().name(),
engine.getDimension().getLoadKey(),
!closing && !closed && !failed,
engine.isStudio(),
closing,
failed,
worldManager == null ? 0L : worldManager.getChunkCount(),
worldManager == null ? 0L : worldManager.getEntityCount(),
worldManager == null ? 0D : worldManager.getEntitySaturation(),
engine.getGenerated(),
statistics == null ? 0L : statistics.getChunksGenerated(),
chunksPerSecond,
engine.getBlockUpdatesPerSecond(),
engine.getParallelism(),
sessions == null ? 0 : sessions.activeLeases(),
statistics == null ? 0L : statistics.getTotalHotloads(),
engine.getMantle().getLoadedRegionCount(),
engine.getMantle().getUnloadRegionCount(),
engine.getMantle().getAdjustedIdleDuration(),
engine.getMetrics().telemetryAverages()
);
}
public static Aggregate aggregate(List<EngineTelemetrySnapshot> snapshots) {
List<EngineTelemetrySnapshot> safeSnapshots = snapshots == null ? List.of() : snapshots;
int active = 0;
int studio = 0;
int closing = 0;
int failed = 0;
long loadedChunks = 0L;
long loadedEntities = 0L;
double entitySaturation = 0D;
long generatedSession = 0L;
long generatedTotal = 0L;
double chunksPerSecond = 0D;
long blockUpdatesPerSecond = 0L;
int parallelism = 0;
int activeGenerationLeases = 0;
long hotloadsTotal = 0L;
long mantleResidentPlates = 0L;
long mantleQueuedPlates = 0L;
double mantleIdleTotal = 0D;
double mantleIdleMax = 0D;
double mantleIdleMin = Double.POSITIVE_INFINITY;
int mantleIdleCount = 0;
int worldCount = 0;
Map<String, Double> timingMaxima = new LinkedHashMap<>();
for (EngineTelemetrySnapshot snapshot : safeSnapshots) {
if (snapshot == null) {
continue;
}
worldCount++;
active += snapshot.active() ? 1 : 0;
studio += snapshot.studio() ? 1 : 0;
closing += snapshot.closing() ? 1 : 0;
failed += snapshot.failed() ? 1 : 0;
loadedChunks += snapshot.loadedChunks();
loadedEntities += snapshot.loadedEntities();
entitySaturation = Math.max(entitySaturation, snapshot.entitySaturation());
generatedSession += snapshot.generatedSession();
generatedTotal += snapshot.generatedTotal();
chunksPerSecond += snapshot.chunksPerSecond();
blockUpdatesPerSecond += snapshot.blockUpdatesPerSecond();
parallelism += snapshot.parallelism();
activeGenerationLeases += snapshot.activeGenerationLeases();
hotloadsTotal += snapshot.hotloadsTotal();
mantleResidentPlates += snapshot.mantleResidentPlates();
mantleQueuedPlates += snapshot.mantleQueuedPlates();
mantleIdleTotal += snapshot.mantleIdleMs();
mantleIdleMax = Math.max(mantleIdleMax, snapshot.mantleIdleMs());
mantleIdleMin = Math.min(mantleIdleMin, snapshot.mantleIdleMs());
mantleIdleCount++;
for (Map.Entry<String, Double> entry : snapshot.generationTimingsMs().entrySet()) {
timingMaxima.merge(entry.getKey(), entry.getValue(), Math::max);
}
}
return new Aggregate(
worldCount,
active,
studio,
closing,
failed,
loadedChunks,
loadedEntities,
entitySaturation,
generatedSession,
generatedTotal,
chunksPerSecond,
blockUpdatesPerSecond,
parallelism,
activeGenerationLeases,
hotloadsTotal,
mantleResidentPlates,
mantleQueuedPlates,
mantleIdleCount == 0 ? 0D : mantleIdleTotal / mantleIdleCount,
mantleIdleMax,
mantleIdleCount == 0 ? 0D : mantleIdleMin,
Map.copyOf(timingMaxima)
);
}
private static double finiteNonNegative(double value) {
return Double.isFinite(value) && value > 0D ? value : 0D;
}
private static Map<String, Double> sanitizeTimings(Map<String, Double> timings) {
if (timings == null || timings.isEmpty()) {
return Map.of();
}
Map<String, Double> sanitized = new LinkedHashMap<>(timings.size());
for (Map.Entry<String, Double> entry : timings.entrySet()) {
String key = entry.getKey();
Double value = entry.getValue();
if (key == null || key.isBlank() || value == null || !Double.isFinite(value) || value < 0D) {
continue;
}
sanitized.put(key, value);
}
return Map.copyOf(sanitized);
}
public record Aggregate(
int worldCount,
int active,
int studio,
int closing,
int failed,
long loadedChunks,
long loadedEntities,
double entitySaturationMax,
long generatedSession,
long generatedTotal,
double chunksPerSecond,
long blockUpdatesPerSecond,
int parallelism,
int activeGenerationLeases,
long hotloadsTotal,
long mantleResidentPlates,
long mantleQueuedPlates,
double mantleIdleAverageMs,
double mantleIdleMaxMs,
double mantleIdleMinMs,
Map<String, Double> generationTimingMaximaMs
) {
public Aggregate {
generationTimingMaximaMs = generationTimingMaximaMs == null
? Map.of()
: Map.copyOf(generationTimingMaximaMs);
}
}
}
@@ -20,6 +20,9 @@ package art.arcane.iris.engine.framework;
@SuppressWarnings("EmptyMethod")
public interface EngineWorldManager {
default void start() {
}
void close();
int getEntityCount();
@@ -38,6 +38,23 @@ public final class GenerationSessionManager {
}
}
public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || state.sessionId() != sessionId) {
throw rejected(operation, state);
}
state.activeLeases().incrementAndGet();
if (state != current.get()) {
releaseLease(state);
continue;
}
return new GenerationSessionLease(this, state, state.sessionId());
}
}
public long currentSessionId() {
GenerationSessionState state = current.get();
return state == null ? 0L : state.sessionId();
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -32,7 +33,7 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayDeque;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -105,18 +106,15 @@ public final class HintedLocator<T> implements Locator<T> {
}
@Override
public Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
public CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
Locator.cancelSearch();
return MultiBurst.burst.completeValue(() -> {
AtomicBoolean stop = new AtomicBoolean(false);
LocatorCanceller.cancel = () -> stop.set(true);
try {
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("hinted_locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SearchPlan plan = planner.apply(engine);
if (!plan.isPossible()) {
@@ -124,10 +122,9 @@ public final class HintedLocator<T> implements Locator<T> {
}
return search(engine, plan, pos, timeout, checks, stop);
} finally {
LocatorCanceller.cancel = null;
}
});
return LocatorCanceller.requestScoped(search, stop);
}
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
@@ -140,7 +137,7 @@ public final class HintedLocator<T> implements Locator<T> {
KList<Position2> batch = new KList<>();
for (int ring = 0; ring <= maxRing; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -173,7 +170,7 @@ public final class HintedLocator<T> implements Locator<T> {
int index = i;
Position2 sample = batch.get(i);
executor.queue(() -> {
if (stop.get() || (fine && matched.get())) {
if (stop.get() || engine.isClosing() || (fine && matched.get())) {
return;
}
@@ -210,7 +207,7 @@ public final class HintedLocator<T> implements Locator<T> {
return batch.get(i);
}
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -228,7 +225,7 @@ public final class HintedLocator<T> implements Locator<T> {
KList<Position2> cells = new KList<>();
for (int ring = 0; ring <= stride; ring++) {
if (stop.get() || stopwatch.getMilliseconds() >= timeout) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
}
@@ -239,7 +236,7 @@ public final class HintedLocator<T> implements Locator<T> {
for (Position2 cell : cells) {
executor.queue(() -> {
if (stop.get() || found.get() != null) {
if (stop.get() || engine.isClosing() || found.get() != null) {
return;
}
@@ -34,7 +34,7 @@ import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -42,13 +42,6 @@ import java.util.function.Consumer;
@FunctionalInterface
public interface Locator<T> {
static void cancelSearch() {
if (LocatorCanceller.cancel != null) {
LocatorCanceller.cancel.run();
LocatorCanceller.cancel = null;
}
}
static Locator<IrisRegion> region(String loadKey) {
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
@@ -107,55 +100,54 @@ public interface Locator<T> {
boolean matches(Engine engine, Position2 chunk);
default Future<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
default CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
cancelSearch();
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicInteger searched = new AtomicInteger();
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && !engine.isClosing() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
return MultiBurst.burst.completeValue(() -> {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicInteger searched = new AtomicInteger();
AtomicBoolean stop = new AtomicBoolean(false);
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
LocatorCanceller.cancel = () -> stop.set(true);
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get() || stop.get() || engine.isClosing()) {
return;
}
if (matches(engine, p)) {
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
});
}
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get()) {
return;
}
if (matches(engine, p)) {
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
});
e.complete();
checks.accept(searched.get());
}
e.complete();
checks.accept(searched.get());
if (found.get() && foundPos.get() != null) {
return foundPos.get();
}
return null;
}
LocatorCanceller.cancel = null;
if (found.get() && foundPos.get() != null) {
return foundPos.get();
}
return null;
});
return LocatorCanceller.requestScoped(search, stop);
}
}
@@ -18,6 +18,49 @@
package art.arcane.iris.engine.framework;
public class LocatorCanceller {
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
public final class LocatorCanceller {
protected static Runnable cancel = null;
private LocatorCanceller() {
}
static <T> CompletableFuture<T> requestScoped(CompletableFuture<T> future, AtomicBoolean stop) {
return new RequestFuture<>(Objects.requireNonNull(future), Objects.requireNonNull(stop));
}
private static final class RequestFuture<T> extends CompletableFuture<T> {
private final CompletableFuture<T> delegate;
private final AtomicBoolean stop;
private RequestFuture(CompletableFuture<T> delegate, AtomicBoolean stop) {
this.delegate = delegate;
this.stop = stop;
delegate.whenComplete((value, exception) -> {
if (isDone()) {
return;
}
if (exception == null) {
complete(value);
} else {
completeExceptionally(exception);
}
});
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (isDone()) {
return false;
}
stop.set(true);
boolean cancelled = super.cancel(mayInterruptIfRunning);
delegate.cancel(mayInterruptIfRunning);
return cancelled;
}
}
}
@@ -0,0 +1,28 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.spi.PlatformBlockState;
import java.util.Objects;
public record TreeBlockMaterial(String materialKey) {
public TreeBlockMaterial {
Objects.requireNonNull(materialKey, "materialKey");
if (materialKey.isBlank()) {
throw new IllegalArgumentException("materialKey must not be blank");
}
}
public static TreeBlockMaterial of(PlatformBlockState state) {
return of(Objects.requireNonNull(state, "state").key());
}
public static TreeBlockMaterial of(String blockStateKey) {
String key = Objects.requireNonNull(blockStateKey, "blockStateKey");
int properties = key.indexOf('[');
return new TreeBlockMaterial(properties < 0 ? key : key.substring(0, properties));
}
public boolean matches(String blockStateKey) {
return equals(of(blockStateKey));
}
}
@@ -26,4 +26,8 @@ public class WrongEngineBroException extends Exception {
public WrongEngineBroException(String message) {
super(message);
}
public WrongEngineBroException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisPosition;
@@ -138,6 +139,10 @@ public interface EngineMantle extends MatterGenerator {
return getEngine().getDimension().isDebugSmartBore();
}
default void trim(long duration) {
getMantle().trim(duration);
}
default void trim(long dur, int limit) {
getMantle().trim(dur, limit);
}
@@ -281,6 +286,7 @@ public interface EngineMantle extends MatterGenerator {
chunk.deleteSlices(MatterCavern.class);
chunk.deleteSlices(MatterFluidBody.class);
chunk.deleteSlices(MatterMarker.class);
chunk.deleteSlices(TreeBlockMaterial.class);
chunk.trimSlices();
});
} finally {
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -556,6 +557,7 @@ public class IrisStructureComponent extends IrisMantleComponent {
for (ObjectBlockPosition position : positions) {
writer.clearBlock(position.x(), position.y(), position.z());
writer.clearData(position.x(), position.y(), position.z(), String.class);
writer.clearData(position.x(), position.y(), position.z(), TreeBlockMaterial.class);
writer.clearData(position.x(), position.y(), position.z(), TileWrapper.class);
writer.clearData(position.x(), position.y(), position.z(), MatterStructurePOI.class);
}
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.iris.engine.object.CarvingMode;
import art.arcane.iris.engine.object.DecayControlPlacer;
import art.arcane.iris.engine.object.IObjectPlacer;
@@ -45,6 +46,7 @@ import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.IrisObjectVacuum;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralPlacement;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.TileData;
@@ -138,6 +140,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
return key + "@" + id;
}
private static boolean isTreePlacement(IrisObject object, IrisObjectPlacement placement) {
String key = object == null ? null : object.getLoadKey();
return (key != null && key.startsWith("trees/"))
|| (placement != null && placement.getTrees() != null && placement.getTrees().isNotEmpty());
}
private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, PlatformBlockState data) {
placer.setData(x, y, z, TreeBlockMaterial.of(data));
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
@@ -440,6 +452,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean golden = isGoldenDebugChunk(x, z);
CaveAnchorCache caveAnchorCache = new CaveAnchorCache();
for (IrisProceduralPlacement p : proceduralObjects.getAllPlacements()) {
boolean treePlacement = p instanceof IrisProceduralTree;
boolean chancePassed = rng.chance(p.getChance() + rng.d(-0.005, 0.005));
if (golden) {
IrisLogging.info("Goldendebug procedural chance: chunk=" + x + "," + z
@@ -535,6 +548,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
minDepthBelowSurface,
id,
"procedural",
treePlacement,
rng
);
placeResult = contained.resultY();
@@ -545,6 +559,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (marker != null) {
placer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (treePlacement && marker != null) {
writeTreeMaterial(placer, b.getX(), b.getY(), b.getZ(), data);
}
}, null, getData());
}
if (golden) {
@@ -608,6 +625,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int minDepthBelowSurface,
int id,
String markerContext,
boolean treePlacement,
RNG rng
) {
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface);
@@ -620,6 +638,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (marker != null) {
transaction.setData(block.getX(), block.getY(), block.getZ(), marker);
}
if (treePlacement && marker != null) {
writeTreeMaterial(transaction, block.getX(), block.getY(), block.getZ(), data);
}
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
transaction.setData(block.getX(), block.getY(), block.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -669,6 +690,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int xx = rng.i(x, x + 16);
int zz = rng.i(z, z + 16);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
boolean treePlacement = isTreePlacement(v, effectivePlacement);
int id = rng.i(0, Integer.MAX_VALUE);
IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
if (golden) {
@@ -686,6 +708,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (treePlacement && marker != null) {
writeTreeMaterial(writer, b.getX(), b.getY(), b.getZ(), data);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -866,6 +891,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
objectMinDepthBelowSurface,
id,
"cave",
isTreePlacement(object, effectivePlacement),
rng
);
int result = contained.resultY();
@@ -1046,12 +1072,16 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (forcePlace) {
placement.setForcePlace(true);
}
boolean treePlacement = isTreePlacement(v, objectPlacement);
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
String marker = placementMarker(v, id, "upper");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (treePlacement && marker != null) {
writeTreeMaterial(writer, b.getX(), b.getY(), b.getZ(), data);
}
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -59,6 +59,7 @@ import org.bukkit.block.Biome;
import java.awt.Color;
import java.util.EnumMap;
import java.util.Objects;
@Accessors(chain = true)
@NoArgsConstructor
@@ -216,7 +217,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisDepositVariant.class)
@Desc("Deposit ore remap rules scoped to this biome. Each entry declares a vertical band and a source->replacement block id map. Applied before regional and dimension rules; first matching biome rule wins.")
private KList<IrisDepositVariant> depositVariants = new KList<>();
private transient InferredType inferredType;
private transient volatile InferredType inferredType;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient EnumMap<InferredType, IrisBiome> inferredVariants = new EnumMap<>(InferredType.class);
@Desc("Collection of ores to be generated")
@ArrayType(type = IrisOreGenerator.class, min = 1)
private KList<IrisOreGenerator> ores = new KList<>();
@@ -242,6 +246,37 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return !getUndergroundOres().isEmpty();
}
public synchronized IrisBiome setInferredType(InferredType inferredType) {
this.inferredType = inferredType;
return this;
}
public synchronized IrisBiome withInferredType(InferredType type) {
Objects.requireNonNull(type, "type");
if (inferredType == null) {
inferredType = type;
return this;
}
if (inferredType == type) {
return this;
}
IrisBiome cached = inferredVariants.get(type);
if (cached != null) {
return cached;
}
IrisData data = getLoader();
if (data == null) {
throw new IllegalStateException("Cannot create an inferred biome variant without an Iris data loader.");
}
IrisBiome variant = data.getGson().fromJson(data.getGson().toJson(this), IrisBiome.class);
variant.setLoader(data);
variant.setLoadKey(getLoadKey());
variant.setLoadFile(getLoadFile());
variant.inferredType = type;
inferredVariants.put(type, variant);
return variant;
}
private PlatformBlockState generateOres(KList<IrisOreGenerator> localOres, int x, int y, int z, RNG rng, IrisData data) {
if (localOres.isEmpty()) {
return null;
@@ -20,16 +20,23 @@ package art.arcane.iris.engine.object;
import lombok.Data;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@Data
public class IrisEngineStatistics {
private int totalHotloads = 0;
private int chunksGenerated = 0;
private int IrisToUpgradedVersion = 0;
private int IrisCreationVersion = 0;
private int MinecraftVersion = 0;
private static final AtomicIntegerFieldUpdater<IrisEngineStatistics> TOTAL_HOTLOADS =
AtomicIntegerFieldUpdater.newUpdater(IrisEngineStatistics.class, "totalHotloads");
private static final AtomicIntegerFieldUpdater<IrisEngineStatistics> CHUNKS_GENERATED =
AtomicIntegerFieldUpdater.newUpdater(IrisEngineStatistics.class, "chunksGenerated");
private volatile int totalHotloads = 0;
private volatile int chunksGenerated = 0;
private volatile int IrisToUpgradedVersion = 0;
private volatile int IrisCreationVersion = 0;
private volatile int MinecraftVersion = 0;
public void generatedChunk() {
chunksGenerated++;
CHUNKS_GENERATED.incrementAndGet(this);
}
public void setUpgradedVersion(int i) {
@@ -55,6 +62,6 @@ public class IrisEngineStatistics {
}
public void hotloaded() {
totalHotloads++;
TOTAL_HOTLOADS.incrementAndGet(this);
}
}
@@ -20,13 +20,14 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.Required;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -36,6 +37,11 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
@Snippet("expression-load")
@Accessors(chain = true)
@NoArgsConstructor
@@ -44,6 +50,7 @@ import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = false)
public class IrisExpressionLoad {
private static final int STYLE_CACHE_SIZE = 8;
@Required
@Desc("The variable to assign this value to. Do not set the name to x, y, or z")
private String name = "";
@@ -60,24 +67,30 @@ public class IrisExpressionLoad {
@Desc("If defined, iris will use an internal value from the engine as it's value")
private IrisEngineValueType engineValue = null;
private transient AtomicCache<ProceduralStream<Double>> streamCache = new AtomicCache<>();
private transient AtomicCache<Double> valueCache = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient final KMap<Long, CNG> styleCache = new KMap<>();
private transient final Map<Engine, EngineCache> engineCaches =
Collections.synchronizedMap(new IdentityHashMap<>());
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient final ConcurrentLinkedHashMap<StandaloneStyleKey, CNG> standaloneStyleCache =
new ConcurrentLinkedHashMap.Builder<StandaloneStyleKey, CNG>()
.maximumWeightedCapacity(STYLE_CACHE_SIZE)
.build();
public double getValue(RNG rng, IrisData data, double x, double z) {
if (engineValue != null) {
return valueCache.aquire(() -> engineValue.get(data.getEngine()));
Engine engine = requireEngine(data);
return cacheFor(engine).value.aquire(() -> engineValue.get(engine));
}
if (engineStreamValue != null) {
return streamCache.aquire(() -> engineStreamValue.get(data.getEngine())).get(x, z);
Engine engine = requireEngine(data);
return cacheFor(engine).stream.aquire(() -> engineStreamValue.get(engine)).get(x, z);
}
if (styleValue != null) {
return styleCache.computeIfAbsent(rng.getSeed(), k -> styleValue.createNoCache(new RNG(k), data))
.noise(x, z);
return style(data, rng).noise(x, z);
}
return staticValue;
@@ -85,18 +98,92 @@ public class IrisExpressionLoad {
public double getValue(RNG rng, IrisData data, double x, double y, double z) {
if (engineValue != null) {
return valueCache.aquire(() -> engineValue.get(data.getEngine()));
Engine engine = requireEngine(data);
return cacheFor(engine).value.aquire(() -> engineValue.get(engine));
}
if (engineStreamValue != null) {
return streamCache.aquire(() -> engineStreamValue.get(data.getEngine())).get(x, z);
Engine engine = requireEngine(data);
return cacheFor(engine).stream.aquire(() -> engineStreamValue.get(engine)).get(x, z);
}
if (styleValue != null) {
return styleCache.computeIfAbsent(rng.getSeed(), k -> styleValue.createNoCache(new RNG(k), data))
.noise(x, y, z);
return style(data, rng).noise(x, y, z);
}
return staticValue;
}
private Engine requireEngine(IrisData data) {
Engine engine = data.getEngine();
if (engine == null) {
throw new IllegalStateException("Expression variable '" + name + "' requires an active Iris engine.");
}
return engine;
}
private EngineCache cacheFor(Engine engine) {
synchronized (engineCaches) {
Iterator<Map.Entry<Engine, EngineCache>> iterator = engineCaches.entrySet().iterator();
while (iterator.hasNext()) {
if (iterator.next().getKey().isClosed()) {
iterator.remove();
}
}
EngineCache cache = engineCaches.get(engine);
if (cache != null) {
return cache;
}
EngineCache created = new EngineCache();
engineCaches.put(engine, created);
return created;
}
}
private CNG style(IrisData data, RNG rng) {
Engine engine = data.getEngine();
long seed = rng.getSeed();
if (engine != null) {
return cacheFor(engine).styles.computeIfAbsent(seed,
ignored -> styleValue.createNoCache(new RNG(seed), data));
}
StandaloneStyleKey key = new StandaloneStyleKey(data, seed);
return standaloneStyleCache.computeIfAbsent(key,
ignored -> styleValue.createNoCache(new RNG(seed), data));
}
private static final class EngineCache {
private final AtomicCache<ProceduralStream<Double>> stream = new AtomicCache<>();
private final AtomicCache<Double> value = new AtomicCache<>();
private final ConcurrentLinkedHashMap<Long, CNG> styles =
new ConcurrentLinkedHashMap.Builder<Long, CNG>()
.maximumWeightedCapacity(STYLE_CACHE_SIZE)
.build();
}
private static final class StandaloneStyleKey {
private final IrisData data;
private final long seed;
private StandaloneStyleKey(IrisData data, long seed) {
this.data = data;
this.seed = seed;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof StandaloneStyleKey key)) {
return false;
}
return data == key.data && seed == key.seed;
}
@Override
public int hashCode() {
return 31 * System.identityHashCode(data) + Long.hashCode(seed);
}
}
}
@@ -19,7 +19,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
@@ -30,9 +30,13 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.ExpressionNoise;
import art.arcane.iris.util.project.noise.ImageNoise;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.io.File;
@@ -46,8 +50,14 @@ import java.util.concurrent.ConcurrentHashMap;
@Desc("A gen style")
@Data
public class IrisGeneratorStyle {
private static final int GENERATOR_CACHE_SIZE = 8;
private static final ConcurrentHashMap<String, String> ACTIVE_CACHE_KEYS = new ConcurrentHashMap<>();
private final transient AtomicCache<CNG> cng = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ConcurrentLinkedHashMap<GeneratorCacheKey, CNG> generatorCache =
new ConcurrentLinkedHashMap.Builder<GeneratorCacheKey, CNG>()
.maximumWeightedCapacity(GENERATOR_CACHE_SIZE)
.build();
@Desc("The chance is 1 in CHANCE per interval")
private NoiseStyle style = NoiseStyle.FLAT;
@@ -90,11 +100,11 @@ public class IrisGeneratorStyle {
}
public CNG createNoCache(RNG rng, IrisData data) {
return createNoCache(rng, data, false, 0, false);
return createNoCache(rng, data, false, 0, false, resolveEngine(data));
}
public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) {
return createNoCache(rng, data, false, Math.max(0, fallbackCacheSize), true);
return createNoCache(rng, data, false, Math.max(0, fallbackCacheSize), true, resolveEngine(data));
}
@@ -120,15 +130,16 @@ public class IrisGeneratorStyle {
return hash();
}
private String cachePrefix(RNG rng, int effectiveCacheSize) {
private String cachePrefix(RNG rng, int effectiveCacheSize, long engineStamp) {
return "style-" + Integer.toUnsignedString(hash())
+ "-seed-" + Long.toUnsignedString(rng.getSeed())
+ "-eng-" + Long.toUnsignedString(engineStamp)
+ "-sz-" + effectiveCacheSize
+ "-src-";
}
private String cacheKey(RNG rng, long sourceStamp, int effectiveCacheSize) {
return cachePrefix(rng, effectiveCacheSize) + Long.toUnsignedString(sourceStamp);
private String cacheKey(RNG rng, long sourceStamp, int effectiveCacheSize, long engineStamp) {
return cachePrefix(rng, effectiveCacheSize, engineStamp) + Long.toUnsignedString(sourceStamp);
}
private void clearStaleCacheEntries(IrisData data, String prefix, String key) {
@@ -156,10 +167,11 @@ public class IrisGeneratorStyle {
}
public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) {
return createNoCache(rng, data, actuallyCached, 0, false);
return createNoCache(rng, data, actuallyCached, 0, false, resolveEngine(data));
}
private CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached, int fallbackCacheSize, boolean quietCacheLog) {
private CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached, int fallbackCacheSize,
boolean quietCacheLog, Engine engine) {
CNG cng = null;
long sourceStamp = 0L;
if (getExpression() != null) {
@@ -182,7 +194,8 @@ public class IrisGeneratorStyle {
cng.setTrueFracturing(axialFracturing);
if (fracture != null) {
cng.fractureWith(fracture.createNoCache(rng.nextParallelRNG(2934), data, false, fallbackCacheSize, quietCacheLog), fracture.getMultiplier());
cng.fractureWith(fracture.createNoCache(rng.nextParallelRNG(2934), data, false,
fallbackCacheSize, quietCacheLog, engine), fracture.getMultiplier());
}
if (cellularFrequency > 0) {
@@ -191,8 +204,9 @@ public class IrisGeneratorStyle {
int effectiveCacheSize = cacheSize > 0 ? cacheSize : Math.max(0, fallbackCacheSize);
if (effectiveCacheSize > 0) {
String key = cacheKey(rng, sourceStamp, effectiveCacheSize);
clearStaleCacheEntries(data, cachePrefix(rng, effectiveCacheSize), key);
long engineStamp = engineStamp(engine);
String key = cacheKey(rng, sourceStamp, effectiveCacheSize, engineStamp);
clearStaleCacheEntries(data, cachePrefix(rng, effectiveCacheSize, engineStamp), key);
cng = cng.cached(effectiveCacheSize, key, data.getDataFolder(), quietCacheLog);
}
@@ -204,7 +218,13 @@ public class IrisGeneratorStyle {
}
public CNG create(RNG rng, IrisData data) {
return cng.aquire(() -> createNoCache(rng, data, true));
return create(rng, data, resolveEngine(data));
}
public CNG create(RNG rng, IrisData data, Engine engine) {
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, rng.getSeed());
return generatorCache.computeIfAbsent(key,
ignored -> createNoCache(rng, data, true, 0, false, engine));
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@@ -215,4 +235,51 @@ public class IrisGeneratorStyle {
public double getMaxFractureDistance() {
return multiplier;
}
private long engineStamp(Engine engine) {
if (engine == null) {
return 0L;
}
return Integer.toUnsignedLong(Objects.hash(
engine.getSeedManager().getSeed(),
engine.getMinHeight(),
engine.getMaxHeight(),
engine.getDimension().getLoadKey(),
engine.getDimension().getFluidHeight()
));
}
private Engine resolveEngine(IrisData data) {
return data == null ? null : data.getEngine();
}
private static final class GeneratorCacheKey {
private final IrisData data;
private final Engine engine;
private final long seed;
private GeneratorCacheKey(IrisData data, Engine engine, long seed) {
this.data = data;
this.engine = engine;
this.seed = seed;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GeneratorCacheKey key)) {
return false;
}
return data == key.data && engine == key.engine && seed == key.seed;
}
@Override
public int hashCode() {
int result = System.identityHashCode(data);
result = 31 * result + System.identityHashCode(engine);
return 31 * result + Long.hashCode(seed);
}
}
}
@@ -38,11 +38,15 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.bukkit.Bukkit;
import org.bukkit.Material;
@@ -62,7 +66,13 @@ import java.util.function.Function;
@Desc("Represents an iris object placer. It places objects.")
@Data
public class IrisObjectPlacement {
private final transient AtomicCache<CNG> surfaceWarp = new AtomicCache<>();
private static final int SURFACE_WARP_CACHE_SIZE = 8;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient ConcurrentLinkedHashMap<SurfaceWarpCacheKey, CNG> surfaceWarpCache =
new ConcurrentLinkedHashMap.Builder<SurfaceWarpCacheKey, CNG>()
.maximumWeightedCapacity(SURFACE_WARP_CACHE_SIZE)
.build();
@RegistryListResource(IrisObject.class)
@Required
@ArrayType(min = 1, type = String.class)
@@ -211,11 +221,14 @@ public class IrisObjectPlacement {
}
public CNG getSurfaceWarp(RNG rng, IrisData data) {
return surfaceWarp.aquire(() -> {
Engine engine = data.getEngine();
RNG warpRng = engine != null ? new RNG(engine.getSeedManager().getComponent() + 1024) : new RNG(8675309L);
return getWarp().create(warpRng, data);
});
return getSurfaceWarp(rng, data, data.getEngine());
}
public CNG getSurfaceWarp(RNG rng, IrisData data, @Nullable Engine engine) {
long seed = engine == null ? rng.getSeed() : engine.getSeedManager().getComponent() + 1024L;
SurfaceWarpCacheKey key = new SurfaceWarpCacheKey(data, engine, seed);
return surfaceWarpCache.computeIfAbsent(key,
ignored -> getWarp().create(new RNG(seed), data, engine));
}
public double warp(RNG rng, double x, double y, double z, IrisData data) {
@@ -344,6 +357,36 @@ public class IrisObjectPlacement {
return null;
}
private static final class SurfaceWarpCacheKey {
private final IrisData data;
private final Engine engine;
private final long seed;
private SurfaceWarpCacheKey(IrisData data, Engine engine, long seed) {
this.data = data;
this.engine = engine;
this.seed = seed;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof SurfaceWarpCacheKey key)) {
return false;
}
return data == key.data && engine == key.engine && seed == key.seed;
}
@Override
public int hashCode() {
int result = System.identityHashCode(data);
result = 31 * result + System.identityHashCode(engine);
return 31 * result + Long.hashCode(seed);
}
}
private static class TableCache {
final transient WeightedTables global = new WeightedTables();
final transient KMap<Material, WeightedTables> basic = new KMap<>();
@@ -275,7 +275,7 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
if (object == null || object.getBlocks().isEmpty()) {
continue;
}
object.setLoadKey("procedural/" + name + "#" + i);
object.setLoadKey(getVariantLoadKey(i));
object.setLoader(data);
baked.add(object);
}
@@ -303,6 +303,13 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
return baked.get(rng.i(baked.size()));
}
public String getVariantLoadKey(int index) {
if (index < 0) {
throw new IllegalArgumentException("index must not be negative");
}
return "procedural/tree/" + name + "#" + index;
}
public IrisObjectPlacement asPlacement() {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setMode(mode);
@@ -22,7 +22,6 @@ import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;
@@ -41,6 +40,7 @@ import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisDimensionContractException;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
@@ -57,6 +57,7 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.io.ReactiveFolder;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
@@ -266,7 +267,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (engine != null) return engine.getTarget();
return targetCache.aquire(() -> {
IrisData data = IrisData.get(dataLocation);
IrisData data = IrisData.openRuntime(dataLocation);
data.dump();
data.clearLists();
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
@@ -383,39 +384,48 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public CompletableFuture<Void> closeAsync() {
CompletableFuture<Void> existing = closeFuture.get();
if (existing != null && !existing.isDone()) {
return existing;
}
closing = true;
CompletableFuture<Void> future = withExclusiveControlFuture(() -> {
Looper activeHotloader = hotloader;
hotloader = null;
if (isStudio() && activeHotloader != null) {
activeHotloader.interrupt();
try {
activeHotloader.join(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
}
CompletableFuture<Void> future = new CompletableFuture<>();
while (!closeFuture.compareAndSet(null, future)) {
CompletableFuture<Void> existing = closeFuture.get();
if (existing != null) {
return existing;
}
Engine currentEngine = engine;
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
folder.clear();
populators.clear();
});
if (!closeFuture.compareAndSet(existing, future)) {
CompletableFuture<Void> winningFuture = closeFuture.get();
return winningFuture == null ? future : winningFuture;
}
future.whenComplete((ignored, throwable) -> {
if (throwable != null) {
CompletableFuture<Void> operation;
try {
operation = withExclusiveControlFuture(() -> {
Looper activeHotloader = hotloader;
hotloader = null;
if (isStudio() && activeHotloader != null) {
activeHotloader.interrupt();
try {
activeHotloader.join(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
}
}
Engine currentEngine = engine;
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
folder.clear();
populators.clear();
});
} catch (Throwable throwable) {
future.completeExceptionally(throwable);
closeFuture.compareAndSet(future, null);
return future;
}
operation.whenComplete((ignored, throwable) -> {
if (throwable == null) {
future.complete(null);
} else {
future.completeExceptionally(throwable);
closeFuture.compareAndSet(future, null);
}
});
@@ -446,8 +456,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
IrisLogging.reportError(e);
e.printStackTrace();
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
if (acquired) {
loadLock.release(LOAD_LOCKS);
@@ -486,7 +498,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
if (closing) {
return;
throw new IllegalStateException("Iris chunk generation was rejected while the generator is closing.");
}
throwIfInitializationFailed();
@@ -501,8 +513,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
boolean useMulticore = studio && !J.isFolia();
engine.generate(x << 4, z << 4, blocks, biomes, useMulticore);
blocks.apply();
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, blocks, biomes, useMulticore);
blocks.apply();
}
}
IrisLogging.debug("Generated " + x + " " + z);
@@ -510,7 +525,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
throw e;
} catch (GenerationSessionException e) {
if (closing || isExpectedTeardown(engine, e)) {
return;
throw new IllegalStateException("Iris chunk generation was rejected during an engine transition.", e);
}
IrisLogging.error("======================================");
@@ -518,22 +533,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
reportErrorChunk(x, z, e);
IrisLogging.error("======================================");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData());
}
}
throw new IllegalStateException("Iris chunk generation could not acquire its engine runtime.", e);
} catch (Throwable e) {
IrisLogging.error("======================================");
e.printStackTrace();
reportErrorChunk(x, z, e);
IrisLogging.error("======================================");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData());
}
}
throw new IllegalStateException("Iris failed to generate chunk " + x + "," + z + ".", e);
}
}
@@ -595,7 +602,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
default -> false;
};
return currentEngine.getMinHeight() + currentEngine.getHeight(x, z, ignoreFluid) + 1;
try (GenerationSessionLease lease = currentEngine.acquireGenerationLease("bukkit_base_height");
IrisContext.Scope ignored = IrisContext.open(currentEngine, lease.sessionId(), null)) {
return currentEngine.getMinHeight() + currentEngine.getHeight(x, z, ignoreFluid) + 1;
} catch (GenerationSessionException e) {
throw new IllegalStateException("Iris base height query was rejected for world '"
+ worldInfo.getName() + "'.", e);
}
}
private void computeStudioGenerator() {
@@ -22,8 +22,8 @@ import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.events.IrisLootEvent;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.framework.PlacedObject;
@@ -43,6 +43,7 @@ import art.arcane.iris.util.common.reflect.KeyedType;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.common.scheduling.jobs.SingleJob;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -74,7 +75,10 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.Lootable;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;
@@ -83,6 +87,8 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
public final class EngineBukkitOps {
private static final ConcurrentHashMap<UUID, CompletableFuture<Position2>> ACTIVE_LOCATE_REQUESTS = new ConcurrentHashMap<>();
private EngineBukkitOps() {
}
@@ -526,11 +532,11 @@ public final class EngineBukkitOps {
}
public static void gotoBiome(Engine engine, IrisBiome biome, Player player, boolean teleport) {
find(Locator.surfaceBiome(biome.getLoadKey()), player, teleport, "Biome " + biome.getName());
find(engine, Locator.surfaceBiome(biome.getLoadKey()), player, teleport, "Biome " + biome.getName());
}
public static void gotoObject(Engine engine, String s, Player player, boolean teleport) {
find(Locator.object(s), player, teleport, "Object " + s);
find(engine, Locator.object(s), player, teleport, "Object " + s);
}
public static void gotoRegion(Engine engine, IrisRegion r, Player player, boolean teleport) {
@@ -539,19 +545,19 @@ public final class EngineBukkitOps {
return;
}
find(Locator.region(r.getLoadKey()), player, teleport, "Region " + r.getName());
find(engine, Locator.region(r.getLoadKey()), player, teleport, "Region " + r.getName());
}
public static void gotoPOI(Engine engine, String type, Player p, boolean teleport) {
find(Locator.poi(type), p, teleport, "POI " + type);
find(engine, Locator.poi(type), p, teleport, "POI " + type);
}
public static void gotoStructure(Engine engine, String key, Player player, boolean teleport) {
find(Locator.structure(key), player, teleport, "Structure " + key);
find(engine, Locator.structure(key), player, teleport, "Structure " + key);
}
private static void find(Locator<?> locator, Player player, boolean teleport, String message) {
find(locator, player, 120_000, location -> {
private static void find(Engine engine, Locator<?> locator, Player player, boolean teleport, String message) {
find(engine, locator, player, 120_000, location -> {
if (location == null) {
player.sendMessage(C.RED + "Could not find " + message + " within search range.");
return;
@@ -565,26 +571,50 @@ public final class EngineBukkitOps {
});
}
private static void find(Locator<?> locator, Player player, long timeout, Consumer<Location> consumer) {
private static void find(Engine engine, Locator<?> locator, Player player, long timeout, Consumer<Location> consumer) {
AtomicLong checks = new AtomicLong();
long ms = M.ms();
World world = player.getWorld();
Location origin = player.getLocation();
int originChunkX = origin.getBlockX() >> 4;
int originChunkZ = origin.getBlockZ() >> 4;
new SingleJob("Searching", () -> {
CompletableFuture<Position2> search = null;
boolean resultDispatched = false;
UUID playerId = player.getUniqueId();
try {
World world = player.getWorld();
Engine engine = IrisToolbelt.access(world).getEngine();
Position2 at = locator.find(engine, new Position2(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4), timeout, checks::set).get();
search = locator.find(engine, new Position2(originChunkX, originChunkZ), timeout, checks::set);
CompletableFuture<Position2> previous = ACTIVE_LOCATE_REQUESTS.put(playerId, search);
if (previous != null && previous != search) {
previous.cancel(true);
}
Position2 at = search.get();
if (ACTIVE_LOCATE_REQUESTS.get(playerId) != search) {
return;
}
if (at != null) {
int bx = (at.getX() << 4) + 8;
int bz = (at.getZ() << 4) + 8;
consumer.accept(new Location(world, bx,
world.getHighestBlockYAt(bx, bz) + 2,
bz));
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_locator_result");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int by = engine.getMinHeight() + engine.getHeight(bx, bz, false) + 2;
resultDispatched = dispatchLocateResult(
player, world, consumer, new Location(world, bx, by, bz), playerId, search);
}
} else {
consumer.accept(null);
resultDispatched = dispatchLocateResult(player, world, consumer, null, playerId, search);
}
} catch (WrongEngineBroException | InterruptedException | ExecutionException e) {
} catch (CancellationException ignored) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (WrongEngineBroException | ExecutionException e) {
IrisLogging.reportError(e);
e.printStackTrace();
} finally {
if (search != null && !resultDispatched) {
ACTIVE_LOCATE_REQUESTS.remove(playerId, search);
}
}
}) {
@Override
@@ -604,6 +634,24 @@ public final class EngineBukkitOps {
}.execute(new VolmitSender(player));
}
private static boolean dispatchLocateResult(Player player, World world, Consumer<Location> consumer,
Location location, UUID playerId,
CompletableFuture<Position2> search) {
boolean scheduled = J.runEntity(player, () -> {
if (!ACTIVE_LOCATE_REQUESTS.remove(playerId, search)) {
return;
}
if (!player.isOnline() || !world.equals(player.getWorld())) {
return;
}
consumer.accept(location);
});
if (!scheduled) {
IrisLogging.warn("Could not schedule an Iris locator result for " + player.getName() + ".");
}
return scheduled;
}
private static void teleportAsyncSafely(Player player, Location location) {
if (player == null || location == null) {
return;
@@ -21,11 +21,13 @@ package art.arcane.iris.engine.platform.studio.generators;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Objects;
@@ -43,21 +45,28 @@ public class BiomeBuffetGenerator extends EnginedStudioGenerator {
}
@Override
public void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException {
public synchronized void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException {
int id = Cache.to1D(x / biomeSize, 0, z / biomeSize, width, 1);
if (id >= 0 && id < biomes.length) {
IrisBiome biome = biomes[id];
String foc = engine.getDimension().getFocus();
if (!Objects.equals(foc, biome.getLoadKey())) {
engine.getDimension().setFocus(biome.getLoadKey());
engine.hotloadComplex();
if (id < 0 || id >= biomes.length) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_biome_buffet_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
tc.setRegion(0, 0, 0, 16, 1, 16, FLOOR);
}
return;
}
IrisBiome biome = biomes[id];
String focus = engine.getDimension().getFocus();
if (!Objects.equals(focus, biome.getLoadKey())) {
engine.getDimension().setFocus(biome.getLoadKey());
engine.hotloadComplex();
}
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_biome_buffet_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, tc, true);
} else {
tc.setRegion(0, 0, 0, 16, 1, 16, FLOOR);
}
}
}
@@ -25,6 +25,7 @@ import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.platform.studio.EnginedStudioGenerator;
@@ -38,6 +39,7 @@ import art.arcane.iris.util.common.math.Vector3i;
import org.bukkit.Material;
import org.bukkit.block.Biome;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.project.context.IrisContext;
import java.io.File;
import java.util.LinkedHashMap;
@@ -84,6 +86,13 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
@Override
public void generateChunk(Engine engine, TerrainChunk tc, int x, int z) throws WrongEngineBroException {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_object_studio_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
generateChunkWithinSession(engine, tc, x, z);
}
}
private void generateChunkWithinSession(Engine engine, TerrainChunk tc, int x, int z) {
ensureLayout(engine);
int floorY = Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
@@ -5,8 +5,11 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.volmlib.util.parallel.MultiBurstSupport;
import art.arcane.volmlib.util.math.M;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.Future;
import java.util.function.IntSupplier;
public class MultiBurst extends MultiBurstSupport {
@@ -44,6 +47,29 @@ public class MultiBurst extends MultiBurstSupport {
return worker.getPool() == pool;
}
public <T> CompletableFuture<T> completeValueAsync(Callable<T> task) {
CompletableFuture<T> completion = new CompletableFuture<>();
Future<?> submitted;
try {
submitted = service().submit(() -> {
try {
completion.complete(task.call());
} catch (Throwable exception) {
completion.completeExceptionally(exception);
}
});
} catch (Throwable exception) {
completion.completeExceptionally(exception);
return completion;
}
completion.whenComplete((value, exception) -> {
if (completion.isCancelled()) {
submitted.cancel(true);
}
});
return completion;
}
@Override
public BurstExecutor burst(int estimate) {
return new BurstExecutor(service(), estimate);
@@ -26,6 +26,7 @@ import art.arcane.iris.util.project.matter.slices.IdentifierMatter;
import art.arcane.iris.util.project.matter.slices.PlatformBlockMatter;
import art.arcane.iris.util.project.matter.slices.SpawnerMatter;
import art.arcane.iris.util.project.matter.slices.TileMatter;
import art.arcane.iris.util.project.matter.slices.TreeBlockMaterialMatter;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.block.data.BlockData;
@@ -63,6 +64,7 @@ public final class IrisMatterSupport {
IrisMatter.registerSliceType(new PlatformBlockMatter());
IrisMatter.registerSliceType(new SpawnerMatter());
IrisMatter.registerSliceType(new TileMatter());
IrisMatter.registerSliceType(new TreeBlockMaterialMatter());
registered = true;
}
@@ -0,0 +1,36 @@
package art.arcane.iris.util.project.matter.slices;
import art.arcane.iris.engine.framework.TreeBlockMaterial;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@Sliced
public class TreeBlockMaterialMatter extends RawMatter<TreeBlockMaterial> {
public TreeBlockMaterialMatter() {
this(1, 1, 1);
}
public TreeBlockMaterialMatter(int width, int height, int depth) {
super(width, height, depth, TreeBlockMaterial.class);
}
@Override
public Palette<TreeBlockMaterial> getGlobalPalette() {
return null;
}
@Override
public void writeNode(TreeBlockMaterial material, DataOutputStream output) throws IOException {
output.writeUTF(material.materialKey());
}
@Override
public TreeBlockMaterial readNode(DataInputStream input) throws IOException {
return new TreeBlockMaterial(input.readUTF());
}
}
@@ -0,0 +1,92 @@
package art.arcane.iris.core.loader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDataEngineRegistryTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisData data;
@Before
public void registerPreservationService() {
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
}
@After
public void closeDataAndServices() {
if (data != null) {
data.close();
}
IrisServices.clear();
}
@Test
public void ambiguousRegistrationsRequireMatchingExecutionContext() throws Exception {
data = IrisData.openRuntime(temporaryFolder.newFolder("pack"));
AtomicBoolean firstClosed = new AtomicBoolean(false);
Engine first = engine(firstClosed);
Engine second = engine(new AtomicBoolean(false));
data.registerEngine(first);
data.registerEngine(first);
data.registerEngine(second);
assertEquals(2, data.getEngines().size());
assertNull(data.getEngine());
try (IrisContext.Scope ignored = IrisContext.open(first, 1L, null)) {
assertSame(first, data.getEngine());
}
firstClosed.set(true);
data.cleanupEngine();
assertEquals(1, data.getEngines().size());
assertSame(second, data.getEngine());
data.unregisterEngine(second);
assertNull(data.getEngine());
}
private Engine engine(AtomicBoolean closed) {
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.isClosed()).thenAnswer(ignored -> closed.get());
return engine;
}
private static final class NoOpPreservationRegistry implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
}
@@ -0,0 +1,93 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.IrisSettings;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EngineMaintenanceTest {
@Test
public void normalMaintenanceUsesIdleAgeWithoutAResidentPlateLimit() {
EngineMaintenance.Plan plan = EngineMaintenance.plan(30, 0.50D, false);
assertEquals(30_000L, plan.idleDurationMillis());
assertFalse(plan.multicoreUnload());
assertFalse(plan.heapPressure());
}
@Test
public void negativeKeepAliveIsClampedToImmediateEligibility() {
EngineMaintenance.Plan plan = EngineMaintenance.plan(-1, 0.50D, false);
assertEquals(0L, plan.idleDurationMillis());
assertFalse(plan.multicoreUnload());
assertFalse(plan.heapPressure());
}
@Test
public void risingHeapUsageGraduallyShortensRetention() {
EngineMaintenance.Plan plan = EngineMaintenance.plan(30, 0.87D, false);
assertEquals(15_000L, plan.idleDurationMillis());
assertFalse(plan.multicoreUnload());
assertFalse(plan.heapPressure());
}
@Test
public void heapPressureRequestsImmediateParallelReclamation() {
EngineMaintenance.Plan plan = EngineMaintenance.plan(30, 0.92D, false);
assertEquals(0L, plan.idleDurationMillis());
assertTrue(plan.multicoreUnload());
assertTrue(plan.heapPressure());
}
@Test
public void forcedMulticoreWriteDoesNotShortenNormalRetention() {
EngineMaintenance.Plan plan = EngineMaintenance.plan(30, 0.50D, true);
assertEquals(30_000L, plan.idleDurationMillis());
assertTrue(plan.multicoreUnload());
assertFalse(plan.heapPressure());
}
@Test
public void nestedMantleClosedFailureIsRecognized() {
IllegalStateException cause = new IllegalStateException("Mantle is closed");
RuntimeException failure = new RuntimeException("maintenance failed", cause);
assertTrue(EngineMaintenance.isMantleClosed(failure));
assertFalse(EngineMaintenance.isMantleClosed(new IllegalStateException("unrelated")));
}
@Test
public void configuredParallelismOverridesHardwareSizing() {
IrisSettings.IrisSettingsEngineSVC settings = new IrisSettings.IrisSettingsEngineSVC();
settings.parallelism = 1;
assertEquals(1, settings.getParallelism());
}
@Test
public void configuredParallelismIsBoundedByHardware() {
IrisSettings.IrisSettingsEngineSVC settings = new IrisSettings.IrisSettingsEngineSVC();
settings.parallelism = Integer.MAX_VALUE;
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
int maximumParallelism = processors > Integer.MAX_VALUE / 2
? Integer.MAX_VALUE
: processors * 2;
assertEquals(maximumParallelism, settings.getParallelism());
}
@Test
public void automaticParallelismScalesWithAvailableProcessors() {
IrisSettings.IrisSettingsEngineSVC settings = new IrisSettings.IrisSettingsEngineSVC();
settings.parallelism = 0;
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
assertEquals(Math.max(1, (int) Math.ceil(Math.sqrt(processors))), settings.getParallelism());
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.core.service.tree;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralTree;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisTree;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TreeDefinitionIndexTest {
@Test
public void indexSeparatesOrdinaryProceduralAndStructureOwnership() {
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
IrisRegion region = new IrisRegion();
IrisBiome biome = new IrisBiome();
IrisObjectPlacement explicit = new IrisObjectPlacement();
explicit.getPlace().add("custom/ancient_oak");
explicit.getTrees().add(new IrisTree());
region.getObjects().add(explicit);
IrisProceduralTree proceduralTree = new IrisProceduralTree();
proceduralTree.setName("towering-oak");
proceduralTree.setVariants(2);
IrisProceduralObjects proceduralObjects = new IrisProceduralObjects();
proceduralObjects.getTrees().add(proceduralTree);
biome.setProceduralObjects(proceduralObjects);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getAllRegions(engine)).thenReturn(new KList<IrisRegion>().qadd(region));
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<IrisBiome>().qadd(biome));
TreeDefinitionIndex index = TreeDefinitionIndex.build(engine);
assertTrue(index.isTreeMarker("trees/oak/giant@1"));
assertTrue(index.isTreeMarker("custom/ancient_oak@2"));
assertTrue(index.isTreeMarker("procedural/tree/towering-oak#0@3"));
assertTrue(index.isTreeMarker("procedural/tree/towering-oak#1@4"));
assertFalse(index.isTreeMarker("procedural/towering-oak#0@3"));
assertFalse(index.isTreeMarker("procedural/tree/towering-oak#2@5"));
assertFalse(index.isTreeMarker(StructurePlacementMarker.encodeStructure(
"trees/oak/giant",
1,
"village"
)));
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.core.service.tree;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TreeMarkerTraversalTest {
@Test
public void traversalUsesExactMarkerAndIncludesTriggerOnce() {
String marker = "trees/oak@7";
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(0, 64, 0);
Map<TreeMarkerTraversal.Position, String> markers = new HashMap<>();
markers.put(trigger, marker);
markers.put(new TreeMarkerTraversal.Position(1, 65, 1), marker);
markers.put(new TreeMarkerTraversal.Position(2, 65, 1), "trees/oak@8");
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
trigger,
marker,
-64,
320,
(x, y, z) -> markers.get(new TreeMarkerTraversal.Position(x, y, z))
);
assertTrue(discovery.complete());
assertEquals(2, discovery.members().size());
assertEquals(trigger, discovery.members().getFirst());
assertEquals(1, discovery.members().stream().filter(trigger::equals).count());
}
@Test
public void matchingOwnershipBeyondAxisBoundReportsIncomplete() {
String marker = "trees/giant@99";
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(0, 64, 0);
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
trigger,
marker,
-64,
320,
(x, y, z) -> y == 64 && z == 0 && x >= 0 && x <= TreeMarkerTraversal.MAX_AXIS_DISTANCE + 1
? marker
: null
);
assertFalse(discovery.complete());
assertEquals(TreeMarkerTraversal.MAX_AXIS_DISTANCE + 1, discovery.members().size());
}
@Test
public void connectedMembersAreDiscoveredInOutwardErosionOrder() {
String marker = "trees/giant@12";
TreeMarkerTraversal.Position trigger = new TreeMarkerTraversal.Position(0, 64, 0);
TreeMarkerTraversal.Position near = new TreeMarkerTraversal.Position(1, 64, 0);
TreeMarkerTraversal.Position far = new TreeMarkerTraversal.Position(2, 64, 0);
Map<TreeMarkerTraversal.Position, String> markers = new HashMap<>();
markers.put(trigger, marker);
markers.put(near, marker);
markers.put(far, marker);
TreeMarkerTraversal.Discovery discovery = TreeMarkerTraversal.discover(
trigger,
marker,
-64,
320,
(x, y, z) -> markers.get(new TreeMarkerTraversal.Position(x, y, z))
);
assertEquals(List.of(trigger, near, far), discovery.members());
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.engine;
import org.junit.Test;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class EnginePanicTest {
@Test
public void savedSnapshotDoesNotReadCurrentValues() {
EnginePanic.Diagnostics diagnostics = EnginePanic.scoped("test-world");
diagnostics.add("read-chunk", "Chunk[1]");
diagnostics.saveLast();
diagnostics.add("read-chunk", "Chunk[2]");
assertEquals("Chunk[1]", diagnostics.lastSnapshot().get("read-chunk"));
assertEquals("Chunk[2]", diagnostics.currentSnapshot().get("read-chunk"));
}
@Test
public void currentDiagnosticsAreIsolatedByThread() throws Exception {
EnginePanic.Diagnostics diagnostics = EnginePanic.scoped("test-world");
AtomicReference<Map<String, String>> first = new AtomicReference<>();
AtomicReference<Map<String, String>> second = new AtomicReference<>();
Thread firstThread = new Thread(() -> {
diagnostics.add("read-chunk", "Chunk[10]");
first.set(diagnostics.currentSnapshot());
});
Thread secondThread = new Thread(() -> {
diagnostics.add("read-chunk", "Chunk[20]");
second.set(diagnostics.currentSnapshot());
});
firstThread.start();
firstThread.join();
secondThread.start();
secondThread.join();
assertEquals("Chunk[10]", first.get().get("read-chunk"));
assertEquals("Chunk[20]", second.get().get("read-chunk"));
assertTrue(diagnostics.currentSnapshot().isEmpty());
}
}
@@ -4,25 +4,15 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.data.BlockData;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -32,17 +22,6 @@ public class IrisComplexImplodeParityTest {
@BeforeClass
public static void setup() throws Exception {
if (Bukkit.getServer() == null) {
Server server = mock(Server.class);
doReturn(Logger.getLogger("IrisTest")).when(server).getLogger();
doReturn("IrisTestServer").when(server).getName();
doReturn("1.0").when(server).getVersion();
doReturn("1.0").when(server).getBukkitVersion();
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
Bukkit.setServer(server);
}
Class<?> childSelectionClass = Class.forName("art.arcane.iris.engine.IrisComplex$ChildSelectionPlan");
childSelectionCreateMethod = childSelectionClass.getDeclaredMethod("create", KList.class);
childSelectionCreateMethod.setAccessible(true);
@@ -50,13 +29,6 @@ public class IrisComplexImplodeParityTest {
childSelectionSelectMethod.setAccessible(true);
}
private static BlockData namedBlockData(String key) {
String canonical = key.indexOf(':') >= 0 ? key : "minecraft:" + key;
BlockData data = mock(BlockData.class);
doReturn(canonical).when(data).getAsString();
return data;
}
@Test
public void selectionPlanMatchesLegacyFitRarityAcrossSeedAndCoordinateGrid() throws Exception {
List<KList<IrisBiome>> scenarios = buildScenarios();
@@ -0,0 +1,65 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.junit.Test;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisComplexSurfaceBiomeTest {
@Test
public void shorelineHeightSelectsShoreBiome() {
IrisBiome base = mock(IrisBiome.class);
IrisBiome shore = mock(IrisBiome.class);
IrisRegion region = mock(IrisRegion.class);
doReturn(false).when(base).isShore();
doReturn(3D).when(region).getShoreHeight(12D, 18D);
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
63D,
base,
region,
12D,
18D,
63D,
constant(base),
constant(base),
constant(shore));
assertSame(shore, resolved);
}
@Test
public void raisedAquaticBiomeReturnsToLand() {
IrisBiome aquatic = mock(IrisBiome.class);
IrisBiome land = mock(IrisBiome.class);
IrisRegion region = mock(IrisRegion.class);
doReturn(false).when(aquatic).isShore();
doReturn(false).when(aquatic).isLand();
doReturn(1D).when(region).getShoreHeight(4D, 7D);
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
66D,
aquatic,
region,
4D,
7D,
63D,
constant(land),
constant(aquatic),
constant(aquatic));
assertSame(land, resolved);
}
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
@SuppressWarnings("unchecked")
ProceduralStream<IrisBiome> stream = mock(ProceduralStream.class);
doReturn(biome).when(stream).get(anyDouble(), anyDouble());
return stream;
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.object.IrisEngineData;
import com.google.gson.Gson;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class IrisEngineDataPersistenceTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void atomicWriteCreatesAndReplacesEngineData() throws Exception {
File folder = temporaryFolder.newFolder("engine-data");
File output = new File(folder, "dimension.json");
IrisEngineData first = new IrisEngineData();
first.getStatistics().setVersion(10);
IrisEngine.writeEngineDataAtomically(output, first);
IrisEngineData firstRead = new Gson().fromJson(Files.readString(output.toPath()), IrisEngineData.class);
assertEquals(10, firstRead.getStatistics().getVersion());
IrisEngineData replacement = new IrisEngineData();
replacement.getStatistics().setVersion(20);
IrisEngine.writeEngineDataAtomically(output, replacement);
IrisEngineData replacementRead = new Gson().fromJson(Files.readString(output.toPath()), IrisEngineData.class);
assertEquals(20, replacementRead.getStatistics().getVersion());
File[] temporaryFiles = folder.listFiles((ignored, name) -> name.endsWith(".tmp"));
assertFalse(temporaryFiles != null && temporaryFiles.length > 0);
}
@Test(expected = IOException.class)
public void atomicWriteRejectsParentlessPath() throws Exception {
IrisEngine.writeEngineDataAtomically(new File("parentless-engine-data.json"), new IrisEngineData());
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.engine;
import org.junit.Test;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisEngineLifecycleGateTest {
@Test
public void incompleteBackgroundDrainBlocksResourceRelease() {
IrisEngine.BackgroundTaskDrain drain = new IrisEngine.BackgroundTaskDrain(
new TimeoutException("still running"), false);
assertFalse(drain.allowsResourceRelease());
}
@Test
public void completedFailedTaskAllowsSafeResourceRelease() {
IrisEngine.BackgroundTaskDrain drain = new IrisEngine.BackgroundTaskDrain(
new IllegalStateException("completed exceptionally"), true);
assertTrue(drain.allowsResourceRelease());
}
}
@@ -2,7 +2,15 @@ package art.arcane.iris.engine;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisWorldManagerMarkerTest {
@Test
@@ -18,4 +26,50 @@ public class IrisWorldManagerMarkerTest {
assertEquals(255, IrisWorldManager.toWorldY(319, -64));
assertEquals(42, IrisWorldManager.toWorldY(42, 0));
}
@Test
public void completedEntityTasksAreAccepted() {
assertTrue(IrisWorldManager.awaitEntityTasks(new CountDownLatch(0), 0, TimeUnit.MILLISECONDS));
}
@Test
public void incompleteEntityTasksAreRejected() {
assertFalse(IrisWorldManager.awaitEntityTasks(new CountDownLatch(1), 0, TimeUnit.MILLISECONDS));
}
@Test
public void interruptedEntityWaitPreservesInterruptStatus() throws Exception {
AtomicBoolean preserved = new AtomicBoolean();
Thread thread = new Thread(() -> {
Thread.currentThread().interrupt();
boolean completed = IrisWorldManager.awaitEntityTasks(new CountDownLatch(1), 1, TimeUnit.SECONDS);
preserved.set(!completed && Thread.currentThread().isInterrupted());
});
thread.start();
thread.join();
assertTrue(preserved.get());
}
@Test
public void deferredDropsUseTheRouteAndFallbackOnlyWhenDeclined() {
List<String> routed = new ArrayList<>();
List<String> fallback = new ArrayList<>();
IrisWorldManager.routeDrops(
List.of("routed", "fallback"),
drop -> {
if (drop.equals("routed")) {
routed.add((String) drop);
return true;
}
return false;
},
fallback::add
);
assertEquals(List.of("routed"), routed);
assertEquals(List.of("fallback"), fallback);
}
}
@@ -0,0 +1,132 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import org.bukkit.Chunk;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
public class EngineAssignedWorldManagerLifecycleTest {
@Test
public void failedRegistrationStillAttemptsListenerRollback() {
TestWorldManager manager = new TestWorldManager(mock(Engine.class));
manager.failRegister = true;
assertThrows(IllegalStateException.class, manager::start);
assertEquals(1, manager.unregisterAttempts);
assertEquals(0, manager.cancelAttempts);
}
@Test
public void closeRetriesOnlyIncompleteManagerResources() {
TestWorldManager manager = new TestWorldManager(mock(Engine.class));
manager.start();
manager.failUnregister = true;
manager.failCancel = true;
assertThrows(IllegalStateException.class, manager::close);
assertEquals(1, manager.unregisterAttempts);
assertEquals(1, manager.cancelAttempts);
manager.failUnregister = false;
manager.failCancel = false;
manager.close();
assertEquals(2, manager.unregisterAttempts);
assertEquals(2, manager.cancelAttempts);
manager.close();
assertEquals(2, manager.unregisterAttempts);
assertEquals(2, manager.cancelAttempts);
}
private static final class TestWorldManager extends EngineAssignedWorldManager {
private boolean failRegister;
private boolean failUnregister;
private boolean failCancel;
private int unregisterAttempts;
private int cancelAttempts;
private TestWorldManager(Engine engine) {
super(engine);
}
@Override
protected void registerManagerListener() {
if (failRegister) {
throw new IllegalStateException("registration failure");
}
}
@Override
protected int scheduleManagerTick(Runnable tick) {
return 42;
}
@Override
protected void unregisterManagerListener() {
unregisterAttempts++;
if (failUnregister) {
throw new IllegalStateException("listener failure");
}
}
@Override
protected void cancelManagerTick(int scheduledTaskId) {
cancelAttempts++;
if (failCancel) {
throw new IllegalStateException("scheduler failure");
}
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0.0;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent event) {
}
@Override
public void onBlockPlace(BlockPlaceEvent event) {
}
@Override
public void onChunkLoad(Chunk chunk, boolean generated) {
}
@Override
public void onChunkUnload(Chunk chunk) {
}
@Override
public void teleportAsync(PlayerTeleportEvent event) {
}
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class EngineLifecycleTasksTest {
@Test
public void activeLifecycleTaskHoldsGenerationDrainUntilCompletion() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
Engine engine = mock(Engine.class);
when(engine.acquireGenerationLease(anyString()))
.thenAnswer(invocation -> manager.acquire(invocation.getArgument(0)));
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicReference<Throwable> taskFailure = new AtomicReference<>();
AtomicReference<Engine> contextEngine = new AtomicReference<>();
Thread task = new Thread(() -> {
try {
EngineLifecycleTasks.run(engine, "world_manager_test", () -> {
contextEngine.set(IrisContext.get().getEngine());
entered.countDown();
try {
release.await();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
});
} catch (Throwable exception) {
taskFailure.set(exception);
}
});
task.start();
assertTrue(entered.await(1L, TimeUnit.SECONDS));
CountDownLatch drained = new CountDownLatch(1);
Thread sealer = new Thread(() -> {
try {
manager.sealAndAwait("hotload", 1_000L);
drained.countDown();
} catch (GenerationSessionException exception) {
taskFailure.set(exception);
}
});
sealer.start();
waitForSeal(manager);
assertFalse(drained.await(50L, TimeUnit.MILLISECONDS));
release.countDown();
assertTrue(drained.await(1L, TimeUnit.SECONDS));
task.join(1_000L);
sealer.join(1_000L);
assertSame(engine, contextEngine.get());
assertTrue(taskFailure.get() == null);
}
@Test
public void sealedLifecycleRejectsQueuedManagerTask() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
manager.sealAndAwait("hotload", 1_000L);
Engine engine = mock(Engine.class);
when(engine.acquireGenerationLease(anyString()))
.thenAnswer(invocation -> manager.acquire(invocation.getArgument(0)));
when(engine.isClosing()).thenReturn(true);
AtomicBoolean ran = new AtomicBoolean();
boolean accepted = EngineLifecycleTasks.run(engine, "queued_world_manager_test", () -> ran.set(true));
assertFalse(accepted);
assertFalse(ran.get());
}
private void waitForSeal(GenerationSessionManager manager) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
while (System.nanoTime() < deadline) {
try (GenerationSessionLease ignored = manager.acquire("seal_probe")) {
Thread.onSpinWait();
} catch (GenerationSessionException expected) {
return;
}
}
throw new AssertionError("Generation session did not seal within one second.");
}
}
@@ -0,0 +1,186 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class EngineTelemetrySnapshotTest {
@Test
public void aggregatesMultipleWorldsWithExplicitSumMaxAndMeanRules() {
EngineTelemetrySnapshot first = snapshot(
"minecraft:overworld",
"world",
true,
false,
12L,
3L,
0.25D,
4L,
100L,
2D,
5L,
2,
1,
1L,
4L,
1L,
10D,
Map.of("total", 5D, "terrain", 2D)
);
EngineTelemetrySnapshot second = snapshot(
"minecraft:the_nether",
"world_nether",
false,
true,
8L,
5L,
0.75D,
6L,
200L,
3D,
7L,
4,
2,
2L,
6L,
3L,
30D,
Map.of("total", 8D, "terrain", 1D)
);
EngineTelemetrySnapshot.Aggregate aggregate = EngineTelemetrySnapshot.aggregate(List.of(first, second));
assertEquals(2, aggregate.worldCount());
assertEquals(1, aggregate.active());
assertEquals(1, aggregate.studio());
assertEquals(1, aggregate.failed());
assertEquals(20L, aggregate.loadedChunks());
assertEquals(8L, aggregate.loadedEntities());
assertEquals(0.75D, aggregate.entitySaturationMax(), 0D);
assertEquals(10L, aggregate.generatedSession());
assertEquals(300L, aggregate.generatedTotal());
assertEquals(5D, aggregate.chunksPerSecond(), 0D);
assertEquals(12L, aggregate.blockUpdatesPerSecond());
assertEquals(6, aggregate.parallelism());
assertEquals(3, aggregate.activeGenerationLeases());
assertEquals(3L, aggregate.hotloadsTotal());
assertEquals(10L, aggregate.mantleResidentPlates());
assertEquals(4L, aggregate.mantleQueuedPlates());
assertEquals(20D, aggregate.mantleIdleAverageMs(), 0D);
assertEquals(30D, aggregate.mantleIdleMaxMs(), 0D);
assertEquals(10D, aggregate.mantleIdleMinMs(), 0D);
assertEquals(8D, aggregate.generationTimingMaximaMs().get("total"), 0D);
assertEquals(2D, aggregate.generationTimingMaximaMs().get("terrain"), 0D);
}
@Test
public void normalizesInvalidRuntimeCountersAtSnapshotBoundary() {
EngineTelemetrySnapshot snapshot = new EngineTelemetrySnapshot(
1L,
"minecraft:overworld",
"world",
"overworld",
true,
false,
false,
false,
-1L,
-1L,
Double.NaN,
-1L,
-1L,
Double.POSITIVE_INFINITY,
-1L,
-1,
-1,
-1L,
-1L,
-1L,
Double.NaN,
Map.of("bad", Double.NaN, "negative", -1D, "valid", 2D)
);
assertEquals(0L, snapshot.loadedChunks());
assertEquals(0D, snapshot.entitySaturation(), 0D);
assertEquals(0D, snapshot.chunksPerSecond(), 0D);
assertEquals(Map.of("valid", 2D), snapshot.generationTimingsMs());
}
@Test
public void aggregateWorldCountIgnoresMissingSnapshots() {
List<EngineTelemetrySnapshot> snapshots = new ArrayList<>();
snapshots.add(snapshot(
"minecraft:overworld",
"world",
true,
false,
0L,
0L,
0D,
0L,
0L,
0D,
0L,
0,
0,
0L,
0L,
0L,
0D,
Map.of()
));
snapshots.add(null);
assertEquals(1, EngineTelemetrySnapshot.aggregate(snapshots).worldCount());
}
private static EngineTelemetrySnapshot snapshot(
String identity,
String name,
boolean active,
boolean studio,
long chunks,
long entities,
double saturation,
long generatedSession,
long generatedTotal,
double chunksPerSecond,
long blockUpdates,
int parallelism,
int leases,
long hotloads,
long resident,
long queued,
double idleMs,
Map<String, Double> timings
) {
return new EngineTelemetrySnapshot(
1_000L,
identity,
name,
"dimension",
active,
studio,
false,
!active,
chunks,
entities,
saturation,
generatedSession,
generatedTotal,
chunksPerSecond,
blockUpdates,
parallelism,
leases,
hotloads,
resident,
queued,
idleMs,
timings
);
}
}
@@ -4,8 +4,11 @@ import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotEquals;
public class GenerationSessionManagerTest {
@Test
@@ -44,4 +47,56 @@ public class GenerationSessionManagerTest {
manager.sealAndAwait("close", 1000L, true);
}
@Test
public void activatingAfterSealPublishesAnIndependentSession() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
long sealedSession = manager.currentSessionId();
manager.sealAndAwait("hotload", 1000L);
manager.activateNextSession();
try (GenerationSessionLease lease = manager.acquire("chunk_generate")) {
assertNotEquals(sealedSession, lease.sessionId());
}
}
@Test
public void nestedWorkCanContinueAnAlreadyLeasedSessionAfterSeal() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
GenerationSessionLease outer = manager.acquire("chunk_pipeline");
AtomicReference<Throwable> sealFailure = new AtomicReference<>();
Thread sealer = new Thread(() -> {
try {
manager.sealAndAwait("hotload", 1000L);
} catch (Throwable exception) {
sealFailure.set(exception);
}
});
sealer.start();
waitForSeal(manager);
try (GenerationSessionLease nested = manager.continueSession("biome_lookup", outer.sessionId())) {
assertEquals(outer.sessionId(), nested.sessionId());
}
outer.close();
sealer.join(1000L);
assertTrue(!sealer.isAlive());
if (sealFailure.get() != null) {
throw new AssertionError("Session seal failed", sealFailure.get());
}
}
private void waitForSeal(GenerationSessionManager manager) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
while (System.nanoTime() < deadline) {
try (GenerationSessionLease ignored = manager.acquire("seal_probe")) {
Thread.onSpinWait();
} catch (GenerationSessionException expected) {
return;
}
}
throw new AssertionError("Generation session did not seal within one second.");
}
}
@@ -22,6 +22,9 @@ import java.io.File;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
@@ -74,7 +77,7 @@ public class HintedLocatorTest {
@Before
@SuppressWarnings("unchecked")
public void setup() {
public void setup() throws Exception {
engine = mock(Engine.class);
dimension = mock(IrisDimension.class);
complex = mock(IrisComplex.class);
@@ -86,6 +89,7 @@ public class HintedLocatorTest {
when(engine.getFocus()).thenReturn(null);
when(engine.getFocusRegion()).thenReturn(null);
when(engine.isClosed()).thenReturn(false);
when(engine.acquireGenerationLease(any(String.class))).thenReturn(GenerationSessionLease.noop());
when(data.getBiomeLoader()).thenReturn(biomeLoader);
}
@@ -371,6 +375,35 @@ public class HintedLocatorTest {
assertTrue(elapsed < 10_000);
}
@Test
public void startingAnotherSearchDoesNotCancelTheActiveRequest() throws Exception {
CountDownLatch planning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
Position2 origin = new Position2(4, -7);
Locator<String> firstLocator = new HintedLocator<>((ignoredEngine, chunk) -> chunk.equals(origin), ignoredEngine -> {
planning.countDown();
try {
release.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return HintedLocator.SearchPlan.impossible();
}
return HintedLocator.SearchPlan.unpruned();
});
Locator<String> secondLocator = new HintedLocator<>((ignoredEngine, chunk) -> false,
ignoredEngine -> HintedLocator.SearchPlan.impossible());
Future<Position2> first = firstLocator.find(engine, origin, 60_000, (Integer count) -> {
});
assertTrue(planning.await(10, TimeUnit.SECONDS));
Future<Position2> second = secondLocator.find(engine, origin, 60_000, (Integer count) -> {
});
release.countDown();
assertNull(second.get(10, TimeUnit.SECONDS));
assertEquals(origin, first.get(10, TimeUnit.SECONDS));
}
@Test
public void findLocatesObjectThroughCoarseCascade() throws Exception {
IrisRegion regionA = region("reg_a");
@@ -0,0 +1,44 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class LocatorCancellerTest {
@Test
public void cancellingOneRequestDoesNotCancelAnother() throws Exception {
CompletableFuture<String> firstDelegate = new CompletableFuture<>();
CompletableFuture<String> secondDelegate = new CompletableFuture<>();
AtomicBoolean firstStop = new AtomicBoolean();
AtomicBoolean secondStop = new AtomicBoolean();
Future<String> first = LocatorCanceller.requestScoped(firstDelegate, firstStop);
Future<String> second = LocatorCanceller.requestScoped(secondDelegate, secondStop);
assertTrue(first.cancel(false));
secondDelegate.complete("found");
assertTrue(firstStop.get());
assertTrue(first.isCancelled());
assertTrue(firstDelegate.isCancelled());
assertFalse(secondStop.get());
assertFalse(second.isCancelled());
assertEquals("found", second.get());
}
@Test
public void completedRequestCannotBeCancelled() throws Exception {
CompletableFuture<String> delegate = CompletableFuture.completedFuture("done");
AtomicBoolean stop = new AtomicBoolean();
Future<String> request = LocatorCanceller.requestScoped(delegate, stop);
assertFalse(request.cancel(false));
assertFalse(stop.get());
assertEquals("done", request.get());
}
}
@@ -0,0 +1,16 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class TreeBlockMaterialTest {
@Test
public void materialComparisonIgnoresBlockStateProperties() {
TreeBlockMaterial expected = TreeBlockMaterial.of("minecraft:oak_log[axis=y]");
assertTrue(expected.matches("minecraft:oak_log[axis=x]"));
assertFalse(expected.matches("minecraft:spruce_log[axis=y]"));
}
}
@@ -0,0 +1,32 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import com.google.gson.Gson;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeInferenceIsolationTest {
@Test
public void conflictingRolesUseStableCachedVariants() {
Gson gson = new Gson();
IrisData data = mock(IrisData.class);
when(data.getGson()).thenReturn(gson);
IrisBiome biome = gson.fromJson("{\"name\":\"Shared\"}", IrisBiome.class);
biome.setLoader(data);
biome.setLoadKey("shared");
IrisBiome land = biome.withInferredType(InferredType.LAND);
IrisBiome sea = biome.withInferredType(InferredType.SEA);
assertSame(biome, land);
assertNotSame(land, sea);
assertSame(sea, biome.withInferredType(InferredType.SEA));
assertEquals(InferredType.LAND, land.getInferredType());
assertEquals(InferredType.SEA, sea.getInferredType());
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisExpressionLoadEngineCacheTest {
@Test
public void engineValuesAreCachedPerEngineIdentity() {
Engine first = mock(Engine.class);
Engine second = mock(Engine.class);
when(first.getHeight()).thenReturn(128);
when(second.getHeight()).thenReturn(320);
AtomicReference<Engine> active = new AtomicReference<>(first);
IrisData data = data(active);
IrisExpressionLoad load = new IrisExpressionLoad()
.setName("height")
.setEngineValue(IrisEngineValueType.ENGINE_HEIGHT);
assertEquals(128D, load.getValue(new RNG(1L), data, 0D, 0D), 0D);
active.set(second);
assertEquals(320D, load.getValue(new RNG(1L), data, 0D, 0D), 0D);
}
@Test
public void engineStreamsAreCachedPerEngineIdentity() {
Engine first = streamEngine(12D);
Engine second = streamEngine(27D);
AtomicReference<Engine> active = new AtomicReference<>(first);
IrisData data = data(active);
IrisExpressionLoad load = new IrisExpressionLoad()
.setName("height")
.setEngineStreamValue(IrisEngineStreamType.HEIGHT);
assertEquals(12D, load.getValue(new RNG(1L), data, 4D, 8D), 0D);
active.set(second);
assertEquals(27D, load.getValue(new RNG(1L), data, 4D, 8D), 0D);
}
@Test(expected = IllegalStateException.class)
public void engineBackedValueFailsWithoutUnambiguousEngine() {
IrisData data = mock(IrisData.class);
IrisExpressionLoad load = new IrisExpressionLoad()
.setName("height")
.setEngineValue(IrisEngineValueType.ENGINE_HEIGHT);
load.getValue(new RNG(1L), data, 0D, 0D);
}
private IrisData data(AtomicReference<Engine> active) {
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenAnswer(ignored -> active.get());
return data;
}
private Engine streamEngine(double value) {
ProceduralStream<Double> stream = ProceduralStream.ofDouble((x, z) -> value);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getHeightStream()).thenReturn(stream);
Engine engine = mock(Engine.class);
when(engine.getComplex()).thenReturn(complex);
return engine;
}
}
@@ -0,0 +1,59 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisGeneratorStyleCacheIsolationTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void cacheSeparatesSeedsAndEngineIdentity() throws Exception {
Engine firstEngine = mock(Engine.class);
Engine secondEngine = mock(Engine.class);
AtomicReference<Engine> activeEngine = new AtomicReference<>(firstEngine);
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenAnswer(ignored -> activeEngine.get());
when(data.getDataFolder()).thenReturn(temporaryFolder.newFolder("pack"));
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.SIMPLEX);
CNG first = style.create(new RNG(41L), data);
CNG firstAgain = style.create(new RNG(41L), data);
CNG differentSeed = style.create(new RNG(42L), data);
activeEngine.set(secondEngine);
CNG differentEngine = style.create(new RNG(41L), data);
assertSame(first, firstAgain);
assertNotSame(first, differentSeed);
assertNotSame(first, differentEngine);
assertNotEquals(first.noise(12D, -7D), differentSeed.noise(12D, -7D), 0D);
}
@Test
public void cacheEvictsOldEngineSeedEntries() {
IrisData data = mock(IrisData.class);
Engine engine = mock(Engine.class);
when(data.getEngine()).thenReturn(engine);
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.SIMPLEX);
CNG first = style.create(new RNG(0L), data);
for (long seed = 1L; seed <= 8L; seed++) {
style.create(new RNG(seed), data);
}
assertNotSame(first, style.create(new RNG(0L), data));
}
}
@@ -0,0 +1,56 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectPlacementSurfaceWarpCacheTest {
@Test
public void surfaceWarpSeparatesEngineSeeds() {
Engine first = engine(100L);
Engine second = engine(200L);
AtomicReference<Engine> active = new AtomicReference<>(first);
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenAnswer(ignored -> active.get());
IrisObjectPlacement placement = placement();
CNG firstWarp = placement.getSurfaceWarp(new RNG(1L), data);
assertSame(firstWarp, placement.getSurfaceWarp(new RNG(999L), data));
active.set(second);
assertNotSame(firstWarp, placement.getSurfaceWarp(new RNG(1L), data));
}
@Test
public void standaloneSurfaceWarpUsesSuppliedSeed() {
IrisData data = mock(IrisData.class);
IrisObjectPlacement placement = placement();
CNG first = placement.getSurfaceWarp(new RNG(1L), data);
assertSame(first, placement.getSurfaceWarp(new RNG(1L), data));
assertNotSame(first, placement.getSurfaceWarp(new RNG(2L), data));
}
private IrisObjectPlacement placement() {
return new IrisObjectPlacement().setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX));
}
private Engine engine(long componentSeed) {
SeedManager seedManager = mock(SeedManager.class);
when(seedManager.getComponent()).thenReturn(componentSeed);
Engine engine = mock(Engine.class);
when(engine.getSeedManager()).thenReturn(seedManager);
return engine;
}
}
@@ -21,6 +21,14 @@ import static org.junit.Assert.assertTrue;
public class ProceduralTreeGeneratorTest {
@Test
public void variantsUseTreeSpecificProceduralOwnershipKeys() {
IrisProceduralTree tree = new IrisProceduralTree();
tree.setName("towering-oak");
assertEquals("procedural/tree/towering-oak#3", tree.getVariantLoadKey(3));
}
@Test
public void trunkBuildsConnectedColumnOfRequestedHeight() {
IrisProceduralTree tree = new IrisProceduralTree();