mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+147
-35
@@ -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 {
|
||||
|
||||
+2
@@ -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
@@ -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;
|
||||
|
||||
+19
-10
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+132
@@ -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());
|
||||
}
|
||||
}
|
||||
+73
@@ -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;
|
||||
}
|
||||
}
|
||||
+59
@@ -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));
|
||||
}
|
||||
}
|
||||
+56
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user