Preserved terrain across updates

- Saved chunks retain their natural geometry and generation ownership
- Future chunks blend against frozen terrain boundaries
- Build revisions capture generation source and dependencies automatically
- Native terrain queries use contained transition geometry and final height caches
- Historical searches retain recorded pack identities and placement facts
- World creation and Studio updates preserve native lifecycle ordering
- Hydrology containment and scoped content workers retain generation context
- Pregeneration validates saved completion before trusting cached progress
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-05 11:57:01 -04:00
1 parent 20ad49b9d2
commit b1e1fbc15e
214 files changed
+11985 -4082

No files matched your search

@@ -22,6 +22,11 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -53,7 +58,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 3;
private static final int INPUT_FINGERPRINT_SCHEMA = 4;
private static final int INPUT_BUFFER_BYTES = 64 * 1024;
private static final int WORLD_PACK_SCAN_DEPTH = 8;
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
@@ -177,6 +182,9 @@ public final class IrisDatapackCompiler {
}
}
}
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
updateDigestString(digest, contract.fingerprint());
}
return HexFormat.of().formatHex(digest.digest());
}
@@ -220,6 +228,14 @@ public final class IrisDatapackCompiler {
data.close();
}
}
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
for (PhysicalResourceKey key : contract.generatedSources().keySet()) {
putRegistryRequirement(requirements, registryPath(key) + "/" + key.resourceKey(),
fingerprintContent(GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer)));
}
contract.biomeTags().forEach((biome, tags) ->
biomeTags.computeIfAbsent(biome, ignored -> new TreeSet<>()).addAll(tags));
}
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
@@ -494,11 +510,111 @@ public final class IrisDatapackCompiler {
}
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
installRetainedRegistrySources(packRoots, datapackRoots, fixer);
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
return new CompilationResult(packCount, dimensionCount, countBiomes(biomes));
}
public static void installRetainedRegistrySources(List<File> packRoots, Collection<File> datapackRoots,
IDataFixer fixer) throws IOException {
Map<PhysicalResourceKey, String> sources = new TreeMap<>();
Map<String, Set<String>> tags = new TreeMap<>();
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
for (PhysicalResourceKey key : contract.generatedSources().keySet()) {
String source = GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer);
String previous = sources.putIfAbsent(key, source);
if (previous != null && !JsonParser.parseString(previous).equals(JsonParser.parseString(source))) {
throw new IOException("Conflicting retained registry source for " + key);
}
}
contract.biomeTags().forEach((biome, memberships) -> {
for (String tag : memberships) {
tags.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(biome);
}
});
}
for (File root : datapackRoots) {
for (Map.Entry<PhysicalResourceKey, String> entry : sources.entrySet()) {
Path output = registryOutput(root.toPath(), registryPath(entry.getKey()), entry.getKey().resourceKey());
if (Files.exists(output)) {
String previous = Files.readString(output, StandardCharsets.UTF_8);
if (!JsonParser.parseString(previous).equals(JsonParser.parseString(entry.getValue()))) {
throw new IOException("Conflicting retained registry output at " + output);
}
continue;
}
Files.createDirectories(output.getParent());
Files.writeString(output, entry.getValue(), StandardCharsets.UTF_8);
}
for (Map.Entry<String, Set<String>> entry : tags.entrySet()) {
writeRetainedBiomeTag(registryOutput(root.toPath(), "tags/worldgen/biome", entry.getKey()), entry.getValue());
}
}
}
private static void writeRetainedBiomeTag(Path output, Set<String> biomes) throws IOException {
Set<String> merged = new TreeSet<>(biomes);
if (Files.exists(output)) {
JsonObject existing = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
for (JsonElement value : existing.getAsJsonArray("values")) {
merged.add(value.getAsString());
}
}
JsonArray values = new JsonArray();
merged.forEach(values::add);
JsonObject tag = new JsonObject();
tag.addProperty("replace", false);
tag.add("values", values);
Files.createDirectories(output.getParent());
Files.writeString(output, tag.toString(), StandardCharsets.UTF_8);
}
private static Path registryOutput(Path root, String registryPath, String resourceKey) throws IOException {
int separator = resourceKey.indexOf(':');
Path directory = root.toAbsolutePath().normalize().resolve("data")
.resolve(resourceKey.substring(0, separator)).resolve(registryPath);
Path output = directory.resolve(resourceKey.substring(separator + 1) + ".json").normalize();
if (!output.startsWith(directory)) {
throw new IOException("Unsafe retained registry resource key " + resourceKey);
}
return output;
}
private static String registryPath(PhysicalResourceKey key) {
return key.registryKey().substring(key.registryKey().indexOf(':') + 1);
}
private static List<GenerationRegistryContract> retainedRegistryContracts(List<File> packRoots) throws IOException {
Map<String, GenerationRegistryContract> contracts = new TreeMap<>();
Set<Path> loadedWorlds = new LinkedHashSet<>();
for (File pack : packRoots) {
Path dimensionRoot = historyDimensionRoot(pack.toPath());
if (dimensionRoot == null || !loadedWorlds.add(dimensionRoot)) {
continue;
}
for (GenerationEpoch epoch : GenerationHistory.open(dimensionRoot).manifest().epochs()) {
contracts.putIfAbsent(epoch.registryContract().fingerprint(), epoch.registryContract());
}
}
return List.copyOf(contracts.values());
}
private static Path historyDimensionRoot(Path packRoot) {
Path pack = packRoot.toAbsolutePath().normalize();
Path epoch = pack.getParent();
Path epochs = epoch == null ? null : epoch.getParent();
Path generation = epochs == null ? null : epochs.getParent();
Path iris = generation == null ? null : generation.getParent();
if (iris == null || iris.getParent() == null || !"pack".equals(pack.getFileName().toString())
|| !"epochs".equals(epochs.getFileName().toString())
|| !"generation".equals(generation.getFileName().toString())
|| !"iris".equals(iris.getFileName().toString())) {
return null;
}
return iris.getParent();
}
private static void installLevelStemBindings(
List<IrisGeneratorBinding> bindings,
Map<String, List<DimensionCandidate>> dimensions,
@@ -648,11 +764,9 @@ public final class IrisDatapackCompiler {
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(generationRoot))) {
GenerationHistory history = GenerationHistory.open(worldRoot);
for (GenerationEpoch epoch : history.manifest().epochs()) {
Path retainedPack = history.paths().packRoot(epoch.epochId());
if (hasDimensions(retainedPack)) {
candidates.add(retainedPack);
}
candidates.add(history.activePackRoot());
if (history.pendingActivation().isPresent()) {
candidates.add(history.packRoot(history.pendingActivation().orElseThrow().activationId()));
}
return true;
}
@@ -10,25 +10,50 @@ public final class IrisStartupValidation {
}
public static synchronized void begin() {
snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of());
snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of(), ValidationState.DISABLED, "");
}
public static synchronized void disable() {
snapshot = Snapshot.disabled();
}
public static synchronized void beginRuntimeValidation() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.PENDING, "");
}
public static synchronized void markRuntimeReady() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.READY, "");
}
public static synchronized void markRuntimeInvalid(String failure) {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.INVALID,
normalizeFailure(failure, "Iris runtime injection failed. Resolve the startup errors and restart the server."));
}
public static synchronized void beginDatapackValidation() {
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
return;
}
snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures());
snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markDatapacksReady() {
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
return;
}
snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures());
snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markDatapacksInvalid(String failure) {
@@ -40,7 +65,7 @@ public final class IrisStartupValidation {
ValidationState.INVALID,
snapshot.packs(),
List.of(normalizeFailure(failure, "External datapack validation failed.")),
snapshot.packFailures());
snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void requireRestart(String reason) {
@@ -52,14 +77,14 @@ public final class IrisStartupValidation {
ValidationState.RESTART_REQUIRED,
snapshot.packs(),
List.of(normalizeFailure(reason, "A restart is required to load validated datapacks.")),
snapshot.packFailures());
snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markPacksReady() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of());
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markPacksInvalid(List<String> failures) {
@@ -72,7 +97,7 @@ public final class IrisStartupValidation {
.map(failure -> normalizeFailure(failure, "Iris dimension-pack validation failed."))
.toList();
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID,
snapshot.datapackFailures(), normalized);
snapshot.datapackFailures(), normalized, snapshot.runtime(), snapshot.runtimeFailure());
}
public static boolean isReady() {
@@ -92,6 +117,7 @@ public final class IrisStartupValidation {
Snapshot current = snapshot;
if (force
&& current.enforced()
&& isRuntimeReady(current)
&& current.datapacks() == ValidationState.RESTART_REQUIRED
&& current.packs() == ValidationState.READY) {
return Optional.empty();
@@ -103,6 +129,11 @@ public final class IrisStartupValidation {
if (!current.enforced() || isReady(current)) {
return Optional.empty();
}
if (!isRuntimeReady(current)) {
return Optional.of(current.runtime() == ValidationState.INVALID
? current.runtimeFailure()
: "Iris is still validating runtime injection.");
}
if (current.datapacks() == ValidationState.RESTART_REQUIRED) {
return Optional.of(firstFailure(current.datapackFailures(),
"Iris installed validated datapacks and requires a restart before the server is safe."));
@@ -131,6 +162,7 @@ public final class IrisStartupValidation {
public static void requireWorldReplacementStagingReady() {
Snapshot current = snapshot;
if (current.enforced()
&& isRuntimeReady(current)
&& current.datapacks() == ValidationState.RESTART_REQUIRED
&& current.packs() == ValidationState.READY) {
return;
@@ -150,9 +182,14 @@ public final class IrisStartupValidation {
return failures == null || failures.isEmpty() ? fallback : failures.getFirst();
}
private static boolean isRuntimeReady(Snapshot current) {
return current.runtime() == ValidationState.DISABLED || current.runtime() == ValidationState.READY;
}
private static boolean isReady(Snapshot current) {
return !current.enforced()
|| current.datapacks() == ValidationState.READY
|| isRuntimeReady(current)
&& current.datapacks() == ValidationState.READY
&& current.packs() == ValidationState.READY;
}
@@ -169,7 +206,9 @@ public final class IrisStartupValidation {
ValidationState datapacks,
ValidationState packs,
List<String> datapackFailures,
List<String> packFailures
List<String> packFailures,
ValidationState runtime,
String runtimeFailure
) {
Snapshot {
datapackFailures = List.copyOf(datapackFailures);
@@ -177,7 +216,7 @@ public final class IrisStartupValidation {
}
private static Snapshot disabled() {
return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of());
return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of(), ValidationState.DISABLED, "");
}
}
}
@@ -143,7 +143,7 @@ public final class ImageMapStudioGUI {
private ImageMapStudioGUI(Engine engine) {
this.engine = Objects.requireNonNull(engine, "Image Map Studio engine");
this.packFolder = engine.getData().getDataFolder();
this.packFolder = engine.getPackSource().toFile();
this.dimensionKey = engine.getDimension().getLoadKey();
this.boundary = engine.getDimension().getWorldBoundary() == null
? null
@@ -163,7 +163,6 @@ public final class ImageMapStudioGUI {
}
static void reloadActiveEngine(Engine engine) {
engine.getData().getImageMapLoader().clearCache();
engine.hotloadSilently();
}
@@ -431,9 +430,14 @@ public final class ImageMapStudioGUI {
}
private void refreshPresets() {
String[] keys = engine.getData().getImageMapLoader().getPossibleKeys();
Arrays.sort(keys);
presetPicker.setModel(new DefaultComboBoxModel<>(keys));
IrisData data = IrisData.openRuntime(packFolder);
try {
String[] keys = data.getImageMapLoader().getPossibleKeys();
Arrays.sort(keys);
presetPicker.setModel(new DefaultComboBoxModel<>(keys));
} finally {
data.close();
}
}
private void loadPreset(String key) {
@@ -32,9 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend {
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
INMS.get().ensureServerLevelInjection();
try {
INMS.get().ensureServerLevelInjection();
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
} catch (Throwable e) {
@@ -189,6 +189,26 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return Optional.ofNullable(dataLoaders.get(dataFolder));
}
public static void invalidateLoadedAuthoringResources(File source) {
Path requested = dataFolderIdentity(Objects.requireNonNull(source, "authoring pack source"));
for (Map.Entry<File, IrisData> entry : dataLoaders.entrySet()) {
if (dataFolderIdentity(entry.getKey()).equals(requested)) {
entry.getValue().invalidateAuthoringResources();
}
}
}
private synchronized void invalidateAuthoringResources() {
if (generationRegistryContract != null) {
throw new IllegalStateException("Cannot invalidate an immutable generation pack as an authoring source.");
}
dump();
clearLists();
generationPackFingerprint = null;
contentGate = null;
dimensionCustomBiomeResourceKeys.clear();
}
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
@@ -18,6 +18,8 @@
package art.arcane.iris.core.nms;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.util.concurrent.CompletableFuture;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
@@ -57,9 +59,19 @@ import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public interface INMSBinding {
default CompletableFuture<Void> flushSavedTerrainCapture(World world) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"Native saved terrain checkpoints are unavailable."));
}
default CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(
World world, int chunkX, int chunkZ, int minimumY, int height) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"Native saved terrain capture is unavailable."));
}
default PlatformGenerationRegistry generationRegistry() {
throw new UnsupportedOperationException(
"The active NMS binding does not expose generation registry definitions."
@@ -110,6 +110,11 @@ final class PackStructurePlacementValidator {
continue;
}
if (hasNativeStructures) {
Object anchor = placement.opt("anchor");
if (anchor != null && anchor != JSONObject.NULL && !"LEGACY".equals(anchor)) {
blockingErrors.add(placementPath
+ ".anchor must be omitted, null, or LEGACY for nativeStructures.");
}
validateNativeStructures(
placementPath, nativeStructures,
registries.structures(), registries.jigsaws(),
@@ -211,7 +211,6 @@ public class IrisPregenerator {
historyLastCached.set(cached.get());
historyLastMillis.set(startedAt);
ticker.start();
checkRegions();
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ());
@@ -280,10 +279,6 @@ public class IrisPregenerator {
+ "] chunks/s";
}
private void checkRegions() {
task.iterateRegions(this::checkRegion);
}
private void init() {
generator.init();
}
@@ -413,9 +408,6 @@ public class IrisPregenerator {
}
generatedRegions.add(pos);
// No checkRegions() here: re-spiraling every region after every completed region
// was O(regions^2) dead work on the submitter thread (result discarded, and the
// only side effect was CachedPregenMethod faulting plates against eviction).
}
}
@@ -437,14 +429,6 @@ public class IrisPregenerator {
MantleHeapPressure.requestPanicReclaim();
}
private void checkRegion(int x, int z) {
if (generatedRegions.contains(new Position2(x, z))) {
return;
}
generator.supportsRegions(x, z, listener);
}
public long getFailedChunks() {
return failed.get();
}
@@ -21,22 +21,15 @@ package art.arcane.iris.core.pregenerator;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Objects;
public final class PregenPerformanceProfile {
private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L;
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
private PregenPerformanceProfile() {
}
@@ -44,7 +37,6 @@ public final class PregenPerformanceProfile {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
@@ -52,73 +44,41 @@ public final class PregenPerformanceProfile {
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
changed = true;
}
if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void apply(Engine engine) {
apply();
if (!prepareNoiseCacheInPlace(engine)) {
engine.hotloadComplex();
logApplied();
if (engine != null) {
prepareNoiseCacheInPlace(engine);
IrisLogging.info("Pregen profile applied: noiseCacheSize="
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
}
}
public static void applyToGenerator(PlatformChunkGenerator generator) {
apply();
Engine engine = generator == null ? null : generator.getEngine();
if (!prepareNoiseCacheInPlace(engine)) {
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
logApplied();
}
apply(generator == null ? null : generator.getEngine());
}
static int pregenBurstParallelism(int availableProcessors) {
return Math.max(4, availableProcessors * 2);
}
private static boolean prepareNoiseCacheInPlace(Engine engine) {
if (engine == null) {
return true;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return false;
}
ProceduralStream<IrisBiome> caveBiomeStream = complex.getCaveBiomeStream();
if (!(caveBiomeStream instanceof CachedStream2D<?> cachedStream) || !cachedStream.usesFastCache()) {
return false;
}
private static void prepareNoiseCacheInPlace(Engine engine) {
IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex");
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
if (!resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks)) {
return false;
}
return resizeNoiseCache(complex.getHeightStream(), configuredChunks);
resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks);
resizeNoiseCache(complex.getRawHeightStream(), configuredChunks);
}
private static boolean resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
if (stream instanceof CachedDoubleStream2D cachedStream) {
cachedStream.setMaximumChunks(configuredChunks);
return true;
private static void resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
if (!(stream instanceof CachedDoubleStream2D cachedStream)) {
throw new IllegalStateException("Pregeneration requires a mutable double terrain cache");
}
if (!(stream instanceof MeteredCache cache)) {
return false;
}
return cache.getMaxSize() == (long) configuredChunks * 256L;
}
private static void logApplied() {
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
cachedStream.setMaximumChunks(configuredChunks);
}
}
@@ -0,0 +1,72 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.LinkedHashMap;
import java.util.Objects;
public final class PregenSavedChunkStatus {
private static final int MAXIMUM_REGIONS = 256;
private final StatusReader reader;
private final LinkedHashMap<Long, RegionStatus> regions = new LinkedHashMap<>(16, 0.75f, true);
public PregenSavedChunkStatus(StatusReader reader) {
this.reader = Objects.requireNonNull(reader);
}
public static PregenSavedChunkStatus fromWorld(Path dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot).toAbsolutePath().normalize();
return new PregenSavedChunkStatus((x, z) -> readFull(root, x, z));
}
public synchronized boolean isFull(int chunkX, int chunkZ) {
long key = ((long) (chunkX >> 5) << 32) | ((chunkZ >> 5) & 0xffffffffL);
RegionStatus status = regions.get(key);
if (status == null) {
status = new RegionStatus(new BitSet(1024), new BitSet(1024));
regions.put(key, status);
if (regions.size() > MAXIMUM_REGIONS) {
regions.remove(regions.keySet().iterator().next());
}
}
int index = ((chunkZ & 31) << 5) | (chunkX & 31);
if (!status.checked().get(index)) {
try {
status.full().set(index, reader.isFull(chunkX, chunkZ));
status.checked().set(index);
} catch (IOException failure) {
throw new UncheckedIOException("Cannot verify saved pregeneration chunk " + chunkX + "," + chunkZ, failure);
}
}
return status.full().get(index);
}
private static boolean readFull(Path root, int chunkX, int chunkZ) throws IOException {
Path region = root.resolve("region/r." + (chunkX >> 5) + "." + (chunkZ >> 5) + ".mca");
if (Files.notExists(region)) {
return false;
}
try (RandomAccessFile input = new RandomAccessFile(region.toFile(), "r")) {
input.seek((long) (((chunkZ & 31) << 5) | (chunkX & 31)) * Integer.BYTES);
if (input.readInt() == 0) {
return false;
}
}
return SavedTerrainChunk.isComplete(SavedTerrainChunk.readStatus(root, chunkX, chunkZ));
}
@FunctionalInterface
public interface StatusReader {
boolean isFull(int chunkX, int chunkZ) throws IOException;
}
private record RegionStatus(BitSet checked, BitSet full) {
}
}
@@ -4,19 +4,22 @@ import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
public class CachedPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod method;
private final PregenCache cache;
private final PregenTask task;
private final PregenSavedChunkStatus savedStatus;
private volatile PregenListener wrappedSource;
private volatile PregenListener wrappedListener;
public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) {
this.method = method;
this.cache = cache.sync();
this.task = task;
public CachedPregenMethod(Configuration configuration) {
this.method = configuration.method();
this.cache = configuration.cache().sync();
this.task = configuration.task();
this.savedStatus = configuration.savedStatus();
}
@Override
@@ -26,7 +29,6 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void close() {
cache.write();
method.close();
cache.write();
}
@@ -39,12 +41,12 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener);
return isSavedRegionCached(x, z) || method.supportsRegions(x, z, listener);
}
@Override
public String getMethod(int x, int z) {
if (cache.isRegionCached(x, z)) {
if (isSavedRegionCached(x, z)) {
return "Cached";
}
return method.getMethod(x, z);
@@ -52,7 +54,7 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void generateRegion(int x, int z, PregenListener listener) {
if (cache.isRegionCached(x, z)) {
if (isSavedRegionCached(x, z)) {
listener.onRegionGenerated(x, z);
task.iterateChunks(x, z, (cX, cZ) -> {
listener.onChunkGenerated(cX, cZ, true);
@@ -60,13 +62,16 @@ public class CachedPregenMethod implements PregeneratorMethod {
});
return;
}
method.generateRegion(x, z, listener);
cache.cacheRegion(x, z);
if (!method.supportsRegions(x, z, listener)) {
task.iterateChunks(x, z, (chunkX, chunkZ) -> generateChunk(chunkX, chunkZ, listener));
return;
}
method.generateRegion(x, z, cachingListener(listener));
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
if (cache.isChunkCached(x, z)) {
if (cache.isChunkCached(x, z) && savedStatus.isFull(x, z)) {
listener.onChunkGenerated(x, z, true);
listener.onChunkCleaned(x, z);
return;
@@ -74,6 +79,20 @@ public class CachedPregenMethod implements PregeneratorMethod {
method.generateChunk(x, z, cachingListener(listener));
}
private boolean isSavedRegionCached(int x, int z) {
if (!cache.isRegionCached(x, z)) {
return false;
}
for (int localX = 0; localX < 32; localX++) {
for (int localZ = 0; localZ < 32; localZ++) {
if (!savedStatus.isFull((x << 5) + localX, (z << 5) + localZ)) {
return false;
}
}
}
return true;
}
private PregenListener cachingListener(PregenListener listener) {
PregenListener source = wrappedSource;
PregenListener wrapped = wrappedListener;
@@ -185,6 +204,10 @@ public class CachedPregenMethod implements PregeneratorMethod {
method.onRegionSubmitted(regionX, regionZ);
}
public record Configuration(PregeneratorMethod method, PregenCache cache, PregenTask task,
PregenSavedChunkStatus savedStatus) {
}
@Override
public Mantle getMantle() {
return method.getMantle();
@@ -1,5 +1,6 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
@@ -24,6 +25,7 @@ public final class IrisSafeguard {
}
public static void execute() {
IrisStartupValidation.beginRuntimeValidation();
List<Task> tasks = Tasks.getTasks();
LinkedHashMap<Task, ValueWithDiagnostics<Mode>> resultValues = new LinkedHashMap<>(tasks.size());
LinkedHashMap<String, String> contextValues = new LinkedHashMap<>(tasks.size());
@@ -36,9 +38,13 @@ public final class IrisSafeguard {
try {
result = task.run();
} catch (Throwable e) {
boolean injectionFailure = "injection".equals(task.getId());
if (injectionFailure) {
IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server.");
}
IrisLogging.reportError(e);
result = new ValueWithDiagnostics<>(
Mode.WARNING,
injectionFailure ? Mode.UNSTABLE : Mode.WARNING,
new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e)
);
}
@@ -1,6 +1,7 @@
package art.arcane.iris.core.safeguard.task;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisWorldStorage;
@@ -119,18 +120,22 @@ public final class Tasks {
private static final Task INJECTION = Task.of("injection", () -> {
if (!Agent.install()) {
IrisStartupValidation.markRuntimeInvalid("Iris Java agent is unavailable. Add -javaagent:"
+ Agent.AGENT_JAR.getPath() + " to the JVM arguments before -jar and restart the server.");
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Java Agent"),
Diagnostic.Logger.ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."),
Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath()));
Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
}
if (!INMS.get().injectBukkit()) {
IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server.");
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Code Injection"),
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
}
IrisStartupValidation.markRuntimeReady();
return withDiagnostics(Mode.STABLE);
});
@@ -91,7 +91,7 @@ public class ObjectStudioSaveService implements IrisService {
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
File dir = resolveObjectsDir(e.getValue());
File dir = resolveObjectsDir(e.getValue(), engine);
if (dir != null) {
objectsDirs.put(e.getKey(), dir);
}
@@ -200,9 +200,13 @@ public class ObjectStudioSaveService implements IrisService {
return false;
}
private static File resolveObjectsDir(IrisData data) {
static File resolveObjectsDir(IrisData data, Engine engine) {
File root = data.getDataFolder();
if (root == null) return null;
if (root.toPath().toAbsolutePath().normalize().equals(
engine.getData().getDataFolder().toPath().toAbsolutePath().normalize())) {
root = engine.getPackSource().toFile();
}
File objects = new File(root, "objects");
if (!objects.exists()) {
objects.mkdirs();
@@ -21,8 +21,6 @@ package art.arcane.iris.core.structure;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisStructure;
import com.google.gson.GsonBuilder;
import java.io.File;
@@ -38,8 +36,6 @@ import java.util.concurrent.ConcurrentHashMap;
public final class StructureIndexService {
private static final String INDEX_PATH = ".iris/structure-index.json";
private static final String LEGACY_INDEX_PATH = "structures/structure-index.json";
private static final String LEGACY_INDEX_KEY = "structure-index";
private static final Set<String> GENERATED = ConcurrentHashMap.newKeySet();
private StructureIndexService() {
@@ -66,7 +62,6 @@ public final class StructureIndexService {
}
public static File write(IrisData data) {
removeLegacyIndex(data);
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
@@ -93,7 +88,6 @@ public final class StructureIndexService {
List<String> iris = new ArrayList<>();
if (data.getStructureLoader() != null) {
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
iris.removeIf(LEGACY_INDEX_KEY::equals);
}
Collections.sort(vanilla);
@@ -138,22 +132,4 @@ public final class StructureIndexService {
return new File(dataFolder, INDEX_PATH);
}
static File legacyIndexFile(File dataFolder) {
return new File(dataFolder, LEGACY_INDEX_PATH);
}
static void removeLegacyIndex(IrisData data) {
File legacyFile = legacyIndexFile(data.getDataFolder());
try {
Files.deleteIfExists(legacyFile.toPath());
ResourceLoader<IrisStructure> structureLoader = data.getStructureLoader();
if (structureLoader != null) {
structureLoader.getLoadCache().invalidate(LEGACY_INDEX_KEY);
structureLoader.clearList();
}
} catch (Throwable e) {
IrisLogging.reportError("Failed to remove legacy structure index '"
+ legacyFile.getAbsolutePath() + "'", e);
}
}
}
@@ -118,9 +118,9 @@ public class IrisCreator {
*/
private String name = "irisworld";
/**
* Studio mode makes the engine hotloadable and uses the dimension in
* your Iris/packs folder instead of copying the dimension files into
* the world itself. Studio worlds are deleted when they are unloaded.
* Studio mode snapshots edits from your Iris/packs folder into generation history.
* Existing chunks retain their generation while new chunks use accepted edits.
* Studio worlds are deleted when they are unloaded.
*/
private boolean studio = false;
/**
@@ -260,18 +260,15 @@ public class IrisCreator {
+ "Restart the server, then run the command again.");
}
IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
StudioSVC studioService = IrisServices.get(StudioSVC.class);
installedDimension = benchmark
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
if (installedDimension == null) {
throw new IrisException("Failed to install dimension pack for " + dimension());
}
dimension = installedDimension.getLoadKey();
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
StudioSVC studioService = IrisServices.get(StudioSVC.class);
IrisDimension installedDimension = benchmark && !studio
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
if (installedDimension == null) {
throw new IrisException("Failed to install dimension pack for " + dimension());
}
dimension = installedDimension.getLoadKey();
if (studio()) {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
@@ -285,6 +282,7 @@ public class IrisCreator {
long generatorPrepareStart = System.nanoTime();
WorldCreator wc = new IrisWorldCreator()
.dimension(installedDimension)
.studioPackSource(resolvedDimension.getLoader().getDataFolder())
.name(name)
.seed(seed)
.studio(studio)
@@ -33,6 +33,7 @@ import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.project.IrisProject;
@@ -316,7 +317,8 @@ public class IrisToolbelt {
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
PregeneratorMethod activeMethod = useCachedWrapper
? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task)
? new CachedPregenMethod(new CachedPregenMethod.Configuration(method, resolvePregenCache(engine.getWorld()), task,
PregenSavedChunkStatus.fromWorld(engine.getWorld().worldFolder().toPath())))
: method;
return new PregeneratorJob(new PregeneratorJob.Configuration(
task,
@@ -33,6 +33,7 @@ import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
public class IrisWorldCreator {
private String name;
@@ -41,6 +42,7 @@ public class IrisWorldCreator {
private IrisDimension dimension;
private long seed = 1337;
private boolean persistent;
private File studioPackSource;
public IrisWorldCreator() {
@@ -83,6 +85,11 @@ public class IrisWorldCreator {
return this;
}
public IrisWorldCreator studioPackSource(File source) {
this.studioPackSource = Objects.requireNonNull(source, "Studio authoring source");
return this;
}
public WorldCreator create() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
@@ -102,13 +109,13 @@ public class IrisWorldCreator {
.seed(seed)
.worldFolder(worldFolder)
.build();
GenerationHistory generationHistory = persistent
GenerationHistory generationHistory = persistent || studio
? requireGenerationHistory(w.worldFolder(), seed)
: null;
File packRoot = generationHistory != null
File packRoot = studio
? Objects.requireNonNull(studioPackSource, "Studio authoring source")
: generationHistory != null
? requireActivePack(generationHistory)
: studio
? dim.getLoader().getDataFolder()
: new File(w.worldFolder(), "iris/pack");
ChunkGenerator g = new BukkitChunkGenerator(
w,
@@ -0,0 +1,63 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import java.io.File;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
final class EngineDiagnostics {
private final IrisEngine engine;
EngineDiagnostics(IrisEngine engine) {
this.engine = engine;
}
/**
* One line per engine naming what this pack cannot generate on the running Minecraft version. The published
* validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack
* falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws:
* a report failure must not take an otherwise working world down with it.
*/
void logPackCompatSummary() {
try {
File folder = engine.getData().getDataFolder();
// Runtime data comes from an immutable snapshot; the dimension key identifies the pack.
String pack = engine.getDimension().getLoadKey();
String world = engine.getTarget().getWorld().name();
String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
PackValidationResult published = PackValidationRegistry.get(folder.toPath());
if (published == null) {
published = PackValidationRegistry.get(folder.getName());
}
PackCompatReport report = published != null && !published.getCompatFindings().isEmpty()
? PackCompatReport.of(published.getCompatFindings())
: engine.getData().getCompatReport();
if (!report.isEmpty()) {
IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version));
}
if (engine.getDimension().isCompatExcluded()) {
IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft "
+ (version == null || version.isBlank() ? "unknown" : version));
}
} catch (Throwable e) {
IrisLogging.debug("Pack compat summary failed: " + e.getMessage());
}
}
void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!engine.isStudio()) {
return;
}
IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s",
engine.getTarget().getWorld().name(),
engine.getInitializationMode().name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos),
Boolean.toString(skipped));
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
@@ -27,6 +28,10 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationManifest;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
@@ -34,12 +39,13 @@ import art.arcane.iris.util.project.context.IrisContext;
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
/**
* Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload
* and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and
* drain generation before swapping anything, and roll the previous runtime back on failure.
* Reloads mutable engine runtimes and publishes immutable Studio generation activations.
* Studio transitions drain sessions outside the lifecycle lock before freezing chunk ownership.
* Failures before publication preserve the previous runtime; durable publication failures stop generation.
*/
final class EngineHotloader {
private final IrisEngine engine;
private final Object studioTransitionLock = new Object();
EngineHotloader(IrisEngine engine) {
this.engine = engine;
@@ -51,6 +57,10 @@ final class EngineHotloader {
}
void hotloadComplex() {
if (engine.isStudio() && engine.getGenerationHistoryRuntimeRouter().isPresent()) {
hotloadSilently();
return;
}
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
@@ -114,6 +124,11 @@ final class EngineHotloader {
}
void hotloadSilently() {
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
if (engine.isStudio() && router != null) {
hotloadStudio(router);
return;
}
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
@@ -192,6 +207,95 @@ final class EngineHotloader {
}
}
private void hotloadStudio(GenerationHistoryRuntimeRouter router) {
synchronized (studioTransitionLock) {
EngineRuntime previous = null;
GenerationManifest previousManifest = router.history().manifest();
GenerationHistoryRuntimeRouter.StudioCutover cutover = null;
StudioGenerationUpdate update = null;
boolean backgroundAdmissionClosed = false;
try {
engine.requireRunning("hotload the Studio pack");
GenerationHistory history = router.history();
update = StudioGenerationUpdate.prepare(engine, history);
if (update == null) {
return;
}
engine.awaitNativeStructureBootstrap("Studio generation cutover");
engine.backgroundTasks.closeBackgroundTaskAdmission();
backgroundAdmissionClosed = true;
engine.backgroundTasks.drainBackgroundTasks("Studio generation cutover")
.requireComplete("Studio generation cutover");
cutover = router.beginStudioCutover(IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS);
synchronized (engine.lifecycleLock) {
engine.requireRunning("prepare the Studio pack cutover");
previous = engine.runtime;
engine.studioGenerationTransition = true;
engine.lifecycleState = LifecycleState.HOTLOADING;
}
engine.sealForTransition("Studio generation cutover", false);
IrisBoundarySignatureSampler.checkpoint(engine);
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState != LifecycleState.HOTLOADING || engine.runtime != previous) {
throw new IllegalStateException("Studio engine changed while its generation cutover was draining.");
}
engine.prepareRuntimeHotload();
history.stageUpdate(update.source(), update.fingerprint(), update.dimensionContract(),
update.registryContract(), IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
cutover.promotePending();
IrisStructureLocator.invalidate(engine);
StructureReachability.invalidate(engine);
engine.runtimeBuilder.refreshStudioRuntimeServices();
engine.getPrefetchSaveStarted().set(false);
engine.getEngineData().getStatistics().hotloaded();
engine.getPlatformHooks().applyWorldBoundary(engine);
}
broadcastStudioHotload(false, "");
} catch (Throwable failure) {
if (previous != null) {
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState == LifecycleState.HOTLOADING
|| engine.lifecycleState == LifecycleState.RUNNING) {
boolean unchanged = false;
try {
unchanged = router.history().manifest() == previousManifest;
} catch (Throwable historyFailure) {
failure.addSuppressed(historyFailure);
}
if (unchanged) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
} else {
engine.lifecycleState = LifecycleState.FAILED;
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
}
}
}
} else if (backgroundAdmissionClosed && engine.lifecycleState == LifecycleState.RUNNING
&& !engine.getClosing().get()) {
engine.backgroundTasks.openBackgroundTaskAdmission();
}
IrisLogging.reportError("Studio generation update failed for '" + engine.getWorld().name() + "'.", failure);
broadcastStudioHotload(true, failure.getClass().getSimpleName() + ": " + failure.getMessage());
throw EngineShutdownSequence.propagate(failure);
} finally {
synchronized (engine.lifecycleLock) {
engine.studioGenerationTransition = false;
}
if (cutover != null) {
cutover.close();
}
if (update != null) {
try {
update.close();
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove the staged Studio pack after its update.", cleanupFailure);
}
}
}
}
}
private void requireMutableGenerationRuntime(String operation) {
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
throw new IllegalStateException("Cannot " + operation
@@ -210,7 +314,7 @@ final class EngineHotloader {
if (protocolServer == null) {
return;
}
String packKey = engine.getData().getDataFolder().getName();
String packKey = engine.getPackSource().getFileName().toString();
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
} catch (Throwable broadcastFailure) {
IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName()
@@ -248,6 +248,39 @@ final class EngineRuntimeBuilder {
IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
}
void refreshStudioRuntimeServices() {
EngineRuntime previous = requireRuntime("refresh Studio runtime services");
EngineEffects effects = null;
EngineWorldManager worldManager = null;
try {
effects = Objects.requireNonNull(IrisServices.get(EngineEffectsProvider.class).create(engine),
"Studio engine effects");
worldManager = Objects.requireNonNull(IrisServices.get(EngineWorldManagerProvider.class).create(engine),
"Studio world manager");
} catch (Throwable failure) {
if (effects != null) {
EngineEffects createdEffects = effects;
EngineShutdownSequence.runCleanup(failure, createdEffects::close);
}
throw propagate(failure);
}
EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager);
Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close);
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close);
if (retirementFailure != null) {
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, worldManager::close);
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, effects::close);
throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure);
}
engine.runtime = next;
worldManager.start();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
scheduleRuntimeTasks(next);
}
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
schedulePackHash(engineRuntime.generation());
try {
@@ -18,6 +18,8 @@
package art.arcane.iris.engine;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
@@ -25,6 +27,7 @@ import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
@@ -79,6 +82,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -140,6 +144,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
private ProceduralStream<Double> unblendedNaturalHeightStream;
private final ResolvedTerrainProvider resolvedTerrain;
private ProceduralStream<Double> placementHeightStream;
private ProceduralStream<Double> heightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
@@ -188,6 +194,7 @@ public class IrisComplex implements DataProvider {
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
this.transitionGenerationPlan = transitionGenerationPlan;
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
@@ -314,11 +321,7 @@ public class IrisComplex implements DataProvider {
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
Interpolated.DOUBLE
).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize);
naturalHeightStream = transitionGenerationPlan == null ? unblendedNaturalHeightStream
: unblendedNaturalHeightStream.convertAware2D((newHeight, x, z) -> blendNaturalTerrainHeight(
newHeight,
transitionGenerationPlan.terrainSampleAt(blockCoordinate(x), blockCoordinate(z))))
.cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalHeightStream = unblendedNaturalHeightStream;
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
@@ -355,10 +358,11 @@ public class IrisComplex implements DataProvider {
}
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
.cache2DDouble("heightStream", engine, cacheSize);
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
roundedHeighteightStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
slopeStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3);
naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
@@ -480,14 +484,7 @@ public class IrisComplex implements DataProvider {
}
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
double newHeight = sampleUnblendedNaturalTerrainHeight(engine, x, z);
if (transitionGenerationPlan == null) {
return newHeight;
}
TransitionGenerationPlan.TerrainSample sample = transitionGenerationPlan.terrainSampleAt(
blockCoordinate(x),
blockCoordinate(z));
return blendNaturalTerrainHeight(newHeight, sample);
return sampleUnblendedNaturalTerrainHeight(engine, x, z);
}
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
@@ -495,20 +492,6 @@ public class IrisComplex implements DataProvider {
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
}
static double blendNaturalTerrainHeight(
double newHeight,
TransitionGenerationPlan.TerrainSample sample
) {
Objects.requireNonNull(sample, "terrain transition sample");
if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) {
return newHeight;
}
double historicalHeight = sample.nearestSignature().fluidHeight().isPresent()
? sample.historicalOceanFloorHeight()
: sample.historicalSurfaceHeight();
return GenerationBlend.interpolate(historicalHeight, newHeight, sample.newEpochWeight());
}
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
double deltaX = easternHeight - naturalHeight;
double deltaZ = southernHeight - naturalHeight;
@@ -808,6 +791,9 @@ public class IrisComplex implements DataProvider {
int naturalHeight,
double hydrologyWeight
) {
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
return layer;
}
return new HydrologyColumnLayer(
layer.feature(),
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
@@ -847,6 +833,42 @@ public class IrisComplex implements DataProvider {
|| hydrologyRuntime.isPlanned(x, z);
}
public ProceduralStream<Double> getRawHeightStream() {
return heightStream;
}
public ProceduralStream<Double> getHeightStream() {
return placementHeightStream;
}
public Optional<TerrainBoundarySignature> resolvedTerrainColumn(int blockX, int blockZ) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return Optional.empty();
}
return Optional.of(resolvedTerrain.column(blockX, blockZ));
}
public OptionalInt resolvedTerrainHeight(int blockX, int blockZ, boolean ignoreFluid) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return OptionalInt.empty();
}
return OptionalInt.of(resolvedTerrain.height(blockX, blockZ, ignoreFluid));
}
boolean isNaturalTerrainContext() {
IrisContext context = IrisContext.get();
return context != null && context.getChunkContext() != null
&& context.getChunkContext().getComplex() == this
&& context.getChunkContext().isNaturalTerrain();
}
private double samplePlacementHeight(double x, double z) {
OptionalInt resolved = resolvedTerrainHeight(blockCoordinate(x), blockCoordinate(z), true);
return resolved.isPresent() ? resolved.getAsInt() : heightStream.getDouble(x, z);
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
return transitionGenerationPlan != null
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
@@ -1783,6 +1805,7 @@ public class IrisComplex implements DataProvider {
}
public void close() {
resolvedTerrain.clear();
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
@@ -37,6 +37,7 @@ import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.history.GenerationBoundarySignatureSampler;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.ChunkGenerationSemantics;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.framework.SeedManager;
@@ -46,10 +47,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisEngineData;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.spi.IrisLogging;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisPlatforms;
@@ -78,20 +76,17 @@ import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -118,6 +113,11 @@ public class IrisEngine implements Engine {
private final EngineMetrics metrics;
private final CompletableFuture<Void> generationCacheWarm;
private final boolean studio;
@Setter(AccessLevel.NONE)
private volatile Path studioGenerationSource;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
boolean studioGenerationTransition;
private final AtomicRollingSequence wallClock;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -163,6 +163,9 @@ public class IrisEngine implements Engine {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final EngineMetricsReport metricsReport = new EngineMetricsReport(this);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final EngineDiagnostics diagnostics = new EngineDiagnostics(this);
private final AtomicBoolean cleaning;
private final ChronoLatch cleanLatch;
private final SeedManager seedManager;
@@ -267,7 +270,7 @@ public class IrisEngine implements Engine {
this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version");
this.initialTransitionPlan = transitionPlan;
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
this.generationSessions = new GenerationSessionManager();
this.generationSessions = new GenerationSessionManager(requiredMode.studio());
this.closing = new AtomicBoolean(true);
this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio());
this.lifecycleState = LifecycleState.INITIALIZING;
@@ -313,7 +316,7 @@ public class IrisEngine implements Engine {
String studioCacheIdentity = currentStudioCacheIdentity();
getData().loadPrefetch(this);
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
try {
StructureIndexService.writeOnce(getData());
} catch (Throwable e) {
@@ -326,8 +329,8 @@ public class IrisEngine implements Engine {
enableStableStudioCache(initialRuntime, studioCacheIdentity);
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logPackCompatSummary();
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
diagnostics.logPackCompatSummary();
diagnostics.logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
phaseStartedAt = System.nanoTime();
if (requiredMode.warmGenerationCaches()) {
if (requiredMode.studio()) {
@@ -337,7 +340,7 @@ public class IrisEngine implements Engine {
}
} else {
generationCacheWarm.complete(null);
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
}
EngineTickRegistry.registerTicking(this);
} catch (Throwable e) {
@@ -347,38 +350,6 @@ public class IrisEngine implements Engine {
IrisLogging.debug("Engine Initialized " + getCacheID());
}
/**
* One line per engine naming what this pack cannot generate on the running Minecraft version. The published
* validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack
* falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws:
* a report failure must not take an otherwise working world down with it.
*/
private void logPackCompatSummary() {
try {
File folder = getData().getDataFolder();
// A world engine reads its snapshot copy under <world>/iris/pack, so the dimension key is the pack's name.
String pack = getDimension().getLoadKey();
String world = target.getWorld().name();
String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
PackValidationResult published = PackValidationRegistry.get(folder.toPath());
if (published == null) {
published = PackValidationRegistry.get(folder.getName());
}
PackCompatReport report = published != null && !published.getCompatFindings().isEmpty()
? PackCompatReport.of(published.getCompatFindings())
: getData().getCompatReport();
if (!report.isEmpty()) {
IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version));
}
if (getDimension().isCompatExcluded()) {
IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft "
+ (version == null || version.isBlank() ? "unknown" : version));
}
} catch (Throwable e) {
IrisLogging.debug("Pack compat summary failed: " + e.getMessage());
}
}
public void awaitGenerationCacheWarm() {
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
return;
@@ -394,18 +365,6 @@ public class IrisEngine implements Engine {
return !generationCacheWarm.isDone();
}
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!studio) {
return;
}
IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s",
target.getWorld().name(),
initializationMode.name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos),
Boolean.toString(skipped));
}
private String currentStudioCacheIdentity() {
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
return "";
@@ -469,7 +428,7 @@ public class IrisEngine implements Engine {
throw new IllegalStateException("Generation cache warming failed.", failure);
} finally {
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
}
}
@@ -575,6 +534,9 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(int x, int z) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
return Engine.super.getCaveBiome(x, z);
@@ -595,6 +557,9 @@ public class IrisEngine implements Engine {
int z,
IrisDimensionCarvingResolver.State state
) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, y, z, state);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
return Engine.super.getCaveBiome(x, y, z, state);
@@ -604,6 +569,9 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getSurfaceBiome(int x, int z) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getSurfaceBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
return Engine.super.getSurfaceBiome(x, z);
@@ -637,6 +605,10 @@ public class IrisEngine implements Engine {
@ChunkCoordinates
@Override
public Set<String> getObjectsAt(int x, int z) {
Optional<ChunkGenerationSemantics> recorded = recordedChunkSemantics(x, z);
if (recorded.isPresent()) {
return recorded.get().objectKeys();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
return getMantle().getObjectComponent().guess(x, z);
@@ -646,6 +618,15 @@ public class IrisEngine implements Engine {
@ChunkCoordinates
@Override
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkZ) {
Optional<ChunkGenerationSemantics> recorded = recordedChunkSemantics(chunkX, chunkZ);
if (recorded.isPresent()) {
Set<Pair<String, BlockPos>> points = new HashSet<>();
for (ChunkGenerationSemantics.PointOfInterest point : recorded.get().pointsOfInterest()) {
ChunkGenerationSemantics.BlockPosition position = point.position();
points.add(new Pair<>(point.key(), new BlockPos(position.x(), position.y(), position.z())));
}
return points;
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
@@ -654,11 +635,19 @@ public class IrisEngine implements Engine {
chunkX,
chunkZ,
MatterStructurePOI.class,
(x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(x, y, z))));
(x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(
Math.addExact(Math.multiplyExact(chunkX, 16), x), y,
Math.addExact(Math.multiplyExact(chunkZ, 16), z)))));
return pois;
}
}
private Optional<ChunkGenerationSemantics> recordedChunkSemantics(int chunkX, int chunkZ) {
GenerationHistoryRuntimeRouter router = getGenerationHistoryRuntimeRouter().orElse(null);
return router == null ? Optional.empty()
: router.history().semantics(chunkX, chunkZ).filter(ChunkGenerationSemantics::sealed);
}
private void warmupChunk(int x, int z) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
@@ -675,6 +664,30 @@ public class IrisEngine implements Engine {
hotloader.hotload();
}
public void configureStudioGenerationSource(Path source) {
if (!studio) {
throw new IllegalStateException("An authoring pack source requires a Studio engine.");
}
Path required = Objects.requireNonNull(source, "Studio generation source").toAbsolutePath().normalize();
synchronized (lifecycleLock) {
requireRunning("configure the Studio authoring pack");
if (studioGenerationSource != null && !studioGenerationSource.equals(required)) {
throw new IllegalStateException("The Studio authoring pack is already configured.");
}
studioGenerationSource = required;
}
}
public Path getStudioGenerationSource() {
return Objects.requireNonNull(studioGenerationSource, "Studio authoring pack is not configured");
}
@Override
public Path getPackSource() {
Path source = studioGenerationSource;
return source == null ? getData().getDataFolder().toPath() : source;
}
public void hotloadComplex() {
hotloader.hotloadComplex();
}
@@ -827,6 +840,10 @@ public class IrisEngine implements Engine {
return generationRuntimeScopes.current() != null;
}
private boolean usesScopedNaturalTerrain() {
return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext();
}
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
int blockX,
int blockZ,
@@ -956,7 +973,7 @@ public class IrisEngine implements Engine {
EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target");
Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
synchronized (lifecycleLock) {
requireRunning("build a detached generation runtime");
requireGenerationRuntimeMutation("build a detached generation runtime");
GenerationRuntime active = runtime.generation();
if (requiredTarget.getWorld() != active.target().getWorld()) {
throw new IllegalArgumentException("Detached generation runtime must target this Iris world.");
@@ -1066,7 +1083,7 @@ public class IrisEngine implements Engine {
public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) {
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
synchronized (lifecycleLock) {
requireRunning("select the default generation runtime");
requireGenerationRuntimeMutation("select the default generation runtime");
if (required.engine != this) {
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
}
@@ -1439,6 +1456,14 @@ public class IrisEngine implements Engine {
}
}
private void requireGenerationRuntimeMutation(String operation) {
if (!closed && studio && studioGenerationTransition && lifecycleState == LifecycleState.HOTLOADING
&& generationHistoryRuntimeRouter != null) {
return;
}
requireRunning(operation);
}
private boolean EngineSafe() {
// Todo: this has potential if done right
int EngineMCVersion = getEngineData().getStatistics().getMCVersion();
@@ -0,0 +1,240 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TransitionFluidContainment;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ResolvedTerrainProvider {
private static final int MAXIMUM_CACHED_CHUNKS = 64;
private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096;
private final Engine engine;
private final LinkedHashMap<Long, SavedTerrainChunk> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, SavedTerrainChunk> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, int[]> heights = new LinkedHashMap<>(64, 0.75F, true);
public ResolvedTerrainProvider(Engine engine) {
this.engine = engine;
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
return resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
}
public int height(int blockX, int blockZ, boolean ignoreFluid) {
long key = chunkKey(blockX >> 4, blockZ >> 4);
synchronized (heights) {
int[] cached = heights.get(key);
if (cached != null) {
return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)];
}
}
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight();
}
public void generate(EngineMode mode, int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, boolean multicore, ChunkContext context) {
SavedTerrainChunk terrain;
if (context.getComplex().getTransitionGenerationPlan() != null
&& context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) {
terrain = resolve(x >> 4, z >> 4);
copy(terrain, blocks, biomes, context);
} else {
mode.generateTerrain(x, z, blocks, biomes, multicore, context);
terrain = capture(x, z, blocks, biomes, context, true);
}
if (engine instanceof IrisEngine irisEngine) {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
router.recordNaturalTerrain(terrain.boundaryOnly());
}
}
}
public void clear() {
synchronized (completed) {
completed.clear();
}
synchronized (rawCompleted) {
rawCompleted.clear();
}
synchronized (heights) {
heights.clear();
}
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ) {
return resolve(chunkX, chunkZ, false);
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, SavedTerrainChunk> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> loads = raw ? rawPending : pending;
long key = chunkKey(chunkX, chunkZ);
synchronized (cache) {
SavedTerrainChunk cached = cache.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<SavedTerrainChunk> result = new CompletableFuture<>();
CompletableFuture<SavedTerrainChunk> existing = loads.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (cache) {
SavedTerrainChunk published = cache.get(key);
if (published != null) {
result.complete(published);
return published;
}
}
SavedTerrainChunk terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
if (!raw) {
cacheHeights(key, terrain);
}
synchronized (cache) {
cache.put(key, terrain);
while (cache.size() > MAXIMUM_CACHED_CHUNKS) {
cache.pollFirstEntry();
}
}
result.complete(terrain);
return terrain;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
loads.remove(key, result);
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private void cacheHeights(long key, SavedTerrainChunk terrain) {
int[] values = new int[512];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x,
(terrain.chunkZ() << 4) + z);
int index = x * 16 + z;
values[index] = column.surfaceHeight();
values[256 + index] = column.oceanFloorHeight();
}
}
synchronized (heights) {
heights.put(key, values);
while (heights.size() > MAXIMUM_HEIGHT_CHUNKS) {
heights.pollFirstEntry();
}
}
}
private SavedTerrainChunk contain(int chunkX, int chunkZ) {
SavedTerrainChunk raw = resolve(chunkX, chunkZ, true);
TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return raw;
}
try {
return TransitionFluidContainment.contain(raw, plan, (x, z) -> rawColumn(plan, raw, x, z));
} catch (IOException failure) {
throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure);
}
}
private BoundaryColumnGeometry rawColumn(TransitionGenerationPlan plan, SavedTerrainChunk current, int x, int z)
throws IOException {
if (plan.isHistoricalBlock(x, z)) {
BoundaryColumnGeometry geometry = plan.terrainSignatures().signatureAt(x, z)
.orElseThrow(() -> new IOException("Missing saved terrain boundary column at " + x + "," + z))
.geometry();
if (geometry.minimumY() != engine.getMinHeight() || geometry.height() != engine.getHeight()) {
throw new IOException("Saved terrain boundary column has the wrong vertical layout at " + x + "," + z);
}
return geometry;
}
return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ()
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).column(x, z).geometry();
}
private SavedTerrainChunk compute(int chunkX, int chunkZ) {
int x = Math.multiplyExact(chunkX, 16);
int z = Math.multiplyExact(chunkZ, 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine
? irisEngine.openGenerationHistoryCoordinateScope(x, z) : null) {
IrisContext active = IrisContext.get();
long sessionId = active == null ? 0L : active.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, engine.getComplex(), sessionId, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
context.beginSpeculativeTerrain();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
return capture(x, z, blocks, biomes, context, false);
}
} catch (IOException failure) {
throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure);
}
}
private SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context, boolean boundaryOnly) {
try {
return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private static void copy(SavedTerrainChunk terrain, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, PlatformBlockState> states = new HashMap<>();
Map<String, PlatformBiome> physicalBiomes = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, (terrain.chunkZ() << 4) + z);
BoundaryColumnGeometry geometry = column.geometry();
for (int offset = 0; offset < blocks.getHeight(); offset++) {
String stateKey = geometry.voxelAt(geometry.minimumY() + offset).stateKey();
PlatformBlockState state = states.computeIfAbsent(stateKey, registries::blockOrNull);
if (state == null) {
throw new IllegalStateException("Resolved terrain requires unavailable block state " + stateKey);
}
blocks.setRaw(x, offset, z, state);
String biomeKey = column.biomeAtSample(offset / 4);
biomes.setRaw(x, offset, z, physicalBiomes.computeIfAbsent(biomeKey, registries::biome));
}
context.setTerrainHeight(x, z, column.oceanFloorHeight());
}
}
}
}
@@ -0,0 +1,88 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationPackRepository;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.util.List;
record StudioGenerationUpdate(
Path stagingRoot,
Path source,
String fingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) implements AutoCloseable {
static StudioGenerationUpdate prepare(IrisEngine engine, GenerationHistory history) throws IOException {
Path source = engine.getStudioGenerationSource();
IrisData.invalidateLoadedAuthoringResources(source.toFile());
String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
GenerationEpoch active = history.activeEpoch();
if (active.packFingerprintVersion() == GenerationPackFingerprint.CURRENT_VERSION
&& fingerprint.equals(active.packFingerprint())) {
return null;
}
Path stagingRoot = Files.createTempDirectory(history.paths().generationRoot(), ".studio-");
Path snapshot = stagingRoot.resolve(source.getFileName());
try {
GenerationPackRepository.copyPackTree(source, snapshot);
if (!fingerprint.equals(GenerationPackFingerprint.compute(snapshot, GenerationPackFingerprint.CURRENT_VERSION))) {
throw new IOException("Studio pack changed while its update snapshot was being captured.");
}
return prepareSnapshot(engine, history, stagingRoot, snapshot, fingerprint);
} catch (Throwable failure) {
try {
AtomicDirectoryPublisher.deleteTree(stagingRoot);
} catch (IOException cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
throw failure;
}
}
private static StudioGenerationUpdate prepareSnapshot(IrisEngine engine, GenerationHistory history,
Path stagingRoot, Path source, String fingerprint)
throws IOException {
GenerationEpoch active = history.activeEpoch();
PackValidationResult validation = PackValidator.validate(source.toFile());
if (!validation.isLoadable()) {
throw new IOException("Studio pack update is invalid: " + String.join("; ", validation.getBlockingErrors()));
}
IrisData data = IrisData.openRuntime(source.toFile());
try {
IrisDimension replacement = data.getDimensionLoader().load(active.dimensionContract().dimensionKey());
if (replacement == null) {
throw new IOException("Studio pack update is missing dimension '"
+ active.dimensionContract().dimensionKey() + "'.");
}
engine.getPlatformHooks().validateDimensionHotload(engine, replacement);
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
replacement, replacement.getLoadKey(), active.dimensionContract().dimensionTypeKey());
if (!active.dimensionContract().hasSameLayout(contract)) {
throw new IOException("Studio pack update changes the world's immutable dimension layout.");
}
GenerationRegistryContract registry = GenerationRegistryContractFactory.create(data, replacement, fingerprint);
registry.requireDefinitionsAvailableIn(GenerationRegistryContractFactory.captureRequiredDefinitions(List.of(registry)));
return new StudioGenerationUpdate(stagingRoot, source, fingerprint, contract, registry);
} finally {
data.close();
}
}
@Override
public void close() throws IOException {
AtomicDirectoryPublisher.deleteTree(stagingRoot);
}
}
@@ -20,8 +20,6 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
@@ -33,16 +31,12 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
public class UpperDimensionContext implements DataProvider {
private final DimensionTerrainContext terrainContext;
private final ProceduralStream<Double> lowerHeightStream;
private final ProceduralStream<Double> unblendedLowerHeightStream;
private final TransitionGenerationPlan transitionPlan;
private final CeilingLayout ceilingLayout;
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
this.terrainContext = terrainContext;
IrisComplex complex = engine.getComplex();
lowerHeightStream = complex.getHeightStream();
unblendedLowerHeightStream = complex.getUnblendedNaturalHeightStream();
transitionPlan = complex.getTransitionGenerationPlan();
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
}
@@ -103,18 +97,6 @@ public class UpperDimensionContext implements DataProvider {
0D,
Math.min(ceilingLayout.height() - 1D, terrainContext.getNormalTerrainHeight(x, z))
);
if (transitionPlan != null) {
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(x, z);
if (sample.newEpochWeight() < 1D && sample.hasHistoricalSignature()) {
int targetLowerSurfaceY = (int) Math.min(
ceilingLayout.height(),
Math.round(unblendedLowerHeightStream.getDouble(x, z))
);
int targetSurfaceY = effectiveSurfaceY(depth, ceilingLayout, targetLowerSurfaceY);
double targetDepth = Math.max(0D, ceilingLayout.height() - 1D - targetSurfaceY);
depth = blendCeilingDepth(targetDepth, sample);
}
}
int lowerSurfaceY = (int) Math.min(
ceilingLayout.height(),
Math.round(lowerHeightStream.getDouble(x, z))
@@ -122,14 +104,6 @@ public class UpperDimensionContext implements DataProvider {
return effectiveSurfaceY(depth, ceilingLayout, lowerSurfaceY);
}
static double blendCeilingDepth(double newDepth, TransitionGenerationPlan.TerrainSample sample) {
if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) {
return newDepth;
}
return GenerationBlend.interpolate(
sample.historicalUpperCeilingDepth(), newDepth, sample.newEpochWeight());
}
static int effectiveSurfaceY(double depth, CeilingLayout layout, int lowerSurfaceY) {
long mirroredSurface = layout.height() - 1L - Math.round(depth);
long gapSurface = (long) lowerSurfaceY + layout.minimumGap();
@@ -36,6 +36,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
@@ -65,7 +66,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
int depth = h.getDepth();
int height = h.getHeight();
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
@@ -74,21 +75,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
for (int zf = 0; zf < depth; zf++) {
int worldX = x + xf;
int worldZ = z + zf;
if (!complex.allowsNewDiscreteContentAt(worldX, worldZ)) {
writeHistoricalColumn(
h,
mantle,
xf,
zf,
worldX,
worldZ,
height,
engine,
complex
);
continue;
}
IrisBiome biome = biomeCache.get(xf, zf);
ResolvedBiome resolved = resolve(biomeKey(
biome,
@@ -117,11 +103,39 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
context.getDimensionStackLayout(xf, zf)
);
}
writeHistoricalColumn(h, mantle, xf, zf, worldX, worldZ, height, engine, complex);
}
}
engine.getMetrics().getBiome().put(p.getMilliseconds());
}
public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk<PlatformBiome> biomes,
ChunkContext context) {
if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
return;
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
MantleChunk<Matter> chunk = mantle.getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
IrisDimensionStackActuator.clearNaturalMetadata(chunk, context, biomes.getHeight());
chunk.deleteSlices(MatterBiomeInject.class);
for (int localX = 0; localX < biomes.getWidth(); localX += 4) {
for (int localZ = 0; localZ < biomes.getDepth(); localZ += 4) {
for (int y = 0; y < biomes.getHeight(); y += 4) {
PlatformBiome biome = biomes.getRaw(localX, y, localZ);
if (biome != null) {
mantle.set(x + localX, y, z + localZ, BiomeInjectMatter.get(biome.key()));
}
}
}
}
}
} finally {
chunk.release();
}
}
private void writeHistoricalColumn(
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
@@ -137,14 +151,17 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (transitionPlan == null) {
return;
}
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(worldX, worldZ);
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
if (terrainSample.newEpochWeight() == 1D) {
return;
}
int minimumWorldY = engine.getMinHeight();
String activeKey = sample.historicalPhysicalBiomeKeyAt(minimumWorldY).orElse(null);
String activeKey = transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY, worldZ, terrainSample).orElse(null);
int rangeStart = 0;
for (int internalY = 1; internalY <= height; internalY++) {
String nextKey = internalY == height
? null
: sample.historicalPhysicalBiomeKeyAt(minimumWorldY + internalY).orElse(null);
: transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY + internalY, worldZ, terrainSample).orElse(null);
if (Objects.equals(activeKey, nextKey)) {
continue;
}
@@ -179,7 +196,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (resolved.biome() != null) {
output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome());
}
mantle.set(worldX, 0, worldZ, resolved.matter());
if (mantle != null) {
mantle.set(worldX, 0, worldZ, resolved.matter());
}
}
private void applyDimensionStackBiomes(
@@ -256,7 +275,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (minimumY > maximumY) {
return;
}
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
if (mantle != null) {
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
}
if (resolved.biome() != null) {
output.set(
localX,
@@ -268,7 +289,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
resolved.biome()
);
}
if (resolved.matter() != null) {
if (mantle != null && resolved.matter() != null) {
mantle.set(worldX, minimumY, worldZ, resolved.matter());
}
}
@@ -33,6 +33,7 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -81,8 +82,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
if (getEngine().getDimensionStackContext() == null) {
return;
}
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
.getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) {
@@ -100,7 +101,26 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
}
}
} finally {
mantleChunk.release();
if (mantleChunk != null) {
mantleChunk.release();
}
}
}
public static void clearNaturalMetadata(MantleChunk<Matter> chunk, ChunkContext context, int height) {
MetadataCleaner metadata = new MetadataCleaner(chunk, height);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
DimensionStackLayout layout = context.getDimensionStackLayout(x, z);
if (layout == null) {
continue;
}
for (int y = 0; y < height; y++) {
if (layout.isHostFeatureProtectedY(y)) {
metadata.clear(x, y, z);
}
}
}
}
}
@@ -229,17 +249,46 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
}
void clear(int x, int y, int z) {
int sectionIndex = y >> 4;
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
if (slices == null) {
slices = resolveSlices(sectionIndex);
slicesBySection[sectionIndex] = slices;
if (chunk == null) {
return;
}
for (MatterSlice<?> slice : slices) {
slice.set(x, y & 15, z, null);
synchronized (chunk) {
int sectionIndex = y >> 4;
captureNaturalFacts(sectionIndex, x, y & 15, z);
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
if (slices == null) {
slices = resolveSlices(sectionIndex);
slicesBySection[sectionIndex] = slices;
}
for (MatterSlice<?> slice : slices) {
slice.set(x, y & 15, z, null);
}
}
}
private void captureNaturalFacts(int sectionIndex, int x, int y, int z) {
Matter section = chunk.get(sectionIndex);
if (section == null) {
return;
}
MatterSlice<MatterCavern> caverns = section.getSlice(MatterCavern.class);
MatterSlice<HydrologyCaveCell> hydrology = section.getSlice(HydrologyCaveCell.class);
MatterCavern cavern = caverns == null ? null : caverns.get(x, y, z);
HydrologyCaveCell cell = hydrology == null ? null : hydrology.get(x, y, z);
if (cavern == null && cell == null) {
return;
}
MatterSlice<PreObjectMatterCell> journal = section.slice(PreObjectMatterCell.class);
PreObjectMatterCell previous = journal.get(x, y, z);
if (cavern != null) {
previous = previous == null ? PreObjectMatterCell.cavern(cavern) : previous.captureCavern(cavern);
}
if (cell != null) {
previous = previous == null ? PreObjectMatterCell.hydrology(cell) : previous.captureHydrology(cell);
}
journal.set(x, y, z, previous);
}
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
Matter section = chunk.get(sectionIndex);
if (section == null) {
@@ -0,0 +1,157 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.BoundaryGeometryInfluence;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TransitionGeometryBlender;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
public final class IrisTransitionGeometryActuator extends EngineAssignedComponent implements EngineStage {
public IrisTransitionGeometryActuator(Engine engine) {
super(engine, "Terrain Transition");
}
@Override
public void close() {
super.close();
}
@Override
public void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes,
boolean multicore, ChunkContext context) {
TransitionGenerationPlan plan = context.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
return;
}
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context);
if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
blendGeometry(x, z, blocks, context, source, plan);
}
}
public static SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, int minimumY,
ChunkContext context, boolean boundaryOnly) throws IOException {
GeometrySource source = new GeometrySource(blocks, biomes, minimumY, context);
return boundaryOnly
? SavedTerrainChunk.captureBoundary(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source)
: SavedTerrainChunk.capture(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source);
}
private void blendGeometry(int x, int z, Hunk<PlatformBlockState> blocks, ChunkContext context,
GeometrySource source, TransitionGenerationPlan plan) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, PlatformBlockState> resolved = new HashMap<>();
for (int localX = 0; localX < blocks.getWidth(); localX++) {
for (int localZ = 0; localZ < blocks.getDepth(); localZ++) {
BoundaryGeometryInfluence influence = plan.geometryAt(x + localX, z + localZ);
if (influence.newTerrainWeight() == 1D || influence.contributions().isEmpty()) {
continue;
}
BoundaryColumnGeometry current = source.column(localX, localZ);
BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(
influence, x + localX, z + localZ, current);
if (blended == current) {
continue;
}
double expectedFloor = GenerationBlend.interpolate(
plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(),
context.getRoundedHeight(localX, localZ), influence.newTerrainWeight());
int floor = blended.surfaceOffsetNear(expectedFloor);
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
PlatformBlockState existing = blocks.getRaw(localX, offset, localZ);
if (existing != null && existing.key().equals(voxel.stateKey())) {
continue;
}
PlatformBlockState replacement = resolved.computeIfAbsent(voxel.stateKey(), registries::blockOrNull);
if (replacement == null) {
throw new IllegalStateException("Terrain boundary requires unavailable block state " + voxel.stateKey());
}
blocks.setRaw(localX, offset, localZ, replacement);
}
context.setTerrainHeight(localX, localZ, floor);
}
}
}
private static final class GeometrySource implements SavedTerrainChunk.VoxelSource {
private final Hunk<PlatformBlockState> blocks;
private final Hunk<PlatformBiome> biomes;
private final int minimumY;
private final ChunkContext context;
private final Map<PlatformBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>();
private GeometrySource(Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, int minimumY, ChunkContext context) {
this.blocks = blocks;
this.biomes = biomes;
this.minimumY = minimumY;
this.context = context;
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
PlatformBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ);
if (state == null) {
state = IrisPlatforms.get().registries().air();
}
return voxels.computeIfAbsent(state, GeometrySource::encode);
}
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(Math.max(0, Math.min(blocks.getHeight() - 1, context.getRoundedHeight(localX, localZ))));
}
@Override
public String biome(int localX, int worldY, int localZ) throws IOException {
PlatformBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ);
if (biome == null) {
throw new IOException("Natural terrain has no physical biome at " + localX + "," + worldY + "," + localZ);
}
return biome.key();
}
private BoundaryColumnGeometry column(int localX, int localZ) {
ArrayList<BoundaryColumnGeometry.Voxel> values = new ArrayList<>(blocks.getHeight());
for (int offset = 0; offset < blocks.getHeight(); offset++) {
values.add(voxel(localX, minimumY + offset, localZ));
}
return BoundaryColumnGeometry.fromVoxels(minimumY, values);
}
private static BoundaryColumnGeometry.Voxel encode(PlatformBlockState state) {
boolean custom = state.isCustom();
PlatformBlockState nativeState = state.placementBaseState();
if (nativeState != null) {
state = nativeState;
}
BoundaryColumnGeometry.Phase phase = state.isAir() ? BoundaryColumnGeometry.Phase.AIR
: state.isFluid() ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.SOLID;
String fluidKey = state.isFluid() ? state.key()
: state.isWaterLogged() ? "minecraft:water[level=0]" : "";
boolean protectedContent = custom || state.isDecorant() || state.isFoliage() || state.isTreeBlock()
|| state.hasTileEntity();
return new BoundaryColumnGeometry.Voxel(state.key(), phase, fluidKey, protectedContent);
}
}
}
@@ -40,17 +40,16 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
double localFluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
if (height != Math.round(localFluidHeight)) {
return;
}
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
if (Math.round(heightStream.get(realX1, realZ)) >= getEngine().getMantle().getFluidHeight(realX1, realZ)
&& Math.round(heightStream.get(realX_1, realZ)) >= getEngine().getMantle().getFluidHeight(realX_1, realZ)
&& Math.round(heightStream.get(realX, realZ1)) >= getEngine().getMantle().getFluidHeight(realX, realZ1)
&& Math.round(heightStream.get(realX, realZ_1)) >= getEngine().getMantle().getFluidHeight(realX, realZ_1)) {
return;
}
@@ -60,7 +60,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
int fluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
if (inferredType == InferredType.SHORE && height < fluidHeight) {
return;
}
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.framework.render.Renderer;
@@ -75,6 +76,9 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.nio.file.Path;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
@@ -166,12 +170,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
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);
return generationSessions.acquireForEngine(this, operation);
}
default long getGenerationSessionId() {
@@ -238,6 +237,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getTarget().getData();
}
Path getPackSource();
default IrisWorld getWorld() {
return getTarget().getWorld();
}
@@ -448,6 +449,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
public final class EngineLifecycleTasks {
@@ -13,10 +14,16 @@ public final class EngineLifecycleTasks {
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;
try {
Optional<GenerationSessionLease> admitted = tryAcquire(engine, operation);
if (admitted.isEmpty()) {
return false;
}
try (GenerationSessionLease lease = admitted.get();
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;
@@ -27,9 +34,17 @@ public final class EngineLifecycleTasks {
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();
Objects.requireNonNull(engine);
Objects.requireNonNull(operation);
try {
Optional<GenerationSessionLease> admitted = tryAcquire(engine, operation);
if (admitted.isEmpty()) {
return unavailable;
}
try (GenerationSessionLease lease = admitted.get();
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return task.get();
}
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return unavailable;
@@ -37,4 +52,11 @@ public final class EngineLifecycleTasks {
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
private static Optional<GenerationSessionLease> tryAcquire(Engine engine, String operation)
throws GenerationSessionException {
GenerationSessionManager sessions = engine.getGenerationSessions();
return sessions == null ? Optional.of(GenerationSessionLease.noop())
: sessions.tryAcquireForEngine(engine, operation);
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.actuator.IrisBiomeActuator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -31,6 +33,21 @@ import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
public interface EngineMode extends Staged {
KList<EngineStage> getTerrainStages();
void registerTerrainStage(EngineStage stage);
EngineStage getTransitionStage();
default void generateTerrain(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes,
boolean multicore, ChunkContext context) {
context.setTerrainBiomeOutput(biomes);
for (EngineStage stage : getTerrainStages()) {
stage.generate(x, z, blocks, biomes, multicore, context);
}
getTransitionStage().generate(x, z, blocks, biomes, multicore, context);
}
void close();
Engine getEngine();
@@ -159,8 +176,12 @@ public interface EngineMode extends Staged {
return getEngine().getMantle();
}
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateMatter(x, z, multicore, context);
default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateTerrainMatter(x, z, multicore, context);
}
default void generateContentMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateContentMatter(x, z, multicore, context);
}
@BlockCoordinates
@@ -180,6 +201,9 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
getComplex().getResolvedTerrain().generate(this, x, z, blocks, biomes, multicore, ctx);
IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx);
ctx.beginContent();
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
@@ -19,10 +19,23 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.util.concurrent.CompletableFuture;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.volmlib.util.collection.KList;
public interface EnginePlatformHooks {
default CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"This platform does not support saved terrain checkpoints."));
}
default CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"This platform does not support saved terrain boundary capture."));
}
/**
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms
* without native structures return no volumes, which keeps the object veto free on those platforms.
@@ -1,17 +1,20 @@
package art.arcane.iris.engine.framework;
public final class GenerationSessionLease implements AutoCloseable {
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L);
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L, null);
private final GenerationSessionManager manager;
private final GenerationSessionManager.GenerationSessionState state;
private final long sessionId;
private final GenerationTransitionGate.Participation participation;
private boolean released;
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) {
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state,
long sessionId, GenerationTransitionGate.Participation participation) {
this.manager = manager;
this.state = state;
this.sessionId = sessionId;
this.participation = participation;
this.released = false;
}
@@ -23,6 +26,12 @@ public final class GenerationSessionLease implements AutoCloseable {
return sessionId;
}
public void detachThread() {
if (participation != null) {
participation.detachThread();
}
}
@Override
public void close() {
if (released || state == null) {
@@ -30,6 +39,10 @@ public final class GenerationSessionLease implements AutoCloseable {
}
released = true;
manager.releaseLease(state);
try {
manager.releaseLease(state);
} finally {
participation.close();
}
}
}
@@ -1,22 +1,94 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CancellationException;
public final class GenerationSessionManager {
private final AtomicLong sessionSequence;
private final AtomicReference<GenerationSessionState> current;
private final Object drainMonitor;
private final GenerationTransitionGate transitionGate;
public GenerationSessionManager() {
this(false);
}
public GenerationSessionManager(boolean studioTransitions) {
this.transitionGate = new GenerationTransitionGate(studioTransitions);
this.sessionSequence = new AtomicLong(0L);
this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
this.drainMonitor = new Object();
}
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
GenerationTransitionGate.Participation participation = transitionGate.enter();
try {
return acquireAdmitted(operation, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
public GenerationTransitionGate transitionGate() {
return transitionGate;
}
public GenerationSessionLease acquireForEngine(Engine engine, String operation) throws GenerationSessionException {
GenerationTransitionGate.Participation participation;
try {
participation = transitionGate.enter();
} catch (CancellationException cancellation) {
GenerationSessionException failure = new GenerationSessionException(
"Generation session cancelled while waiting to run " + operation + ".",
engine.isClosing() || engine.isClosed());
failure.initCause(cancellation);
throw failure;
}
try {
if (engine.isClosing() || engine.isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", engine.isClosed());
}
return acquireAdmitted(operation, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
public Optional<GenerationSessionLease> tryAcquireForEngine(Engine engine, String operation)
throws GenerationSessionException {
Optional<GenerationTransitionGate.Participation> entered = transitionGate.tryEnter();
if (entered.isEmpty()) {
return Optional.empty();
}
GenerationTransitionGate.Participation participation = entered.get();
try {
if (engine.isClosing() || engine.isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", engine.isClosed());
}
IrisContext context = IrisContext.get();
return Optional.of(context != null && context.getEngine() == engine && context.getGenerationSessionId() != 0L
? continueAdmittedSession(operation, context.getGenerationSessionId(), participation)
: acquireAdmitted(operation, participation));
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
private GenerationSessionLease acquireAdmitted(String operation,
GenerationTransitionGate.Participation participation)
throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || !state.accepting().get()) {
@@ -34,11 +106,23 @@ public final class GenerationSessionManager {
throw rejected(operation, state);
}
return new GenerationSessionLease(this, state, state.sessionId());
return new GenerationSessionLease(this, state, state.sessionId(), participation);
}
}
public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException {
GenerationTransitionGate.Participation participation = transitionGate.continueAdmittedWork();
try {
return continueAdmittedSession(operation, sessionId, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
private GenerationSessionLease continueAdmittedSession(String operation, long sessionId,
GenerationTransitionGate.Participation participation)
throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || state.sessionId() != sessionId) {
@@ -51,7 +135,7 @@ public final class GenerationSessionManager {
continue;
}
return new GenerationSessionLease(this, state, state.sessionId());
return new GenerationSessionLease(this, state, state.sessionId(), participation);
}
}
@@ -0,0 +1,200 @@
package art.arcane.iris.engine.framework;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
public final class GenerationTransitionGate {
private final boolean enabled;
private final Participation noop = new Participation(null);
private final Map<Thread, Integer> participants = new IdentityHashMap<>();
private int active;
private int waitingTransitions;
private Thread transitionOwner;
public GenerationTransitionGate(boolean enabled) {
this.enabled = enabled;
}
public Participation enter() {
if (!enabled) {
return noop;
}
synchronized (this) {
return enter(false);
}
}
public Optional<Participation> tryEnter() {
if (!enabled) {
return Optional.of(noop);
}
synchronized (this) {
Thread owner = Thread.currentThread();
return blocked(owner, false) ? Optional.empty() : Optional.of(admit(owner));
}
}
Participation continueAdmittedWork() {
if (!enabled) {
return noop;
}
synchronized (this) {
return enter(true);
}
}
private Participation enter(boolean continuation) {
Thread owner = Thread.currentThread();
while (blocked(owner, continuation)) {
awaitChange();
}
return admit(owner);
}
private boolean blocked(Thread owner, boolean continuation) {
return transitionOwner != null && transitionOwner != owner
|| waitingTransitions > 0 && transitionOwner != owner
&& !participants.containsKey(owner) && !(continuation && active > 0);
}
private Participation admit(Thread owner) {
active++;
participants.merge(owner, 1, Integer::sum);
return new Participation(owner);
}
public synchronized Transition beginTransition(long timeoutMillis) {
if (!enabled) {
throw new IllegalStateException("Live generation transitions are disabled for this engine.");
}
if (timeoutMillis < 1L) {
throw new IllegalArgumentException("Generation transition timeout must be positive.");
}
Thread owner = Thread.currentThread();
if (participants.containsKey(owner) || transitionOwner == owner) {
throw new IllegalStateException("Cannot reload generation from inside an active generation operation.");
}
waitingTransitions++;
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
try {
while (transitionOwner != null || active > 0) {
long remaining = deadline - System.nanoTime();
if (remaining <= 0L) {
throw new IllegalStateException("Timed out draining generation for a Studio update after "
+ timeoutMillis + "ms; active operations=" + active + ".");
}
try {
TimeUnit.NANOSECONDS.timedWait(this, remaining);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while draining generation for a Studio update.", failure);
}
}
transitionOwner = owner;
return new Transition(owner);
} finally {
waitingTransitions--;
notifyAll();
}
}
private void awaitChange() {
try {
wait();
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
CancellationException cancellation = new CancellationException(
"Interrupted while waiting for generation admission.");
cancellation.initCause(failure);
throw cancellation;
}
}
public final class Participation implements AutoCloseable {
private final Thread owner;
private boolean closed;
private boolean attached = true;
private Participation(Thread owner) {
this.owner = owner;
}
public Participation attach() {
if (owner == null) {
return this;
}
synchronized (GenerationTransitionGate.this) {
if (closed) {
throw new IllegalStateException("Generation participation has already ended.");
}
return enter(true);
}
}
public void detachThread() {
if (owner == null) {
return;
}
synchronized (GenerationTransitionGate.this) {
if (!closed && attached) {
detach();
}
}
}
private void detach() {
int depth = participants.get(owner);
if (depth == 1) {
participants.remove(owner);
} else {
participants.put(owner, depth - 1);
}
attached = false;
}
@Override
public void close() {
if (owner == null) {
return;
}
synchronized (GenerationTransitionGate.this) {
if (closed) {
return;
}
closed = true;
if (attached) {
detach();
}
active--;
GenerationTransitionGate.this.notifyAll();
}
}
}
public final class Transition implements AutoCloseable {
private final Thread owner;
private boolean closed;
private Transition(Thread owner) {
this.owner = owner;
}
@Override
public void close() {
synchronized (GenerationTransitionGate.this) {
if (closed) {
return;
}
if (Thread.currentThread() != owner || transitionOwner != owner) {
throw new IllegalStateException("Generation transition belongs to another thread.");
}
closed = true;
transitionOwner = null;
GenerationTransitionGate.this.notifyAll();
}
}
}
}
@@ -19,15 +19,20 @@
package art.arcane.iris.engine.framework;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator;
public abstract class IrisEngineMode implements EngineMode {
private final Engine engine;
private final EngineStage transitionStage;
private final KList<EngineStage> stages;
private final KList<EngineStage> terrainStages;
private boolean closed;
public IrisEngineMode(Engine engine) {
this.engine = engine;
this.transitionStage = new IrisTransitionGeometryActuator(engine);
this.stages = new KList<>();
this.terrainStages = new KList<>();
this.closed = false;
}
@@ -39,6 +44,9 @@ public abstract class IrisEngineMode implements EngineMode {
closed = true;
dump();
terrainStages.forEach(EngineStage::close);
terrainStages.clear();
transitionStage.close();
}
@Override
@@ -51,6 +59,21 @@ public abstract class IrisEngineMode implements EngineMode {
return stages;
}
@Override
public EngineStage getTransitionStage() {
return transitionStage;
}
@Override
public KList<EngineStage> getTerrainStages() {
return terrainStages;
}
@Override
public void registerTerrainStage(EngineStage stage) {
terrainStages.add(stage);
}
@Override
public void registerStage(EngineStage stage) {
stages.add(stage);
@@ -0,0 +1,258 @@
package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
public final class BoundaryColumnGeometry {
public static final int MAXIMUM_HEIGHT = 65_536;
private static final Comparator<Voxel> VOXEL_ORDER = Comparator.comparing(Voxel::stateKey)
.thenComparing(Voxel::phase)
.thenComparing(Voxel::fluidStateKey)
.thenComparing(Voxel::protectedContent);
private static final Voxel AIR = new Voxel("minecraft:air", Phase.AIR, "", false);
private static final BoundaryColumnGeometry EMPTY = new BoundaryColumnGeometry(0, List.of(), new int[0], new short[0]);
private final int minimumY;
private final List<Voxel> palette;
private final int[] runEnds;
private final short[] paletteIndices;
public BoundaryColumnGeometry(int minimumY, List<Voxel> palette, int[] runEnds, short[] paletteIndices) {
this.minimumY = minimumY;
this.palette = List.copyOf(Objects.requireNonNull(palette, "Geometry palette"));
this.runEnds = Objects.requireNonNull(runEnds, "Geometry run ends").clone();
this.paletteIndices = Objects.requireNonNull(paletteIndices, "Geometry palette indices").clone();
validate();
}
public static BoundaryColumnGeometry empty() {
return EMPTY;
}
public static BoundaryColumnGeometry fromVoxels(int minimumY, List<Voxel> voxels) {
Objects.requireNonNull(voxels, "Geometry voxels");
if (voxels.size() > MAXIMUM_HEIGHT) {
throw new IllegalArgumentException("Geometry column exceeds maximum height");
}
TreeSet<Voxel> used = new TreeSet<>(VOXEL_ORDER);
Voxel previous = null;
for (Voxel voxel : voxels) {
if (previous == null || !previous.equals(voxel)) {
used.add(voxel);
previous = voxel;
}
}
List<Voxel> palette = List.copyOf(used);
Map<Voxel, Short> indices = new HashMap<>(palette.size());
for (int index = 0; index < palette.size(); index++) {
if (index > Short.MAX_VALUE) {
throw new IllegalArgumentException("Geometry palette exceeds compact index capacity");
}
indices.put(palette.get(index), (short) index);
}
int[] ends = new int[voxels.size()];
short[] values = new short[voxels.size()];
int runCount = 0;
previous = null;
for (int offset = 0; offset < voxels.size(); offset++) {
Voxel voxel = voxels.get(offset);
if (previous == null || !previous.equals(voxel)) {
values[runCount++] = indices.get(voxel);
previous = voxel;
}
ends[runCount - 1] = offset + 1;
}
return new BoundaryColumnGeometry(minimumY, palette,
Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount));
}
public int minimumY() {
return minimumY;
}
public int height() {
return runEnds.length == 0 ? 0 : runEnds[runEnds.length - 1];
}
public List<Voxel> palette() {
return palette;
}
public int[] runEnds() {
return runEnds.clone();
}
public short[] paletteIndices() {
return paletteIndices.clone();
}
public Voxel voxelAt(int worldY) {
long offset = (long) worldY - minimumY;
if (offset < 0 || offset >= height()) {
return AIR;
}
return palette.get(paletteIndices[runAt((int) offset)]);
}
public boolean isEnclosedOpenAt(int worldY) {
long offset = (long) worldY - minimumY;
if (offset < 0 || offset >= height() || voxelAt(worldY).phase() == Phase.SOLID) {
return false;
}
return hasSolidAbove(worldY);
}
public boolean hasSolidAbove(int worldY) {
long offset = (long) worldY - minimumY;
if (offset >= height() - 1L) {
return false;
}
int startRun = offset < 0 ? 0 : runAt((int) offset);
for (int run = startRun; run < runEnds.length; run++) {
if (runEnds[run] <= offset + 1L) {
continue;
}
if (palette.get(paletteIndices[run]).phase() == Phase.SOLID) {
return true;
}
}
return false;
}
public List<Voxel> voxels() {
ArrayList<Voxel> values = new ArrayList<>(height());
int start = 0;
for (int run = 0; run < runEnds.length; run++) {
Voxel value = palette.get(paletteIndices[run]);
for (int offset = start; offset < runEnds[run]; offset++) {
values.add(value);
}
start = runEnds[run];
}
return List.copyOf(values);
}
public int surfaceOffsetNear(double expectedOffset) {
if (!Double.isFinite(expectedOffset) || height() == 0) {
throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry");
}
int nearest = -1;
double nearestDistance = Double.POSITIVE_INFINITY;
for (int offset = 0; offset + 1 < height(); offset++) {
Voxel current = voxelAt(minimumY + offset);
Voxel above = voxelAt(minimumY + offset + 1);
if (current.phase() != Phase.SOLID || current.protectedContent()
|| above.phase() == Phase.SOLID && !above.protectedContent()) {
continue;
}
double distance = Math.abs(offset - expectedOffset);
if (distance < nearestDistance) {
nearest = offset;
nearestDistance = distance;
}
}
if (nearest >= 0) {
return nearest;
}
for (int offset = height() - 1; offset >= 0; offset--) {
Voxel current = voxelAt(minimumY + offset);
if (current.phase() == Phase.SOLID && !current.protectedContent()) {
return offset;
}
}
return 0;
}
public double[] solidDistances() {
return phaseDistances(Phase.SOLID);
}
public double[] fluidDistances() {
return phaseDistances(Phase.FLUID);
}
private double[] phaseDistances(Phase phase) {
int height = height();
double[] distances = new double[height];
boolean[] solid = new boolean[height];
int start = 0;
for (int run = 0; run < runEnds.length; run++) {
Voxel value = palette.get(paletteIndices[run]);
boolean occupied = value.phase() == phase && !value.protectedContent();
Arrays.fill(solid, start, runEnds[run], occupied);
start = runEnds[run];
}
double distance = height + 0.5D;
for (int offset = 0; offset < height; offset++) {
distance = offset > 0 && solid[offset] != solid[offset - 1] ? 0.5D : distance + 1D;
distances[offset] = distance;
}
distance = height + 0.5D;
for (int offset = height - 1; offset >= 0; offset--) {
distance = offset + 1 < height && solid[offset] != solid[offset + 1] ? 0.5D : distance + 1D;
distances[offset] = Math.min(distances[offset], distance) * (solid[offset] ? 1D : -1D);
}
return distances;
}
@Override
public boolean equals(Object compared) {
return this == compared || compared instanceof BoundaryColumnGeometry other
&& minimumY == other.minimumY && palette.equals(other.palette)
&& Arrays.equals(runEnds, other.runEnds) && Arrays.equals(paletteIndices, other.paletteIndices);
}
@Override
public int hashCode() {
return Objects.hash(minimumY, palette, Arrays.hashCode(runEnds), Arrays.hashCode(paletteIndices));
}
private int runAt(int offset) {
int index = Arrays.binarySearch(runEnds, offset + 1);
return index >= 0 ? index : -index - 1;
}
private void validate() {
if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT
|| runEnds.length != paletteIndices.length) {
throw new IllegalArgumentException("Invalid compact geometry sizes");
}
int previousEnd = 0;
for (int run = 0; run < runEnds.length; run++) {
if (runEnds[run] <= previousEnd || runEnds[run] > MAXIMUM_HEIGHT
|| paletteIndices[run] < 0 || paletteIndices[run] >= palette.size()) {
throw new IllegalArgumentException("Invalid compact geometry run");
}
previousEnd = runEnds[run];
}
if (height() > 0) {
Math.toIntExact((long) minimumY + height() - 1L);
}
}
public enum Phase {
AIR,
SOLID,
FLUID
}
public record Voxel(String stateKey, Phase phase, String fluidStateKey, boolean protectedContent) {
public Voxel {
Objects.requireNonNull(stateKey, "Block state key");
Objects.requireNonNull(phase, "Voxel phase");
Objects.requireNonNull(fluidStateKey, "Fluid state key");
if (stateKey.isBlank()) {
throw new IllegalArgumentException("Block state key cannot be blank");
}
if (phase == Phase.FLUID && fluidStateKey.isBlank()) {
throw new IllegalArgumentException("Fluid voxels require a fluid state key");
}
}
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.engine.history;
import java.util.List;
import java.util.Objects;
public record BoundaryGeometryInfluence(double newTerrainWeight, double openingWeight, List<Contribution> contributions) {
private static final BoundaryGeometryInfluence NONE = new BoundaryGeometryInfluence(1D, 1D, List.of());
public BoundaryGeometryInfluence {
if (!Double.isFinite(newTerrainWeight) || newTerrainWeight < 0D || newTerrainWeight > 1D) {
throw new IllegalArgumentException("Geometry blend weight must be between zero and one");
}
if (!Double.isFinite(openingWeight) || openingWeight < 0D || openingWeight > 1D) {
throw new IllegalArgumentException("Opening blend weight must be between zero and one");
}
contributions = List.copyOf(contributions);
double total = 0D;
for (Contribution contribution : contributions) {
total += contribution.weight();
}
if (!contributions.isEmpty() && Math.abs(total - 1D) > 1E-9D) {
throw new IllegalArgumentException("Geometry contribution weights must sum to one");
}
}
public static BoundaryGeometryInfluence none() {
return NONE;
}
public record Contribution(BoundaryColumnGeometry geometry, double weight) {
public Contribution {
Objects.requireNonNull(geometry, "Historical geometry");
if (geometry.height() == 0) {
throw new IllegalArgumentException("Historical boundary geometry is missing");
}
if (!Double.isFinite(weight) || weight <= 0D || weight > 1D) {
throw new IllegalArgumentException("Geometry contribution weight must be positive and at most one");
}
}
}
}
@@ -8,6 +8,7 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -184,6 +185,34 @@ public final class ChunkGenerationOwnership {
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
for (long activation : selected) {
requireActivation(activation);
}
lock.writeLock().lock();
try {
int removed = 0;
for (long regionKey : orderedRegionKeys()) {
RegionGenerationOwnership region = loadExistingRegion(regionX(regionKey), regionZ(regionKey));
if (region == null) {
continue;
}
int count = region.discardUnstoredClaims(stored, selected);
if (count > 0) {
region.persist(directory);
explicitChunkCount -= count;
regionAssignmentCounts.put(regionKey, region.assignmentCount());
removed += count;
}
}
return removed;
} finally {
lock.writeLock().unlock();
}
}
public int persist() throws IOException {
lock.writeLock().lock();
try {
@@ -6,11 +6,9 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
public final class ChunkGenerationSemantics {
static final int MAX_KEY_BYTES = 1_024;
@@ -18,12 +16,15 @@ public final class ChunkGenerationSemantics {
static final int MAX_RIVER_FEATURES = 16_384;
static final int MAX_STRUCTURES = 4_096;
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_][a-z0-9._:/#@-]*");
private static final Comparator<StructureOccurrence> STRUCTURE_COMPARATOR = Comparator
.comparing(StructureOccurrence::key)
.thenComparingInt(occurrence -> occurrence.position().x())
.thenComparingInt(occurrence -> occurrence.position().z())
.thenComparingInt(occurrence -> occurrence.position().y());
private static final Comparator<PointOfInterest> POI_COMPARATOR = Comparator.comparing(PointOfInterest::key)
.thenComparingInt(point -> point.position().x())
.thenComparingInt(point -> point.position().z())
.thenComparingInt(point -> point.position().y());
private static final Comparator<RiverFeatureOccurrence> RIVER_FEATURE_COMPARATOR = Comparator
.comparing(RiverFeatureOccurrence::profileKey)
.thenComparing(RiverFeatureOccurrence::type)
@@ -43,6 +44,7 @@ public final class ChunkGenerationSemantics {
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private final Set<PointOfInterest> pointsOfInterest;
private ChunkGenerationSemantics(Builder builder) {
chunkX = builder.chunkX;
@@ -56,6 +58,7 @@ public final class ChunkGenerationSemantics {
objectKeys = immutableKeys(builder.objectKeys, "object");
riverFeatures = immutableRiverFeatures(builder.riverFeatures);
structures = immutableStructures(builder.structures);
pointsOfInterest = immutablePoints(builder.pointsOfInterest);
}
public static Builder builder(int chunkX, int chunkZ, long activationId) {
@@ -102,6 +105,10 @@ public final class ChunkGenerationSemantics {
return riverFeatures;
}
public Set<PointOfInterest> pointsOfInterest() {
return pointsOfInterest;
}
public Set<StructureOccurrence> structures() {
return structures;
}
@@ -129,7 +136,8 @@ public final class ChunkGenerationSemantics {
|| !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys)
|| !objectKeys.containsAll(requiredUpdate.objectKeys)
|| !riverFeatures.containsAll(requiredUpdate.riverFeatures)
|| !structures.containsAll(requiredUpdate.structures);
|| !structures.containsAll(requiredUpdate.structures)
|| !pointsOfInterest.containsAll(requiredUpdate.pointsOfInterest);
if (sealed && addsFacts) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics"
@@ -153,7 +161,9 @@ public final class ChunkGenerationSemantics {
.addRiverFeatures(riverFeatures)
.addRiverFeatures(requiredUpdate.riverFeatures)
.addStructures(structures)
.addStructures(requiredUpdate.structures);
.addStructures(requiredUpdate.structures)
.addPointsOfInterest(pointsOfInterest)
.addPointsOfInterest(requiredUpdate.pointsOfInterest);
if (sealed || requiredUpdate.sealed) {
merged.seal();
}
@@ -178,7 +188,8 @@ public final class ChunkGenerationSemantics {
&& riverProfileKeys.equals(semantics.riverProfileKeys)
&& objectKeys.equals(semantics.objectKeys)
&& riverFeatures.equals(semantics.riverFeatures)
&& structures.equals(semantics.structures);
&& structures.equals(semantics.structures)
&& pointsOfInterest.equals(semantics.pointsOfInterest);
}
@Override
@@ -194,7 +205,8 @@ public final class ChunkGenerationSemantics {
riverProfileKeys,
objectKeys,
riverFeatures,
structures
structures,
pointsOfInterest
);
}
@@ -230,11 +242,11 @@ public final class ChunkGenerationSemantics {
if (!requiredKey.equals(requiredKey.trim())) {
throw new IllegalArgumentException("Generation semantic resource keys must not contain surrounding whitespace: " + requiredKey);
}
if (!requiredKey.equals(requiredKey.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("Generation semantic resource keys must be lowercase: " + requiredKey);
}
if (!RESOURCE_KEY_PATTERN.matcher(requiredKey).matches()) {
throw new IllegalArgumentException("Generation semantic resource key is not normalized: " + requiredKey);
for (int index = 0; index < requiredKey.length(); index++) {
char character = requiredKey.charAt(index);
if (character == '\\' || Character.isISOControl(character)) {
throw new IllegalArgumentException("Generation semantic resource key contains an invalid character");
}
}
if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) {
throw new IllegalArgumentException("Generation semantic resource key is too long");
@@ -283,6 +295,28 @@ public final class ChunkGenerationSemantics {
return Collections.unmodifiableSet(validated);
}
private static Set<PointOfInterest> immutablePoints(Collection<PointOfInterest> points) {
if (points.size() > MAX_STRUCTURES) {
throw new IllegalArgumentException("A chunk contains too many points of interest");
}
TreeSet<PointOfInterest> copy = new TreeSet<>(POI_COMPARATOR);
for (PointOfInterest point : points) {
copy.add(Objects.requireNonNull(point, "point of interest"));
}
return Collections.unmodifiableSet(copy);
}
static Comparator<PointOfInterest> pointComparator() {
return POI_COMPARATOR;
}
public record PointOfInterest(String key, BlockPosition position) {
public PointOfInterest {
key = requireResourceKey(key);
position = Objects.requireNonNull(position, "position");
}
}
public record BlockPosition(int x, int y, int z) {
}
@@ -317,6 +351,7 @@ public final class ChunkGenerationSemantics {
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private final Set<PointOfInterest> pointsOfInterest;
private boolean sealed;
private Builder(int chunkX, int chunkZ, long activationId) {
@@ -333,6 +368,7 @@ public final class ChunkGenerationSemantics {
objectKeys = new TreeSet<>();
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
pointsOfInterest = new TreeSet<>(POI_COMPARATOR);
}
public Builder addSurfaceBiome(String key) {
@@ -450,6 +486,22 @@ public final class ChunkGenerationSemantics {
return this;
}
public Builder addPointOfInterest(PointOfInterest point) {
Objects.requireNonNull(point, "point of interest");
if (pointsOfInterest.size() >= MAX_STRUCTURES && !pointsOfInterest.contains(point)) {
throw new IllegalArgumentException("A chunk contains too many points of interest");
}
pointsOfInterest.add(point);
return this;
}
public Builder addPointsOfInterest(Collection<PointOfInterest> points) {
for (PointOfInterest point : points) {
addPointOfInterest(point);
}
return this;
}
public Builder seal() {
sealed = true;
return this;
@@ -0,0 +1,44 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public final class DiskBoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private static final int MAXIMUM_CACHED_CHUNKS = 32;
private final Path dimensionRoot;
private final int minimumY;
private final int height;
private final LinkedHashMap<Long, SavedTerrainChunk> chunks = new LinkedHashMap<>(32, 0.75F, true);
public DiskBoundaryCapture(Path dimensionRoot, int minimumY, int height) {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimension root");
this.minimumY = minimumY;
this.height = height;
}
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
SavedTerrainChunk chunk = chunks.get(key);
if (chunk == null) {
chunk = SavedTerrainChunk.read(dimensionRoot, chunkX, chunkZ, minimumY, height);
chunks.put(key, chunk);
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
Map.Entry<Long, SavedTerrainChunk> oldest = chunks.firstEntry();
chunks.remove(oldest.getKey());
}
}
return chunk.column(blockX, blockZ);
}
@Override
public void close() {
chunks.clear();
}
}
@@ -58,6 +58,23 @@ public final class GenerationBlend {
return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight);
}
public static boolean usesHistoricalMaterial(int blockX, int blockY, int blockZ, double newWeight) {
validateWeight(newWeight);
if (newWeight == 0D) {
return true;
}
if (newWeight == 1D) {
return false;
}
long hash = Math.floorDiv(blockX, 8) * 341873128712L
^ Math.floorDiv(blockY, 8) * 132897987541L
^ Math.floorDiv(blockZ, 8) * 42317861L;
hash = (hash ^ (hash >>> 30)) * 0xbf58476d1ce4e5b9L;
hash = (hash ^ (hash >>> 27)) * 0x94d049bb133111ebL;
hash ^= hash >>> 31;
return newWeight < (hash >>> 11) * 0x1.0p-53;
}
private static void validateWeight(double weight) {
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
@@ -7,4 +7,8 @@ import java.io.IOException;
@FunctionalInterface
public interface GenerationBoundarySignatureSampler {
TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException;
default TerrainBoundarySignatureStore.SignatureSampler open(IrisEngine engine) throws IOException {
return (blockX, blockZ) -> sample(engine, blockX, blockZ);
}
}
@@ -632,13 +632,16 @@ public final class GenerationBoundaryStore {
}
@Override
public synchronized boolean contains(int chunkX, int chunkZ) throws IOException {
public boolean contains(int chunkX, int chunkZ) throws IOException {
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
String hash = shardHashes.get(regionKey);
if (hash == null) {
return false;
}
byte[] mask = load(regionKey, hash);
byte[] mask;
synchronized (this) {
mask = load(regionKey, hash);
}
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
| Math.floorMod(chunkX, REGION_SIDE);
return (mask[localIndex >>> 3] & (1 << (localIndex & 7))) != 0;
@@ -11,31 +11,31 @@ import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
final class GenerationKernelSourceSeal {
final class GenerationBuildRevision {
private static final int MAXIMUM_BYTES = 8 * 1024 * 1024;
private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}");
private GenerationKernelSourceSeal() {
private GenerationBuildRevision() {
}
static String requireFingerprint(int abi, Class<?> factory) {
Descriptor descriptor = load(abi);
if (!descriptor.factoryClass().equals(factory.getName())) {
throw new IllegalStateException("Generation ABI " + abi + " is sealed for " + descriptor.factoryClass() + ".");
throw new IllegalStateException("Generation ABI " + abi + " declares factory " + descriptor.factoryClass() + ".");
}
return descriptor.fingerprint();
}
static Descriptor load(int abi) {
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".seal";
try (InputStream input = GenerationKernelSourceSeal.class.getResourceAsStream(resource)) {
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".revision";
try (InputStream input = GenerationBuildRevision.class.getResourceAsStream(resource)) {
if (input == null) {
throw new IOException("Missing generation kernel source seal " + resource + ".");
throw new IOException("Missing generation kernel build revision " + resource + ".");
}
byte[] bytes = input.readNBytes(MAXIMUM_BYTES + 1);
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n", -1);
if (lines.length < 4 || !lines[2].startsWith("factory\t")) {
throw new IOException("Generation kernel seal has no factory identity.");
throw new IOException("Generation build revision has no factory identity.");
}
String factoryClass = lines[2].substring("factory\t".length());
if (!factoryClass.matches("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)+")) {
@@ -52,7 +52,7 @@ final class GenerationKernelSourceSeal {
}
return new Descriptor(abi, factoryClass, List.copyOf(algorithms), fingerprint);
} catch (IOException failure) {
throw new IllegalStateException("Cannot load sealed Iris generation ABI " + abi + ".", failure);
throw new IllegalStateException("Cannot load Iris generation build revision for ABI " + abi + ".", failure);
}
}
@@ -60,13 +60,13 @@ final class GenerationKernelSourceSeal {
Objects.requireNonNull(factoryClass, "factoryClass");
Objects.requireNonNull(bytes, "bytes");
if (abi < 1 || bytes.length == 0 || bytes.length > MAXIMUM_BYTES) {
throw new IOException("Invalid generation kernel seal size or ABI.");
throw new IOException("Invalid generation build revision size or ABI.");
}
String source = new String(bytes, StandardCharsets.UTF_8);
String prefix = "iris-generation-kernel-seal-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n";
String prefix = "iris-generation-build-revision-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n";
if (!source.startsWith(prefix) || !source.endsWith("\n") || source.indexOf('\r') >= 0
|| !source.contains("\nsource\t") || !source.contains("\ndependency\t")) {
throw new IOException("Generation kernel seal does not match ABI " + abi + " and factory " + factoryClass + ".");
throw new IOException("Generation build revision does not match ABI " + abi + " and factory " + factoryClass + ".");
}
requireCanonicalEntries(source.substring(prefix.length()));
try {
@@ -79,7 +79,7 @@ final class GenerationKernelSourceSeal {
private static void requireCanonicalEntries(String source) throws IOException {
String[] entries = source.split("\n", -1);
if (entries.length > 30_000) {
throw new IOException("Generation kernel seal has too many entries.");
throw new IOException("Generation build revision has too many entries.");
}
int previousType = -1;
String previousKey = "";
@@ -92,7 +92,7 @@ final class GenerationKernelSourceSeal {
if (fields[0].equals("algorithm")) {
if (previousType != -1 || fields.length != 3 || !fields[1].matches("[1-9][0-9]{0,8}")
|| !fields[2].matches("[1-9][0-9]{0,8}")) {
throw new IOException("Generation kernel seal contains an invalid algorithm version.");
throw new IOException("Generation build revision contains an invalid algorithm version.");
}
int rng = Integer.parseInt(fields[1]);
int seed = Integer.parseInt(fields[2]);
@@ -116,14 +116,14 @@ final class GenerationKernelSourceSeal {
|| fields[1].chars().anyMatch(Character::isWhitespace)
|| (type == previousType && fields[1].compareTo(previousKey) <= 0)
|| (type >= 2 && !SHA256.matcher(fields[2]).matches())) {
throw new IOException("Generation kernel seal contains an invalid or noncanonical entry.");
throw new IOException("Generation build revision contains an invalid or noncanonical entry.");
}
previousType = type;
previousKey = fields[1];
hasScope |= type == 0;
}
if (!hasScope || !hasAlgorithm) {
throw new IOException("Generation kernel seal has no source scope or algorithm version.");
throw new IOException("Generation build revision has no source scope or algorithm version.");
}
}
@@ -346,15 +346,10 @@ public final class GenerationEpoch {
return dimensionKey.equals(other.dimensionKey)
&& dimensionTypeKey.equals(other.dimensionTypeKey)
&& environment.equals(other.environment)
&& generationMode.equals(other.generationMode)
&& internalFluidHeight == other.internalFluidHeight
&& minHeight == other.minHeight
&& height == other.height
&& logicalHeight == other.logicalHeight
&& Double.compare(coordinateScale, other.coordinateScale) == 0
&& upperTerrainEnabled == other.upperTerrainEnabled
&& upperDimensionKey.equals(other.upperDimensionKey)
&& upperDimensionGap == other.upperDimensionGap
&& dimensionTypeFingerprintSchema == other.dimensionTypeFingerprintSchema
&& dimensionTypeFingerprint.equals(other.dimensionTypeFingerprint);
}
@@ -1,23 +1,16 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
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.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.volmlib.util.collection.KList;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
@@ -55,7 +48,7 @@ public final class GenerationFindCatalog {
}
public static boolean hasObjectPlacement(Engine engine, String key) {
return engine.hasObjectPlacement(key) || retained(engine).objects.contains(normalizeObjectKey(key));
return engine.hasObjectPlacement(key) || containsKey(retained(engine).objects, normalizeObjectKey(key));
}
public static KList<String> retainedStructureKeys(Engine engine) {
@@ -63,7 +56,7 @@ public final class GenerationFindCatalog {
}
public static boolean hasRetainedStructurePlacement(Engine engine, String key) {
return retained(engine).structures.contains(normalizeKey(key));
return containsKey(retained(engine).structures, normalizeKey(key));
}
private static RetainedCatalog retained(Engine engine) {
@@ -90,87 +83,48 @@ public final class GenerationFindCatalog {
private static RetainedCatalog loadRetained(GenerationHistory history, GenerationManifest manifest) {
RetainedCatalog catalog = new RetainedCatalog(manifest.activeActivation().activationId());
Set<String> loadedPacks = new HashSet<>();
Long activationId = manifest.activeActivation().parentActivationId();
while (activationId != null) {
GenerationActivation activation = manifest.activation(activationId).orElseThrow();
GenerationEpoch epoch = manifest.epoch(activation.epochId()).orElseThrow();
String dimensionKey = epoch.dimensionContract().dimensionKey();
if (loadedPacks.add(epoch.packFingerprint() + ":" + dimensionKey)) {
try {
loadPack(catalog, history.packRoot(activationId), dimensionKey);
} catch (IOException error) {
throw new UncheckedIOException("Unable to read retained Iris find catalog for activation "
+ activationId, error);
}
}
activationId = activation.parentActivationId();
try {
history.forEachRecordedSemantic(semantics -> addRecorded(catalog, semantics));
} catch (IOException error) {
throw new UncheckedIOException("Unable to read recorded Iris find catalog", error);
}
return catalog;
}
private static void loadPack(RetainedCatalog catalog, Path packRoot, String dimensionKey) {
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Retained Iris dimension is absent: " + dimensionKey);
private static void addRecorded(RetainedCatalog catalog, ChunkGenerationSemantics semantics) {
if (!semantics.sealed() || semantics.activationId() == catalog.activeActivationId) {
return;
}
for (String key : semantics.surfaceBiomeKeys()) {
addBiome(catalog, key);
}
for (String key : semantics.caveBiomeKeys()) {
addBiome(catalog, key);
}
for (String key : semantics.regionKeys()) {
String normalized = normalizeKey(key);
if (!catalog.regions.containsKey(normalized)) {
IrisRegion region = new IrisRegion().setName(key);
region.setLoadKey(key);
catalog.regions.put(normalized, region);
}
addStructures(catalog.structures, dimension.getStructures());
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
catalog.biomes.putIfAbsent(normalizeKey(biome.getLoadKey()), biome);
addObjects(catalog.objects, biome.getObjects());
addStructures(catalog.structures, biome.getStructures());
}
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
catalog.regions.putIfAbsent(normalizeKey(region.getLoadKey()), region);
addObjects(catalog.objects, region.getObjects());
addStructures(catalog.structures, region.getStructures());
}
if (dimension.hasUpperDimension() && !dimensionKey.equals(dimension.getUpperDimension())) {
IrisDimension upper = data.getDimensionLoader().load(dimension.getUpperDimension(), false);
if (upper == null) {
throw new IllegalStateException("Retained Iris upper dimension is absent: "
+ dimension.getUpperDimension());
}
for (IrisBiome biome : upper.getReachableBiomes(() -> data)) {
addObjects(catalog.objects, biome.getSurfaceObjects());
}
for (IrisRegion region : upper.getAllRegions(() -> data)) {
addObjects(catalog.objects, region.getSurfaceObjects());
}
}
} finally {
data.close();
}
for (String key : semantics.objectKeys()) {
catalog.objects.add(normalizeObjectKey(key));
}
for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) {
catalog.structures.add(normalizeKey(occurrence.key()));
}
}
private static void addStructures(Set<String> structures, Iterable<IrisStructurePlacement> placements) {
for (IrisStructurePlacement placement : placements) {
if (placement == null) {
continue;
}
for (String key : placement.getStructures()) {
String normalized = normalizeKey(key);
if (!normalized.isEmpty()) {
structures.add(normalized);
}
}
}
}
private static void addObjects(Set<String> objects, Iterable<IrisObjectPlacement> placements) {
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
for (String key : placement.getPlace()) {
String normalized = normalizeObjectKey(key);
if (!normalized.isEmpty()) {
objects.add(normalized);
}
}
private static void addBiome(RetainedCatalog catalog, String key) {
String normalized = normalizeKey(key);
if (catalog.biomes.containsKey(normalized)) {
return;
}
IrisBiome biome = new IrisBiome().setName(key);
biome.setLoadKey(key);
catalog.biomes.put(normalized, biome);
}
private static <T extends IrisRegistrant> KList<T> merge(Iterable<T> active, Map<String, T> historical) {
@@ -184,16 +138,33 @@ public final class GenerationFindCatalog {
private static <T extends IrisRegistrant> T find(Iterable<T> entries, String key) {
String normalized = normalizeKey(key);
T caseInsensitiveMatch = null;
for (T entry : entries) {
if (normalizeKey(entry.getLoadKey()).equals(normalized)) {
String entryKey = normalizeKey(entry.getLoadKey());
if (entryKey.equals(normalized)) {
return entry;
}
if (caseInsensitiveMatch == null && entryKey.equalsIgnoreCase(normalized)) {
caseInsensitiveMatch = entry;
}
}
return null;
return caseInsensitiveMatch;
}
private static boolean containsKey(Set<String> keys, String key) {
if (keys.contains(key)) {
return true;
}
for (String candidate : keys) {
if (candidate.equalsIgnoreCase(key)) {
return true;
}
}
return false;
}
private static String normalizeKey(String key) {
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
return key == null ? "" : key.trim();
}
private static String normalizeObjectKey(String key) {
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public final class GenerationHistory {
@@ -380,9 +381,44 @@ public final class GenerationHistory {
}
}
LiveCutover beginLiveCutover() {
return new LiveCutover(admission.beginCutover());
}
final class LiveCutover implements AutoCloseable {
private final GenerationAdmission.CutoverLease lease;
private final Thread owner = Thread.currentThread();
private boolean closed;
private LiveCutover(GenerationAdmission.CutoverLease lease) {
this.lease = lease;
}
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
requireOpen();
synchronized (GenerationHistory.this) {
return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture"));
}
}
private void requireOpen() {
if (closed || Thread.currentThread() != owner) {
throw new IllegalStateException("Generation cutover is closed or belongs to another thread.");
}
}
@Override
public void close() {
requireOpen();
closed = true;
lease.close();
}
}
private GenerationActivation promotePendingLocked(
BoundarySignatureCapture signatureCapture
) throws IOException {
WorldChunkInventory inventory = recoverUnstoredClaims();
validateReferencedState();
Optional<GenerationActivation> pending = store.pendingActivation();
if (pending.isEmpty()) {
@@ -390,16 +426,9 @@ public final class GenerationHistory {
}
long outgoingActivationId = store.activeActivation().activationId();
semantics.compactJournals();
WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot());
ownership.assignUnassigned(inventory, outgoingActivationId);
semantics.forEachSealedClaim(
outgoingActivationId,
(chunkX, chunkZ) -> ownership.assign(chunkX, chunkZ, outgoingActivationId)
);
ownership.persist();
requireExplicitOwnership(inventory);
requireExplicitSemanticOwnership(outgoingActivationId);
GenerationBoundary boundary = boundaries.publishOwnership(
pending.get().activationId(),
ownership
@@ -440,6 +469,13 @@ public final class GenerationHistory {
return kernels.current();
}
public synchronized boolean usesCurrentGenerator() throws IOException {
GenerationEpoch active = store.activeEpoch();
return active.kernelVersion().equals(kernels.current())
&& active.kernelImplementationFingerprint().equals(
kernels.requireSupported(kernels.current()).implementationFingerprint());
}
public synchronized GenerationActivation stageCurrentKernel(
int transitionWidthBlocks
) throws IOException {
@@ -455,6 +491,49 @@ public final class GenerationHistory {
);
}
public void prepareCurrentGenerator(int transitionWidthBlocks) throws IOException {
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
synchronized (this) {
if (store.pendingActivation().isEmpty() && usesCurrentGenerator()) {
recoverUnstoredClaims();
validateReferencedState();
} else {
if (store.pendingActivation().isEmpty()) {
stageCurrentKernel(transitionWidthBlocks);
}
promoteSavedBoundary();
if (!usesCurrentGenerator()) {
stageCurrentKernel(transitionWidthBlocks);
promoteSavedBoundary();
}
}
packs.releaseArchivedPacks(store.manifest());
}
}
}
private void promoteSavedBoundary() throws IOException {
GenerationEpoch.DimensionContract contract = activeEpoch().dimensionContract();
promotePendingLocked(boundary -> new DiskBoundaryCapture(paths.dimensionRoot(), contract.minHeight(), contract.height()));
}
private WorldChunkInventory recoverUnstoredClaims() throws IOException {
long outgoing = store.activeActivation().activationId();
Set<Long> selected = store.pendingActivation()
.map(pending -> Set.of(outgoing, pending.activationId()))
.orElseGet(() -> Set.of(outgoing));
WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot()).filter((chunkX, chunkZ) -> {
if (ownership.isExplicitlyAssigned(chunkX, chunkZ)
&& !selected.contains(ownership.resolve(chunkX, chunkZ, outgoing))) {
return true;
}
return SavedTerrainChunk.hasTerrain(SavedTerrainChunk.readStatus(paths.dimensionRoot(), chunkX, chunkZ));
});
semantics.discardUnstoredClaims(inventory, selected);
ownership.discardUnstoredClaims(inventory, selected);
return inventory;
}
public synchronized Optional<GenerationActivation> pendingActivation() {
return store.pendingActivation();
}
@@ -482,7 +561,7 @@ public final class GenerationHistory {
GenerationAdmission.StageLease lease = admission.enterStage();
try {
synchronized (this) {
GenerationActivation activation = resolveActivation(chunkX, chunkZ);
GenerationActivation activation = store.activeActivation();
GenerationEpoch epoch = requireEpoch(activation.epochId());
return new GenerationStage(
this,
@@ -526,6 +605,10 @@ public final class GenerationHistory {
return semantics.claimAndPersist(requiredUpdate);
}
public synchronized void forEachRecordedSemantic(GenerationSemanticIndex.RecordConsumer consumer) throws IOException {
semantics.forEachRecord(consumer);
}
public synchronized Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
return semantics.get(chunkX, chunkZ);
}
@@ -652,13 +735,11 @@ public final class GenerationHistory {
private void validateReferencedState() throws IOException {
validateRuntimeVersions();
Collection<GenerationEpoch> epochs = store.manifest().epochs();
for (GenerationEpoch epoch : epochs) {
packs.requireExactPack(
epoch.epochId(),
epoch.packFingerprint(),
epoch.packFingerprintVersion()
);
GenerationEpoch active = store.activeEpoch();
packs.requireExactPack(active.epochId(), active.packFingerprint(), active.packFingerprintVersion());
if (store.pendingEpoch().isPresent()) {
GenerationEpoch pending = store.pendingEpoch().orElseThrow();
packs.requireExactPack(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion());
}
for (GenerationActivation activation : store.manifest().activations()) {
requireSafeDirectoryIfPresent(paths.activationRoot(activation.activationId()));
@@ -698,14 +779,6 @@ public final class GenerationHistory {
private void validateRuntimeVersions() throws IOException {
for (GenerationEpoch epoch : store.manifest().epochs()) {
kernels.requireSupported(
new GenerationKernelRegistry.Version(
epoch.generatorAbi(),
epoch.rngVersion(),
epoch.seedDerivationVersion()
),
epoch.kernelImplementationFingerprint()
);
GenerationPackFingerprint.requireSupported(epoch.packFingerprintVersion());
}
}
@@ -716,7 +789,6 @@ public final class GenerationHistory {
GenerationBoundary expectedBoundary = active.isInitial() ? null : boundary(activeActivationId);
boolean pendingCutover = store.pendingActivation().isPresent();
int[] assignedCount = new int[1];
WorldChunkInventory[] inventory = new WorldChunkInventory[1];
ownership.forEachAssignment((chunkX, chunkZ, activationId) -> {
assignedCount[0] = Math.addExact(assignedCount[0], 1);
if (store.activation(activationId).isEmpty()) {
@@ -739,14 +811,6 @@ public final class GenerationHistory {
+ chunkX + "," + chunkZ + ": expected activation "
+ activeActivationId + " but found " + activationId + ".");
}
if (inventory[0] == null) {
inventory[0] = WorldChunkInventory.scan(paths.dimensionRoot());
}
if (!inventory[0].contains(chunkX, chunkZ)
&& !semantics.hasSealedClaim(chunkX, chunkZ, activationId)) {
throw new IOException("Pending generation cutover claims an ungenerated chunk "
+ chunkX + "," + chunkZ + " without matching sealed semantics.");
}
});
int expectedCount = expectedBoundary == null ? 0 : expectedBoundary.historicalChunkCount();
@@ -803,24 +867,6 @@ public final class GenerationHistory {
+ " was not durably assigned before generation activation promotion.");
}
private void requireExplicitSemanticOwnership(long activationId) throws IOException {
boolean[] missing = new boolean[1];
long[] firstMissing = new long[1];
semantics.forEachSealedClaim(activationId, (chunkX, chunkZ) -> {
if (!missing[0] && !ownership.isExplicitlyAssigned(chunkX, chunkZ)) {
missing[0] = true;
firstMissing[0] = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
}
});
if (!missing[0]) {
return;
}
long first = firstMissing[0];
throw new IOException("Generated chunk " + ChunkGenerationOwnership.chunkX(first)
+ "," + ChunkGenerationOwnership.chunkZ(first)
+ " was not durably assigned before generation activation promotion.");
}
private GenerationActivation requireActivation(long activationId) {
return store.activation(activationId).orElseThrow(() -> new IllegalStateException(
"Generation ownership references missing activation " + activationId + "."
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.object.IrisDimension;
import java.io.IOException;
@@ -19,7 +20,6 @@ import java.util.concurrent.locks.ReentrantLock;
public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
public static final int DEFAULT_TRANSITION_WIDTH_BLOCKS = 256;
public static final int DEFAULT_RUNTIME_CACHE_CAPACITY = 4;
private final IrisEngine engine;
private final GenerationHistory history;
@@ -27,7 +27,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
private final Map<Long, RuntimeRetirement> retiringBindings;
private final int runtimeCacheCapacity;
private final ReentrantLock stateLock;
private final Condition inactive;
private final ThreadLocal<Integer> operationDepth;
@@ -43,18 +42,13 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
ActivationRuntimeFactory runtimeFactory
) throws IOException {
this.engine = Objects.requireNonNull(engine, "Iris engine");
this.history = Objects.requireNonNull(history, "generation history");
this.signatureSampler = Objects.requireNonNull(signatureSampler, "boundary signature sampler");
this.runtimeFactory = Objects.requireNonNull(runtimeFactory, "activation runtime factory");
if (runtimeCacheCapacity < 1) {
throw new IllegalArgumentException("Generation runtime cache capacity must be positive.");
}
this.runtimeCacheCapacity = runtimeCacheCapacity;
this.bindings = new LinkedHashMap<>(runtimeCacheCapacity, 0.75F, true);
this.bindings = new LinkedHashMap<>();
this.retiringBindings = new LinkedHashMap<>();
this.stateLock = new ReentrantLock(true);
this.inactive = stateLock.newCondition();
@@ -65,7 +59,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationEpoch epoch = requireEpoch(active);
IrisEngine.GenerationRuntimeBinding base = engine.getActiveGenerationRuntimeBinding();
runtimeFactory.validateBase(engine, history, active, epoch, base);
requireBindingContract(active, epoch, base);
if (history.usesCurrentGenerator()) {
requireBindingContract(active, epoch, base);
}
RuntimeCacheEntry baseEntry = new RuntimeCacheEntry(active.activationId());
baseEntry.binding = base;
baseEntry.defaultRuntime = true;
@@ -117,8 +113,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
history,
signatureSampler,
transitionWidthBlocks,
new DefaultActivationRuntimeFactory(),
DEFAULT_RUNTIME_CACHE_CAPACITY
new DefaultActivationRuntimeFactory()
);
}
@@ -128,31 +123,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationBoundarySignatureSampler signatureSampler,
int transitionWidthBlocks,
ActivationRuntimeFactory runtimeFactory
) throws IOException {
return attachAndPromotePending(
engine,
history,
signatureSampler,
transitionWidthBlocks,
runtimeFactory,
DEFAULT_RUNTIME_CACHE_CAPACITY
);
}
static GenerationHistoryRuntimeRouter attachAndPromotePending(
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
int transitionWidthBlocks,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
) throws IOException {
GenerationHistoryRuntimeRouter router = attach(
engine,
history,
signatureSampler,
runtimeFactory,
runtimeCacheCapacity
runtimeFactory
);
try {
router.promotePendingAndReconcileCurrentKernel(transitionWidthBlocks);
@@ -174,29 +150,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory
) throws IOException {
return attach(
engine,
history,
signatureSampler,
runtimeFactory,
DEFAULT_RUNTIME_CACHE_CAPACITY
);
}
static GenerationHistoryRuntimeRouter attach(
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
) throws IOException {
return new GenerationHistoryRuntimeRouter(
engine,
history,
signatureSampler,
runtimeFactory,
runtimeCacheCapacity
runtimeFactory
);
}
@@ -208,24 +167,94 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return history;
}
public StudioCutover beginStudioCutover(long timeoutMillis) {
if (!engine.isStudio()) {
throw new IllegalStateException("Live generation cutovers require a Studio world.");
}
if (scopedRoute.get() != null || operationDepth.get() > 0) {
throw new IllegalStateException("Cannot begin a Studio cutover inside another generation operation.");
}
enterOperation();
GenerationTransitionGate.Transition transition = null;
try {
transition = engine.getGenerationSessions().transitionGate().beginTransition(timeoutMillis);
return new StudioCutover(history.beginLiveCutover(), transition);
} catch (Throwable failure) {
if (transition != null) {
transition.close();
}
leaveOperation();
throw failure;
}
}
public final class StudioCutover implements AutoCloseable {
private final GenerationHistory.LiveCutover cutover;
private final GenerationTransitionGate.Transition transition;
private final Thread owner = Thread.currentThread();
private boolean closed;
private StudioCutover(GenerationHistory.LiveCutover cutover, GenerationTransitionGate.Transition transition) {
this.cutover = cutover;
this.transition = transition;
}
public GenerationActivation promotePending() throws IOException {
requireOpen();
GenerationActivation activated = cutover.promote(GenerationHistoryRuntimeRouter.this::captureBoundarySignatures);
try (RuntimeLease lease = acquireRuntime(activated, requireEpoch(activated))) {
engine.setDefaultGenerationRuntime(lease.binding());
retireBindings(pinDefault(lease.entry));
}
return activated;
}
private void requireOpen() {
if (closed || Thread.currentThread() != owner) {
throw new IllegalStateException("Studio cutover is closed or belongs to another thread.");
}
}
@Override
public void close() {
requireOpen();
closed = true;
try {
cutover.close();
} finally {
try {
transition.close();
} finally {
leaveOperation();
}
}
}
}
public Optional<RuntimeOwnership> currentRuntimeOwnership() {
RuntimeRoute current = scopedRoute.get();
return current == null ? Optional.empty() : Optional.of(current.ownership);
}
public RuntimeRoute openRoute(int chunkX, int chunkZ) throws IOException {
enterRouteOperation();
GenerationTransitionGate.Participation participation = engine.getGenerationSessions().transitionGate().enter();
boolean operation = false;
try {
enterRouteOperation();
operation = true;
GenerationHistory.GenerationStage stage = history.openStage(chunkX, chunkZ);
try {
RuntimeLease lease = acquireRuntime(stage.activation(), stage.epoch());
return new RuntimeRoute(this, stage, lease);
return new RuntimeRoute(this, stage, lease, participation);
} catch (Throwable failure) {
stage.close();
throw failure;
}
} catch (Throwable failure) {
leaveRouteOperation();
if (operation) {
leaveRouteOperation();
}
participation.close();
throw propagate(failure, "Unable to open an Iris generation-history stage.");
}
}
@@ -259,6 +288,15 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return new CoordinateScope(blockX, blockZ, stage.route, stage);
}
public void recordNaturalTerrain(SavedTerrainChunk terrain) {
SavedTerrainChunk captured = Objects.requireNonNull(terrain, "natural terrain");
RuntimeRoute route = scopedRoute.get();
if (route == null || route.chunkX() != captured.chunkX() || route.chunkZ() != captured.chunkZ()) {
throw new IllegalStateException("Natural terrain must be captured inside its chunk generation route.");
}
route.naturalTerrain = captured;
}
public void preloadActiveRuntimes() throws IOException {
enterOperation();
try {
@@ -274,6 +312,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
enterOperation();
try {
GenerationActivation activated = history.promotePending(this::captureBoundarySignatures);
if (!history.usesCurrentGenerator()) {
return activated;
}
GenerationEpoch epoch = requireEpoch(activated);
try (RuntimeLease lease = acquireRuntime(activated, epoch)) {
engine.setDefaultGenerationRuntime(lease.binding());
@@ -288,8 +329,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private GenerationActivation promotePendingAndReconcileCurrentKernel(
int transitionWidthBlocks
) throws IOException {
if (history.pendingActivation().isEmpty() && !history.usesCurrentGenerator()) {
history.stageCurrentKernel(transitionWidthBlocks);
}
GenerationActivation activated = promotePending();
while (!history.activeEpoch().kernelVersion().equals(history.currentKernelVersion())) {
if (!history.usesCurrentGenerator()) {
history.stageCurrentKernel(transitionWidthBlocks);
activated = promotePending();
}
@@ -364,8 +408,8 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private TerrainBoundarySignatureStore.SignatureSampler captureBoundarySignatures(
GenerationBoundary boundary
) {
return new BoundaryCapture();
) throws IOException {
return signatureSampler.open(engine);
}
private RuntimeLease acquireRuntime(
@@ -522,7 +566,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private List<RuntimeRetirement> collectEvictionsLocked() {
ArrayList<RuntimeRetirement> retired = new ArrayList<>();
while (bindings.size() > runtimeCacheCapacity) {
while (bindings.size() > 1) {
RuntimeCacheEntry candidate = null;
Iterator<Map.Entry<Long, RuntimeCacheEntry>> iterator = bindings.entrySet().iterator();
while (iterator.hasNext()) {
@@ -576,6 +620,18 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
if (failure != null) {
throw new IllegalStateException("Failed to retire cached generation runtimes.", failure);
}
if (!retired.isEmpty()) {
stateLock.lock();
try {
if (bindings.size() == 1 && retiringBindings.isEmpty()) {
new GenerationPackRepository(history.paths().dimensionRoot()).releaseArchivedPacks(history.manifest());
}
} catch (IOException cleanupFailure) {
throw new IllegalStateException("Unable to release archived generation packs after runtime retirement.", cleanupFailure);
} finally {
stateLock.unlock();
}
}
}
private void requireBindingContract(
@@ -807,49 +863,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
private final class BoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private RuntimeLease lease;
private IrisEngine.GenerationRuntimeScope scope;
private long activationId;
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
GenerationActivation activation = history.resolveActivation(
Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE),
Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE)
);
if (lease == null || activationId != activation.activationId()) {
close();
lease = acquireRuntime(activation, requireEpoch(activation));
try {
scope = engine.openGenerationRuntimeScope(lease.binding());
activationId = activation.activationId();
} catch (Throwable failure) {
close();
throw propagate(failure, "Unable to scope boundary capture.");
}
}
return signatureSampler.sample(engine, blockX, blockZ);
}
@Override
public void close() {
try {
if (scope != null) {
IrisEngine.GenerationRuntimeScope closing = scope;
scope = null;
closing.close();
}
} finally {
if (lease != null) {
RuntimeLease closing = lease;
lease = null;
closing.close();
}
}
}
}
interface ActivationRuntimeFactory {
void validateBase(
IrisEngine engine,
@@ -960,17 +973,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final RuntimeLease lease;
private final IrisEngine.GenerationRuntimeBinding binding;
private final RuntimeOwnership ownership;
private final GenerationTransitionGate.Participation participation;
private int activeScopes;
private volatile SavedTerrainChunk naturalTerrain;
private boolean closed;
private RuntimeRoute(
GenerationHistoryRuntimeRouter router,
GenerationHistory.GenerationStage stage,
RuntimeLease lease
RuntimeLease lease,
GenerationTransitionGate.Participation participation
) {
this.router = router;
this.stage = stage;
this.lease = lease;
this.participation = participation;
this.binding = lease.binding();
this.ownership = new RuntimeOwnership(stage.activation().activationId(), binding);
}
@@ -979,6 +996,10 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return stage;
}
public Optional<SavedTerrainChunk> naturalTerrain() {
return Optional.ofNullable(naturalTerrain);
}
public int chunkX() {
return stage.chunkX();
}
@@ -999,15 +1020,29 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return binding.transitionPlan();
}
public void detachThread() {
participation.detachThread();
}
public boolean claimGeneratedSemantics() throws IOException {
return claimGeneratedSemantics((x, y, z) -> true);
}
public boolean claimGeneratedSemantics(GenerationSemanticCapture.CaveSpace caveSpace) throws IOException {
if (router.scopedRoute.get() != this) {
throw new IllegalStateException(
"Generation semantics must be captured inside this route's runtime scope."
);
}
Optional<ChunkGenerationSemantics> recorded = router.history.semantics(chunkX(), chunkZ());
if (recorded.isPresent() && recorded.get().sealed()
|| router.history.resolveActivation(chunkX(), chunkZ()).activationId() != stage.activation().activationId()) {
return false;
}
ChunkGenerationSemantics semantics = GenerationSemanticCapture.capture(
router.engine,
stage
stage,
caveSpace
);
return router.history.claimGeneratedSemantics(stage, semantics);
}
@@ -1021,10 +1056,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
router.enterRuntimeScope();
RuntimeRoute previousRoute = router.installScopedRoute(this);
GenerationTransitionGate.Participation scopedParticipation = participation.attach();
try {
IrisEngine.GenerationRuntimeScope opened = router.engine.openGenerationRuntimeScope(binding);
return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute);
return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute, scopedParticipation);
} catch (Throwable failure) {
scopedParticipation.close();
try {
router.restoreScopedRoute(this, previousRoute);
} catch (Throwable closeFailure) {
@@ -1077,6 +1114,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
failure.addSuppressed(closeFailure);
}
}
participation.close();
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
@@ -1100,18 +1138,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final IrisEngine.GenerationRuntimeScope runtimeScope;
private final Thread owner;
private final RuntimeRoute previousRoute;
private final GenerationTransitionGate.Participation participation;
private boolean closed;
private RuntimeScope(
RuntimeRoute route,
IrisEngine.GenerationRuntimeScope runtimeScope,
Thread owner,
RuntimeRoute previousRoute
RuntimeRoute previousRoute,
GenerationTransitionGate.Participation participation
) {
this.route = route;
this.runtimeScope = runtimeScope;
this.owner = owner;
this.previousRoute = previousRoute;
this.participation = participation;
}
@Override
@@ -1148,6 +1189,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
participation.close();
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.history;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.Strictness;
@@ -27,6 +29,8 @@ import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Clock;
import java.util.Objects;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class GenerationHistoryStore {
@@ -97,7 +101,7 @@ public final class GenerationHistoryStore {
requiredClock.millis()
);
DirectorySync requiredDirectorySync = Objects.requireNonNull(directorySync, "directorySync");
writeManifestAtomically(directory, manifestPath, manifest, false, requiredDirectorySync);
writeManifestAtomically(directory, manifestPath, manifest, null, false, requiredDirectorySync);
return new GenerationHistoryStore(directory, manifest, requiredClock, requiredDirectorySync);
}
}
@@ -207,7 +211,7 @@ public final class GenerationHistoryStore {
private void writeUpdatedManifest(GenerationManifest updated) throws IOException {
try {
writeManifestAtomically(generationDirectory, manifestPath, updated, true, directorySync);
writeManifestAtomically(generationDirectory, manifestPath, updated, manifest, true, directorySync);
} catch (IOException error) {
reconcileAfterWriteFailure(error);
throw error;
@@ -233,7 +237,16 @@ public final class GenerationHistoryStore {
if (reader.peek() != JsonToken.END_DOCUMENT) {
throw new IllegalArgumentException("Generation manifest contains trailing content.");
}
return GenerationManifest.fromJson(parsed.getAsJsonObject());
JsonObject index = parsed.getAsJsonObject();
JsonArray references = GenerationManifest.JsonSchema.requireArray(index, "epochs", "generation manifest");
List<GenerationEpoch> epochs = new ArrayList<>(references.size());
for (JsonElement reference : references) {
if (!reference.isJsonPrimitive() || !reference.getAsJsonPrimitive().isString()) {
throw new IOException("Generation epoch reference must be a digest.");
}
epochs.add(readEpoch(manifestPath.getParent(), reference.getAsString()));
}
return GenerationManifest.fromJson(index, epochs);
} catch (JsonParseException | IllegalArgumentException | ArithmeticException exception) {
throw new IOException("Invalid generation manifest: " + manifestPath, exception);
}
@@ -243,12 +256,21 @@ public final class GenerationHistoryStore {
Path generationDirectory,
Path manifestPath,
GenerationManifest manifest,
GenerationManifest previous,
boolean replaceExisting,
DirectorySync directorySync
) throws IOException {
requireSafeParent(generationDirectory);
requireDirectory(generationDirectory);
byte[] content = GSON.toJson(manifest.toJson()).getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_MANIFEST_BYTES) {
throw new IOException("Generation manifest exceeds the supported size: " + manifestPath);
}
for (GenerationEpoch epoch : manifest.epochs()) {
if (previous == null || previous.epoch(epoch.epochId()).isEmpty()) {
publishEpoch(generationDirectory, epoch);
}
}
Path temporary = Files.createTempFile(generationDirectory, ".manifest-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(
@@ -282,6 +304,74 @@ public final class GenerationHistoryStore {
}
}
private static GenerationEpoch readEpoch(Path generationDirectory, String epochId) throws IOException {
Path epochDirectory = epochDirectory(generationDirectory, epochId);
requireDirectory(generationDirectory.resolve("epochs"));
requireDirectory(epochDirectory);
Path metadata = epochDirectory.resolve("epoch.json");
requireRegularFile(metadata);
if (Files.size(metadata) > MAX_MANIFEST_BYTES) {
throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata);
}
try (JsonReader reader = new JsonReader(Files.newBufferedReader(metadata, StandardCharsets.UTF_8))) {
reader.setStrictness(Strictness.STRICT);
JsonElement parsed = JsonParser.parseReader(reader);
if (!parsed.isJsonObject() || reader.peek() != JsonToken.END_DOCUMENT) {
throw new IOException("Invalid generation epoch metadata: " + metadata);
}
GenerationEpoch epoch = GenerationEpoch.fromJson(parsed.getAsJsonObject());
if (!epoch.epochId().equals(epochId)) {
throw new IOException("Generation epoch metadata identity differs from its reference: " + metadata);
}
return epoch;
} catch (JsonParseException | IllegalArgumentException exception) {
throw new IOException("Invalid generation epoch metadata: " + metadata, exception);
}
}
private static void publishEpoch(Path generationDirectory, GenerationEpoch epoch) throws IOException {
Path epochsDirectory = generationDirectory.resolve("epochs");
ensureDirectory(epochsDirectory);
Path epochDirectory = epochDirectory(generationDirectory, epoch.epochId());
ensureDirectory(epochDirectory);
Path metadata = epochDirectory.resolve("epoch.json");
if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) {
if (!readEpoch(generationDirectory, epoch.epochId()).equals(epoch)) {
throw new IOException("Generation epoch metadata is immutable: " + metadata);
}
return;
}
byte[] content = GSON.toJson(epoch.toJson()).getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_MANIFEST_BYTES) {
throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata);
}
Path temporary = Files.createTempFile(epochDirectory, ".epoch-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(temporary, metadata, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
throw new IOException("Generation history requires atomic epoch publication.", exception);
}
forceDirectory(epochDirectory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static Path epochDirectory(Path generationDirectory, String epochId) throws IOException {
if (!epochId.matches("[0-9a-f]{64}")) {
throw new IOException("Generation epoch reference must be a digest.");
}
return generationDirectory.resolve("epochs").resolve(epochId);
}
private static Path normalizeDirectory(Path directory) {
return Objects.requireNonNull(directory, "generationDirectory").toAbsolutePath().normalize();
}
@@ -23,19 +23,19 @@ final class GenerationKernelCatalog {
Catalog catalog = parse(input.readNBytes(MAXIMUM_BYTES + 1));
List<GenerationKernelRegistry.Kernel> kernels = new ArrayList<>();
for (int abi : catalog.abis()) {
GenerationKernelSourceSeal.Descriptor seal = GenerationKernelSourceSeal.load(abi);
GenerationBuildRevision.Descriptor revision = GenerationBuildRevision.load(abi);
Class<? extends GenerationKernelRegistry.RuntimeFactory> factoryType = Class.forName(
seal.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader())
revision.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader())
.asSubclass(GenerationKernelRegistry.RuntimeFactory.class);
Map<GenerationKernelRegistry.AlgorithmVersion, GenerationKernelRegistry.RuntimeFactory> factories = new LinkedHashMap<>();
for (GenerationKernelRegistry.AlgorithmVersion algorithm : seal.algorithms()) {
for (GenerationKernelRegistry.AlgorithmVersion algorithm : revision.algorithms()) {
factories.put(algorithm, factoryType.getDeclaredConstructor().newInstance());
}
kernels.add(new GenerationKernelRegistry.Kernel(abi, seal.fingerprint(), factories));
kernels.add(new GenerationKernelRegistry.Kernel(abi, revision.fingerprint(), factories));
}
return new GenerationKernelRegistry(catalog.current(), kernels);
} catch (IOException | ReflectiveOperationException | IllegalArgumentException failure) {
throw new IllegalStateException("Cannot load the sealed Iris generation kernel catalog.", failure);
throw new IllegalStateException("Cannot load the Iris generation kernel catalog.", failure);
}
}
@@ -20,7 +20,7 @@ import java.nio.file.Path;
final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactory {
static final String IMPLEMENTATION_FINGERPRINT =
GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class);
GenerationBuildRevision.requireFingerprint(1, GenerationKernelV1.class);
@Override
public SeedManager createSeedManager(long worldSeed) {
@@ -125,7 +125,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor
return;
}
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) ->
mode.registerTerrainStage((x, z, blocks, biomes, multicore, chunkContext) ->
dimensionStack.actuate(x, z, blocks, multicore, chunkContext));
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) ->
@@ -20,7 +20,7 @@ import java.util.Set;
import java.util.TreeMap;
public final class GenerationManifest {
public static final int CURRENT_SCHEMA_VERSION = 1;
public static final int CURRENT_SCHEMA_VERSION = 2;
private final int schemaVersion;
private final NavigableMap<String, GenerationEpoch> epochs;
@@ -189,7 +189,7 @@ public final class GenerationManifest {
JsonArray epochArray = new JsonArray(epochs.size());
for (GenerationEpoch epoch : epochs.values()) {
epochArray.add(epoch.toJson());
epochArray.add(epoch.epochId());
}
json.add("epochs", epochArray);
@@ -207,7 +207,7 @@ public final class GenerationManifest {
return json;
}
static GenerationManifest fromJson(JsonObject json) {
static GenerationManifest fromJson(JsonObject json, List<GenerationEpoch> epochRecords) {
JsonSchema.requireFields(
json,
"generation manifest",
@@ -231,10 +231,17 @@ public final class GenerationManifest {
"generation manifest"
);
if (epochArray.size() != epochRecords.size()) {
throw new IllegalArgumentException("Generation manifest epoch references do not match metadata.");
}
TreeMap<String, GenerationEpoch> epochs = new TreeMap<>();
for (int index = 0; index < epochArray.size(); index++) {
JsonObject epochJson = JsonSchema.requireObject(epochArray.get(index), "epochs[" + index + "]");
GenerationEpoch epoch = GenerationEpoch.fromJson(epochJson);
GenerationEpoch epoch = epochRecords.get(index);
if (!epochArray.get(index).isJsonPrimitive()
|| !epochArray.get(index).getAsJsonPrimitive().isString()
|| !epoch.epochId().equals(epochArray.get(index).getAsString())) {
throw new IllegalArgumentException("Generation manifest epoch reference does not match metadata.");
}
if (epochs.putIfAbsent(epoch.epochId(), epoch) != null) {
throw new IllegalArgumentException("Generation manifest contains duplicate epoch IDs.");
}
@@ -70,35 +70,55 @@ public final class GenerationPackRepository {
int packFingerprintVersion,
Path source
) throws IOException {
Path target = epochsRoot.resolve(epochId);
Path epochDirectory = ensureChildDirectory(epochsRoot, epochId);
Path target = epochDirectory.resolve("pack");
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
requireSafeDirectory(target, "Generation epoch path is not a safe directory");
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
}
Path stage = Files.createTempDirectory(epochsRoot, ".epoch-" + epochId.substring(0, 12) + "-");
boolean published = false;
Path stage = Files.createTempDirectory(epochDirectory, ".pack-");
try {
Path stagedPack = stage.resolve("pack");
copyPackTree(source, stagedPack);
requireFingerprint(stagedPack, packFingerprint, packFingerprintVersion);
Files.delete(stage);
copyPackTree(source, stage);
requireFingerprint(stage, packFingerprint, packFingerprintVersion);
try (AtomicDirectoryPublisher.Publication publication =
AtomicDirectoryPublisher.publishAbsent(stage, target)) {
publication.commit();
} catch (FileAlreadyExistsException race) {
requireSafeDirectory(target, "Generation epoch path is not a safe directory");
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
}
forceDirectory(epochsRoot);
published = true;
forceDirectory(epochDirectory);
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
} finally {
if (!published && Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
if (Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
AtomicDirectoryPublisher.deleteTree(stage);
}
}
}
public synchronized void releaseArchivedPacks(GenerationManifest manifest) throws IOException {
GenerationManifest retained = Objects.requireNonNull(manifest, "manifest");
String activeEpochId = retained.activeEpoch().epochId();
String pendingEpochId = retained.pendingEpoch().map(GenerationEpoch::epochId).orElse(null);
for (GenerationEpoch epoch : retained.epochs()) {
String epochId = epoch.epochId();
if (epochId.equals(activeEpochId) || epochId.equals(pendingEpochId)) {
continue;
}
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(
epochsRoot, ".epoch-" + epochId + ".lock")) {
validateExistingAncestors(epochId);
Path pack = packRoot(epochId);
if (!Files.exists(pack, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
requireSafeDirectory(pack, "Generation epoch pack is unsafe");
AtomicDirectoryPublisher.deleteTree(pack);
forceDirectory(epochRoot(epochId));
}
}
}
public Path requireExactPack(
String epochId,
String packFingerprint,
@@ -182,7 +202,7 @@ public final class GenerationPackRepository {
}
}
private static void copyPackTree(Path source, Path target) throws IOException {
public static void copyPackTree(Path source, Path target) throws IOException {
Path sourcePath = Objects.requireNonNull(source, "source").toAbsolutePath().normalize();
if (Files.isSymbolicLink(sourcePath)) {
throw new IOException("Pack source is a symbolic link: " + sourcePath);
@@ -11,6 +11,10 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.TreeSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -36,19 +40,22 @@ public final class GenerationRegistryContract {
private final String fingerprint;
private final NavigableMap<PhysicalResourceKey, String> definitions;
private final NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources;
private final NavigableMap<String, List<String>> biomeTags;
private GenerationRegistryContract(
int fingerprintSchema,
String fingerprint,
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources,
Map<String, ? extends Collection<String>> biomeTags
) {
requireSupportedFingerprintSchema(fingerprintSchema);
this.fingerprintSchema = fingerprintSchema;
this.definitions = immutableDefinitions(definitions);
this.generatedSources = immutableGeneratedSources(generatedSources, this.definitions);
this.biomeTags = canonicalBiomeTags(biomeTags, this.definitions);
this.fingerprint = requireSha256(fingerprint, "Registry contract fingerprint");
String expectedFingerprint = fingerprint(fingerprintSchema, this.definitions, this.generatedSources);
String expectedFingerprint = fingerprint(fingerprintSchema, this.definitions, this.generatedSources, this.biomeTags);
if (!this.fingerprint.equals(expectedFingerprint)) {
throw new IllegalArgumentException("Registry contract fingerprint does not match its definitions.");
}
@@ -93,12 +100,24 @@ public final class GenerationRegistryContract {
);
return new GenerationRegistryContract(
fingerprintSchema,
fingerprint(fingerprintSchema, canonicalDefinitions, canonicalSources),
fingerprint(fingerprintSchema, canonicalDefinitions, canonicalSources, new TreeMap<>()),
canonicalDefinitions,
canonicalSources
canonicalSources,
Map.of()
);
}
public GenerationRegistryContract withBiomeTags(Map<String, ? extends Collection<String>> tags) {
NavigableMap<String, List<String>> canonical = canonicalBiomeTags(tags, definitions);
return new GenerationRegistryContract(fingerprintSchema,
fingerprint(fingerprintSchema, definitions, generatedSources, canonical),
definitions, generatedSources, canonical);
}
public NavigableMap<String, List<String>> biomeTags() {
return biomeTags;
}
public int fingerprintSchema() {
return fingerprintSchema;
}
@@ -160,6 +179,13 @@ public final class GenerationRegistryContract {
generatedSourceArray.add(sourceJson);
}
json.add("generatedSources", generatedSourceArray);
JsonObject tags = new JsonObject();
for (Map.Entry<String, List<String>> entry : biomeTags.entrySet()) {
JsonArray values = new JsonArray();
entry.getValue().forEach(values::add);
tags.add(entry.getKey(), values);
}
json.add("biomeTags", tags);
return json;
}
@@ -170,7 +196,8 @@ public final class GenerationRegistryContract {
"fingerprintSchema",
"fingerprint",
"definitions",
"generatedSources"
"generatedSources",
"biomeTags"
);
int fingerprintSchema = GenerationManifest.JsonSchema.requireInt(
json,
@@ -197,12 +224,20 @@ public final class GenerationRegistryContract {
generatedSourceArray,
definitions
);
return new GenerationRegistryContract(
fingerprintSchema,
fingerprint,
definitions,
generatedSources
);
JsonObject encodedTags = GenerationManifest.JsonSchema.requireObject(json.get("biomeTags"), "registry biome tags");
Map<String, List<String>> tags = new TreeMap<>();
for (Map.Entry<String, JsonElement> entry : encodedTags.entrySet()) {
JsonArray values = GenerationManifest.JsonSchema.requireArray(encodedTags, entry.getKey(), "registry biome tags");
List<String> names = new ArrayList<>();
for (JsonElement value : values) {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
throw new IllegalArgumentException("Registry biome tags must contain resource keys.");
}
names.add(value.getAsString());
}
tags.put(entry.getKey(), names);
}
return new GenerationRegistryContract(fingerprintSchema, fingerprint, definitions, generatedSources, tags);
}
@Override
@@ -216,12 +251,13 @@ public final class GenerationRegistryContract {
return fingerprintSchema == contract.fingerprintSchema
&& fingerprint.equals(contract.fingerprint)
&& definitions.equals(contract.definitions)
&& generatedSources.equals(contract.generatedSources);
&& generatedSources.equals(contract.generatedSources)
&& biomeTags.equals(contract.biomeTags);
}
@Override
public int hashCode() {
return Objects.hash(fingerprintSchema, fingerprint, definitions, generatedSources);
return Objects.hash(fingerprintSchema, fingerprint, definitions, generatedSources, biomeTags);
}
@Override
@@ -442,13 +478,39 @@ public final class GenerationRegistryContract {
);
}
private static NavigableMap<String, List<String>> canonicalBiomeTags(
Map<String, ? extends Collection<String>> values,
Map<PhysicalResourceKey, String> definitions
) {
if (values.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry biome tags contain too many biomes.");
}
TreeMap<String, List<String>> normalized = new TreeMap<>();
for (Map.Entry<String, ? extends Collection<String>> entry : values.entrySet()) {
PhysicalResourceKey biome = new PhysicalResourceKey("minecraft:worldgen/biome", entry.getKey());
if (!definitions.containsKey(biome)) {
throw new IllegalArgumentException("Registry biome tags have no matching biome definition: " + entry.getKey());
}
TreeSet<String> tags = new TreeSet<>();
for (String tag : entry.getValue()) {
tags.add(new PhysicalResourceKey("minecraft:worldgen/biome", tag).resourceKey());
}
if (tags.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry biome contains too many tags.");
}
normalized.put(biome.resourceKey(), List.copyOf(tags));
}
return Collections.unmodifiableNavigableMap(normalized);
}
private static String fingerprint(
int fingerprintSchema,
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources,
NavigableMap<String, List<String>> biomeTags
) {
return switch (fingerprintSchema) {
case FINGERPRINT_SCHEMA_VERSION_ONE -> fingerprintVersionOne(definitions, generatedSources);
case FINGERPRINT_SCHEMA_VERSION_ONE -> fingerprintVersionOne(definitions, generatedSources, biomeTags);
default -> throw new IllegalArgumentException(
"Unsupported generation registry fingerprint schema " + fingerprintSchema + "."
);
@@ -457,7 +519,8 @@ public final class GenerationRegistryContract {
private static String fingerprintVersionOne(
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources,
NavigableMap<String, List<String>> biomeTags
) {
MessageDigest digest = sha256();
updateString(digest, FINGERPRINT_DOMAIN);
@@ -479,6 +542,14 @@ public final class GenerationRegistryContract {
updateString(digest, entry.getValue().renderedDefinitionSha256());
updateString(digest, entry.getValue().sourceJson());
}
updateInt(digest, biomeTags.size());
for (Map.Entry<String, List<String>> entry : biomeTags.entrySet()) {
updateString(digest, entry.getKey());
updateInt(digest, entry.getValue().size());
for (String tag : entry.getValue()) {
updateString(digest, tag);
}
}
return HexFormat.of().formatHex(digest.digest());
}
@@ -169,6 +169,7 @@ public final class GenerationRegistryContractFactory {
new LinkedHashMap<>();
Map<GenerationRegistryContract.PhysicalResourceKey, GenerationRegistryContract.GeneratedSource>
generatedSources = new LinkedHashMap<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
String packName = requiredData.getDataFolder().getName();
List<IrisBiome> reachableBiomes = new ArrayList<>(requiredDimension.getReachableBiomes(() -> requiredData));
for (IrisDimension registryDimension : registryDimensions(requiredData, requiredDimension)) {
@@ -196,7 +197,8 @@ public final class GenerationRegistryContractFactory {
requiredData,
requiredFixer,
requiredGenerationRegistry,
requiredAliasPolicy
requiredAliasPolicy,
biomeTags
);
}
addNativeStructures(
@@ -209,7 +211,7 @@ public final class GenerationRegistryContractFactory {
ReferencedPlatformKeys referenced = referencedPlatformKeys(packRoot, requiredRegistries);
addPlatformDefinitions(definitions, BLOCK_REGISTRY, referenced.blockKeys(), requiredGenerationRegistry);
addPlatformDefinitions(definitions, ENTITY_TYPE_REGISTRY, referenced.entityKeys(), requiredGenerationRegistry);
return contract(definitions, generatedSources, requiredGenerationRegistry);
return contract(definitions, generatedSources, requiredGenerationRegistry).withBiomeTags(biomeTags);
}
public static CustomBiomeDefinition customBiomeDefinition(
@@ -798,7 +800,8 @@ public final class GenerationRegistryContractFactory {
IrisData data,
IDataFixer fixer,
PlatformGenerationRegistry registry,
CustomBiomeAliasPolicy aliasPolicy
CustomBiomeAliasPolicy aliasPolicy,
Map<String, Set<String>> biomeTags
) throws IOException {
TreeSet<String> platformBiomeKeys = new TreeSet<>();
Map<String, String> customResourceKeys = new LinkedHashMap<>();
@@ -833,6 +836,19 @@ public final class GenerationRegistryContractFactory {
registry.generatedDefinitionRendererIdentity(),
generated.definition()
);
Set<String> tags = biomeTags.computeIfAbsent(generated.physicalKey().resourceKey(), ignored -> new TreeSet<>());
for (String rawTag : customBiome.getEffectiveTags(biome.getVanillaDerivativeKey())) {
if (rawTag == null || rawTag.isBlank()) {
continue;
}
String tag = rawTag.trim().toLowerCase(Locale.ROOT);
if (tag.indexOf(':') < 0) {
tag = "minecraft:" + tag;
}
if (tag.matches("[a-z0-9_.-]+:[a-z0-9/._-]+")) {
tags.add(tag);
}
}
String logicalKey = dimensionKey.toLowerCase(Locale.ROOT)
+ ":" + customBiomeId.toLowerCase(Locale.ROOT);
customResourceKeys.merge(
@@ -858,6 +874,7 @@ public final class GenerationRegistryContractFactory {
}
for (Map.Entry<String, CustomBiomeDefinition> entry : legacyDefinitions.entrySet()) {
CustomBiomeDefinition generated = entry.getValue();
biomeTags.put(entry.getKey(), new TreeSet<>(biomeTags.get(generated.physicalKey().resourceKey())));
putGeneratedDefinition(
definitions,
generatedSources,
@@ -15,6 +15,7 @@ import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.HashSet;
import java.util.Objects;
@@ -29,6 +30,14 @@ public final class GenerationSemanticCapture {
public static ChunkGenerationSemantics capture(
Engine engine,
GenerationHistory.GenerationStage stage
) {
return capture(engine, stage, (x, y, z) -> true);
}
public static ChunkGenerationSemantics capture(
Engine engine,
GenerationHistory.GenerationStage stage,
CaveSpace caveSpace
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
GenerationHistory.GenerationStage requiredStage = Objects.requireNonNull(stage, "stage");
@@ -36,7 +45,8 @@ public final class GenerationSemanticCapture {
requiredEngine,
requiredStage.chunkX(),
requiredStage.chunkZ(),
requiredStage.activation().activationId()
requiredStage.activation().activationId(),
caveSpace
);
}
@@ -46,6 +56,17 @@ public final class GenerationSemanticCapture {
int chunkZ,
long activationId
) {
return capture(engine, chunkX, chunkZ, activationId, (x, y, z) -> true);
}
private static ChunkGenerationSemantics capture(
Engine engine,
int chunkX,
int chunkZ,
long activationId,
CaveSpace caveSpace
) {
Objects.requireNonNull(caveSpace, "cave space");
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
ChunkGenerationSemantics.Builder semantics = ChunkGenerationSemantics.builder(
chunkX,
@@ -53,7 +74,7 @@ public final class GenerationSemanticCapture {
activationId
);
captureColumns(requiredEngine, chunkX, chunkZ, semantics);
captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics);
captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics, caveSpace);
captureStructures(requiredEngine, chunkX, chunkZ, semantics);
return semantics.seal().build();
}
@@ -110,13 +131,17 @@ public final class GenerationSemanticCapture {
Engine engine,
int chunkX,
int chunkZ,
ChunkGenerationSemantics.Builder semantics
ChunkGenerationSemantics.Builder semantics,
CaveSpace caveSpace
) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
Set<CavePosition> resolvedCaves = new HashSet<>();
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
if (!caveSpace.isOpen(localX, y, localZ)) {
return;
}
captureCave(
engine,
semantics,
@@ -128,6 +153,9 @@ public final class GenerationSemanticCapture {
);
});
mantle.iterateChunk(chunkX, chunkZ, HydrologyCaveCell.class, (localX, y, localZ, cell) -> {
if (!caveSpace.isOpen(localX, y, localZ)) {
return;
}
int blockX = minimumX + localX;
int blockZ = minimumZ + localZ;
if (!hasFullHydrologyWeight(engine.getComplex(), blockX, blockZ)) {
@@ -146,6 +174,13 @@ public final class GenerationSemanticCapture {
semantics.addRiverProfile(cell.fluidProfileKey());
}
});
mantle.iterateChunk(chunkX, chunkZ, MatterStructurePOI.class, (localX, y, localZ, point) -> {
if (point != null) {
semantics.addPointOfInterest(new ChunkGenerationSemantics.PointOfInterest(point.getType(),
new ChunkGenerationSemantics.BlockPosition(Math.addExact(minimumX, localX), y,
Math.addExact(minimumZ, localZ))));
}
});
mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> {
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded != null) {
@@ -219,6 +254,11 @@ public final class GenerationSemanticCapture {
}
}
@FunctionalInterface
public interface CaveSpace {
boolean isOpen(int localX, int internalY, int localZ);
}
private record CavePosition(int x, int y, int z) {
}
}
@@ -53,7 +53,7 @@ public final class GenerationSemanticIndex {
private static final String JOURNAL_SUFFIX = ".iswal";
private static final String CATALOG_FILE_NAME = "index.isix";
private static final int MAGIC = 0x4953454D;
private static final int FORMAT_VERSION = 5;
private static final int FORMAT_VERSION = 6;
private static final int CATALOG_MAGIC = 0x49534958;
private static final int CATALOG_FORMAT_VERSION = 1;
private static final int CATALOG_FIXED_BODY_BYTES = 12;
@@ -204,6 +204,54 @@ public final class GenerationSemanticIndex {
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
for (long activation : selected) {
if (activation <= 0L) {
throw new IllegalArgumentException("Generation activation IDs must be positive.");
}
}
lock.writeLock().lock();
try {
compactJournals();
int removed = 0;
for (long regionKey : allRegionKeys()) {
RegionSummary summary = loadSummaryLocked(regionKey);
boolean relevant = false;
for (long activation : selected) {
if (summary.activations.contains(activation)) {
relevant = true;
break;
}
}
if (!relevant) {
continue;
}
int regionX = regionX(regionKey);
int regionZ = regionZ(regionKey);
RegionShard previous = loadRegionLocked(regionKey, regionX, regionZ);
RegionShard replacement = previous.retainingStoredClaims(stored, selected);
int count = previous.recordCount() - replacement.recordCount();
if (count == 0) {
continue;
}
byte[] encoded = replacement.encode();
String hash = sha256(encoded);
publisher.publish(directory, regionX, regionZ, encoded);
pointerPublisher.publish(directory, regionX, regionZ, hash);
String previousHash = shardHashes.put(regionKey, hash);
regionRecordCounts.put(regionKey, replacement.recordCount());
cacheRegion(regionKey, replacement);
removed += count;
deleteUnreferencedShard(regionX, regionZ, previousHash, hash);
}
return removed;
} finally {
lock.writeLock().unlock();
}
}
public void compactJournals() throws IOException {
lock.writeLock().lock();
try {
@@ -2095,7 +2143,7 @@ public final class GenerationSemanticIndex {
validateRegionCoordinate(file, regionX);
validateRegionCoordinate(file, regionZ);
int recordCount = input.readInt();
if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) {
if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) {
throw invalid(file, "invalid record count " + recordCount);
}
int paletteSize = input.readInt();
@@ -2138,7 +2186,7 @@ public final class GenerationSemanticIndex {
validateRegionCoordinate(file, regionX);
validateRegionCoordinate(file, regionZ);
int recordCount = input.readInt();
if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) {
if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) {
throw invalid(file, "invalid record count " + recordCount);
}
int paletteSize = input.readInt();
@@ -2191,7 +2239,7 @@ public final class GenerationSemanticIndex {
validateRegionCoordinate(source, regionX);
validateRegionCoordinate(source, regionZ);
int recordCount = input.readInt();
if (recordCount <= 0 || recordCount > CHUNKS_PER_REGION) {
if (recordCount < 0 || recordCount > CHUNKS_PER_REGION) {
throw invalid(source, "invalid record count " + recordCount);
}
int paletteSize = input.readInt();
@@ -2237,6 +2285,7 @@ public final class GenerationSemanticIndex {
readKeys(source, input, palette, paletteUsed, builder, SemanticKind.OBJECT);
readRiverFeatures(source, input, palette, paletteUsed, builder);
readStructures(source, input, palette, paletteUsed, builder);
readPointsOfInterest(source, input, palette, paletteUsed, builder);
if ((recordFlags & 1) != 0) {
builder.seal();
}
@@ -2280,6 +2329,19 @@ public final class GenerationSemanticIndex {
return records[localIndex(chunkX, chunkZ)];
}
private RegionShard retainingStoredClaims(WorldChunkInventory stored, Set<Long> activationIds) {
RegionShard replacement = new RegionShard(regionX, regionZ);
for (int index = 0; index < records.length; index++) {
ChunkGenerationSemantics record = records[index];
if (record != null && (!activationIds.contains(record.activationId())
|| stored.contains(record.chunkX(), record.chunkZ()))) {
replacement.records[index] = record;
replacement.recordCount++;
}
}
return replacement;
}
private RegionShard withRecord(ChunkGenerationSemantics semantics) {
if ((semantics.chunkX() >> 5) != regionX || (semantics.chunkZ() >> 5) != regionZ) {
throw new IllegalArgumentException("Chunk does not belong to generation semantic shard");
@@ -2362,6 +2424,9 @@ public final class GenerationSemanticIndex {
for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) {
addPaletteKey(paletteKeys, occurrence.key());
}
for (ChunkGenerationSemantics.PointOfInterest point : semantics.pointsOfInterest()) {
addPaletteKey(paletteKeys, point.key());
}
}
List<String> palette = new ArrayList<>(paletteKeys);
Map<String, Integer> paletteIndexes = new HashMap<>(palette.size());
@@ -2418,6 +2483,13 @@ public final class GenerationSemanticIndex {
output.writeInt(occurrence.position().y());
output.writeInt(occurrence.position().z());
}
output.writeShort(semantics.pointsOfInterest().size());
for (ChunkGenerationSemantics.PointOfInterest point : semantics.pointsOfInterest()) {
output.writeShort(paletteIndexes.get(point.key()));
output.writeInt(point.position().x());
output.writeInt(point.position().y());
output.writeInt(point.position().z());
}
if (bodyBytes.size() > MAX_FILE_BYTES - CHECKSUM_BYTES) {
throw new IOException("Generation semantic shard exceeds its size limit");
}
@@ -2552,6 +2624,42 @@ public final class GenerationSemanticIndex {
}
}
private static void readPointsOfInterest(
Path file,
DataInputStream input,
String[] palette,
boolean[] paletteUsed,
ChunkGenerationSemantics.Builder builder
) throws IOException {
int count = input.readUnsignedShort();
if (count > ChunkGenerationSemantics.MAX_STRUCTURES) {
throw invalid(file, "too many points of interest");
}
ChunkGenerationSemantics.PointOfInterest previous = null;
for (int index = 0; index < count; index++) {
int paletteIndex = input.readUnsignedShort();
if (paletteIndex >= palette.length) {
throw invalid(file, "point of interest key palette index is out of range");
}
paletteUsed[paletteIndex] = true;
ChunkGenerationSemantics.PointOfInterest occurrence =
new ChunkGenerationSemantics.PointOfInterest(
palette[paletteIndex],
new ChunkGenerationSemantics.BlockPosition(
input.readInt(),
input.readInt(),
input.readInt()
)
);
if (previous != null
&& ChunkGenerationSemantics.pointComparator().compare(previous, occurrence) >= 0) {
throw invalid(file, "points of interest are not in canonical order");
}
builder.addPointOfInterest(occurrence);
previous = occurrence;
}
}
private static void readRiverFeatures(
Path file,
DataInputStream input,
@@ -9,7 +9,6 @@ import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import java.math.BigInteger;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
@@ -29,7 +28,7 @@ public final class GenerationSemanticQueries {
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
String requiredKey = ChunkGenerationSemantics.requireResourceKey(
Objects.requireNonNull(key, "key").trim().toLowerCase(Locale.ROOT));
Objects.requireNonNull(key, "key").trim());
int radius = Math.max(0, Math.min(maximumChunkRadius, 2048));
GenerationHistory history = history(requiredEngine).orElse(null);
if (history == null) {
@@ -1,170 +1,94 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.math.RNG;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class IrisBoundarySignatureSampler implements GenerationBoundarySignatureSampler {
public static final IrisBoundarySignatureSampler INSTANCE = new IrisBoundarySignatureSampler();
private static final int MAXIMUM_CACHED_CHUNKS = 32;
static final int VERTICAL_SAMPLE_STEP = 4;
private IrisBoundarySignatureSampler() {
}
public static void checkpoint(IrisEngine engine) throws IOException {
if (engine.getPlatformHooks().isMainThread()) {
throw new IOException("Terrain save checkpoints must run outside the platform tick thread.");
}
try {
engine.getPlatformHooks().flushSavedTerrainCapture(engine).get(30, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while saving the terrain boundary.", failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to save the terrain boundary.", failure);
}
}
@Override
public TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException {
IrisEngine requiredEngine = Objects.requireNonNull(engine, "Iris engine");
try {
IrisComplex complex = Objects.requireNonNull(requiredEngine.getComplex(), "Iris complex");
int height = requiredEngine.getHeight();
if (height <= 0) {
throw new IOException("Iris boundary sampling requires a positive world height.");
try (TerrainBoundarySignatureStore.SignatureSampler capture = open(engine)) {
return capture.sample(blockX, blockZ);
}
}
@Override
public TerrainBoundarySignatureStore.SignatureSampler open(IrisEngine engine) throws IOException {
if (engine.getPlatformHooks().isMainThread()) {
throw new IOException("Terrain boundary capture must run outside the platform tick thread.");
}
return new LiveCapture(engine);
}
private static final class LiveCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private final IrisEngine engine;
private final LinkedHashMap<Long, SavedTerrainChunk> chunks = new LinkedHashMap<>(32, 0.75F, true);
private LiveCapture(IrisEngine engine) {
this.engine = engine;
}
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
SavedTerrainChunk terrain = chunks.get(key);
if (terrain == null) {
terrain = captureChunk(chunkX, chunkZ);
chunks.put(key, terrain);
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
Map.Entry<Long, SavedTerrainChunk> oldest = chunks.firstEntry();
chunks.remove(oldest.getKey());
}
}
return terrain.column(blockX, blockZ);
}
int minimumY = requiredEngine.getMinHeight();
int terrainHeight = clampHeight(
(int) Math.round(complex.getHeightStream().getDouble(blockX, blockZ)),
height
);
int fluidHeight = clampHeight(
(int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(blockX, blockZ)),
height
);
OptionalInt fluid = fluidHeight > terrainHeight
? OptionalInt.of(fluidHeight)
: OptionalInt.empty();
int sampleCount = Math.ceilDiv(height, VERTICAL_SAMPLE_STEP);
TerrainBoundarySignature.VerticalLayout layout = new TerrainBoundarySignature.VerticalLayout(
minimumY,
VERTICAL_SAMPLE_STEP,
sampleCount
);
short[] biomeIndices = new short[sampleCount];
LinkedHashMap<String, Short> paletteIndices = new LinkedHashMap<>();
for (int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++) {
int worldY = layout.sampleY(sampleIndex);
int internalY = worldY - minimumY;
String biomeKey = physicalBiomeKey(
requiredEngine,
complex,
blockX,
worldY,
blockZ,
internalY,
terrainHeight
);
biomeIndices[sampleIndex] = paletteIndex(paletteIndices, biomeKey);
private SavedTerrainChunk captureChunk(int chunkX, int chunkZ) throws IOException {
try {
return engine.getPlatformHooks().captureSavedTerrainChunk(engine, chunkX, chunkZ)
.get(30, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing terrain at " + chunkX + "," + chunkZ, failure);
} catch (ExecutionException | TimeoutException failure) {
throw new IOException("Unable to capture terrain at " + chunkX + "," + chunkZ, failure);
}
int visibleSurfaceHeight = Math.max(terrainHeight, fluidHeight);
UpperDimensionContext upperContext = requiredEngine.getUpperContext();
int upperDepth = upperContext == null ? 0
: height - 1 - upperContext.getEffectiveSurfaceY(blockX, blockZ);
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(
blockX,
blockZ,
visibleSurfaceHeight,
terrainHeight,
fluid,
upperDepth > 0 ? OptionalInt.of(upperDepth) : OptionalInt.empty()
),
new TerrainBoundarySignature.Samples(
layout,
new TerrainBoundarySignature.BiomeEncoding(
List.copyOf(paletteIndices.keySet()),
biomeIndices
)
)
);
} catch (IOException exception) {
throw exception;
} catch (RuntimeException exception) {
throw new IOException("Unable to sample Iris terrain boundary at "
+ blockX + "," + blockZ + ".", exception);
}
}
private static int clampHeight(int value, int height) {
return Math.max(0, Math.min(height - 1, value));
}
private static short paletteIndex(Map<String, Short> indices, String biomeKey) {
Short existing = indices.get(biomeKey);
if (existing != null) {
return existing;
}
int next = indices.size();
if (next > Short.MAX_VALUE) {
throw new IllegalStateException("Iris boundary biome palette exceeds compact index capacity.");
}
short index = (short) next;
indices.put(biomeKey, index);
return index;
}
private static String physicalBiomeKey(
IrisEngine engine,
IrisComplex complex,
int blockX,
int worldY,
int blockZ,
int internalY,
int terrainHeight
) {
String historicalKey = complex.historicalPhysicalBiomeKeyAt(blockX, worldY, blockZ).orElse(null);
if (historicalKey != null) {
return historicalKey;
}
int caveSwitchY = Math.max(-8 - engine.getMinHeight(), 40);
boolean underground = internalY <= caveSwitchY && internalY <= terrainHeight - 8;
IrisDimension biomeDimension = engine.getDimension();
IrisBiome biome;
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
biome = underground
? engine.getCaveBiome(blockX, internalY, blockZ)
: complex.getTrueBiomeStream().get(blockX, blockZ);
} else {
DimensionStackLayout.Layer layer = stackContext.getLayout(blockX, blockZ).layerAt(internalY);
biome = layer.biome();
biomeDimension = layer.terrainContext().getDimension();
underground = false;
}
if (biome == null && underground) {
biome = complex.getTrueBiomeStream().get(blockX, blockZ);
}
if (biome == null) {
return "minecraft:plains";
}
RNG rng = new RNG(engine.getWorld().getRawWorldSeed()
^ ((long) blockX * 341873128712L)
^ ((long) worldY * 132897987541L)
^ ((long) blockZ * 42317861L));
if (biome.isCustom()) {
IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, blockX, worldY, blockZ);
return custom == null
? "minecraft:plains"
: engine.getData().customBiomeResourceKey(biomeDimension, custom);
@Override
public void close() throws IOException {
try {
checkpoint(engine);
} finally {
chunks.clear();
}
}
String key = underground
? biome.getGroundBiomeKey(rng, engine, blockX, worldY, blockZ)
: biome.getSkyBiomeKey(rng, engine, blockX, worldY, blockZ);
return key == null || key.isBlank() ? "minecraft:plains" : key;
}
}
@@ -0,0 +1,196 @@
package art.arcane.iris.engine.history;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public final class NativeTerrainReceipt {
public static final String STRUCTURE_ACTIVATION_KEY = "iris:structure_activation";
public static final String NBT_KEY = "iris:natural_terrain";
private static final int MAGIC = 0x4952544E;
private static final int VERSION = 1;
private static final int MAXIMUM_BYTES = 64 * 1024 * 1024;
private NativeTerrainReceipt() {
}
public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(new GZIPOutputStream(bytes))) {
output.writeInt(MAGIC);
output.writeInt(VERSION);
output.writeInt(chunk.chunkX());
output.writeInt(chunk.chunkZ());
output.writeLong(activationId);
output.writeUTF(epochId);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
output.writeBoolean(chunk.hasColumn(x, z));
if (!chunk.hasColumn(x, z)) {
continue;
}
writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z));
}
}
}
return bytes.toByteArray();
}
public static SavedTerrainChunk restore(byte[] bytes, String nativeStatus, int chunkX, int chunkZ,
int minimumY, int height) throws IOException {
SavedTerrainChunk terrain = decode(bytes, nativeStatus).terrain();
if (terrain.chunkX() != chunkX || terrain.chunkZ() != chunkZ) {
throw new IOException("Native terrain receipt coordinates do not match its chunk");
}
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (!terrain.hasColumn(x, z)) {
if (x == 0 || x == 15 || z == 0 || z == 15) {
throw new IOException("Native terrain receipt is missing an edge column");
}
continue;
}
BoundaryColumnGeometry geometry = terrain.column((chunkX << 4) + x, (chunkZ << 4) + z).geometry();
if (geometry.minimumY() != minimumY || geometry.height() != height) {
throw new IOException("Native terrain receipt height does not match its dimension");
}
}
}
return terrain;
}
public static Decoded decode(byte[] bytes, String nativeStatus) throws IOException {
if (bytes.length > MAXIMUM_BYTES) {
throw new IOException("Native terrain receipt exceeds its size limit");
}
byte[] decoded;
try (GZIPInputStream compressed = new GZIPInputStream(new ByteArrayInputStream(bytes))) {
decoded = compressed.readNBytes(MAXIMUM_BYTES + 1);
}
if (decoded.length > MAXIMUM_BYTES) {
throw new IOException("Native terrain receipt expands beyond its size limit");
}
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(decoded))) {
if (input.readInt() != MAGIC || input.readInt() != VERSION) {
throw new IOException("Invalid native terrain receipt format");
}
int chunkX = input.readInt();
int chunkZ = input.readInt();
long activation = input.readLong();
String epoch = input.readUTF();
if (activation < 0 || epoch.isBlank()) {
throw new IOException("Invalid native terrain receipt identity");
}
List<TerrainBoundarySignature> columns = new ArrayList<>(256);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (!input.readBoolean()) {
columns.add(null);
continue;
}
columns.add(readColumn(input, Math.addExact(Math.multiplyExact(chunkX, 16), x),
Math.addExact(Math.multiplyExact(chunkZ, 16), z)));
}
}
if (input.read() != -1) {
throw new IOException("Trailing native terrain receipt data");
}
return new Decoded(new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns), activation, epoch);
} catch (IllegalArgumentException | ArithmeticException exception) {
throw new IOException("Invalid native terrain receipt", exception);
}
}
private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature) throws IOException {
output.writeInt(signature.surfaceHeight());
output.writeInt(signature.oceanFloorHeight());
output.writeInt(signature.fluidHeight().orElse(-1));
output.writeInt(signature.upperCeilingDepth().orElse(-1));
TerrainBoundarySignature.VerticalLayout layout = signature.samples().layout();
output.writeInt(layout.minimumY());
output.writeInt(layout.sampleStep());
output.writeInt(layout.sampleCount());
for (int index = 0; index < layout.sampleCount(); index++) {
output.writeUTF(signature.biomeAtSample(index));
}
BoundaryColumnGeometry geometry = signature.geometry();
output.writeInt(geometry.minimumY());
output.writeInt(geometry.palette().size());
for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) {
output.writeUTF(voxel.stateKey());
output.writeByte(voxel.phase().ordinal());
output.writeUTF(voxel.fluidStateKey());
output.writeBoolean(voxel.protectedContent());
}
int[] ends = geometry.runEnds();
short[] indices = geometry.paletteIndices();
output.writeInt(ends.length);
for (int index = 0; index < ends.length; index++) {
output.writeInt(ends[index]);
output.writeShort(indices[index]);
}
}
private static TerrainBoundarySignature readColumn(DataInputStream input, int x, int z) throws IOException {
int surface = input.readInt();
int floor = input.readInt();
int fluid = input.readInt();
int ceiling = input.readInt();
int biomeMinimum = input.readInt();
int step = input.readInt();
int count = boundedCount(input.readInt());
List<String> biomes = new ArrayList<>();
short[] biomeIndices = new short[count];
for (int index = 0; index < count; index++) {
String biome = input.readUTF();
int paletteIndex = biomes.indexOf(biome);
if (paletteIndex < 0) {
paletteIndex = biomes.size();
biomes.add(biome);
}
biomeIndices[index] = (short) paletteIndex;
}
int minimumY = input.readInt();
int paletteCount = boundedCount(input.readInt());
List<BoundaryColumnGeometry.Voxel> palette = new ArrayList<>(paletteCount);
for (int index = 0; index < paletteCount; index++) {
String state = input.readUTF();
int phase = input.readUnsignedByte();
if (phase >= BoundaryColumnGeometry.Phase.values().length) {
throw new IOException("Invalid terrain receipt voxel phase");
}
palette.add(new BoundaryColumnGeometry.Voxel(state, BoundaryColumnGeometry.Phase.values()[phase],
input.readUTF(), input.readBoolean()));
}
int runs = boundedCount(input.readInt());
int[] ends = new int[runs];
short[] indices = new short[runs];
for (int index = 0; index < runs; index++) {
ends[index] = input.readInt();
indices[index] = input.readShort();
}
return new TerrainBoundarySignature(new TerrainBoundarySignature.Column(x, z, surface, floor,
fluid < 0 ? OptionalInt.empty() : OptionalInt.of(fluid),
ceiling < 0 ? OptionalInt.empty() : OptionalInt.of(ceiling)),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(biomeMinimum, step, count),
new TerrainBoundarySignature.BiomeEncoding(biomes, biomeIndices)),
new BoundaryColumnGeometry(minimumY, palette, ends, indices));
}
private static int boundedCount(int count) throws IOException {
if (count < 0 || count > 4096) {
throw new IOException("Invalid native terrain receipt collection size");
}
return count;
}
public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) {
}
}
@@ -20,6 +20,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.zip.CRC32;
import java.util.Set;
final class RegionGenerationOwnership {
static final int CHUNKS_PER_SIDE = 32;
@@ -202,6 +203,27 @@ final class RegionGenerationOwnership {
return true;
}
synchronized int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) {
int removed = 0;
for (int localZ = 0; localZ < CHUNKS_PER_SIDE; localZ++) {
for (int localX = 0; localX < CHUNKS_PER_SIDE; localX++) {
int index = chunkIndex(localX, localZ);
if (activations[index] == 0L || !activationIds.contains(activations[index])) {
continue;
}
int chunkX = (regionX << 5) + localX;
int chunkZ = (regionZ << 5) + localZ;
if (!stored.contains(chunkX, chunkZ)) {
activations[index] = 0L;
assignmentCount--;
removed++;
}
}
}
dirty |= removed > 0;
return removed;
}
synchronized boolean persist(Path directory) throws IOException {
if (!dirty) {
return false;
@@ -0,0 +1,208 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
public final class SavedTerrainChunk {
private final int chunkX;
private final int chunkZ;
private final String nativeStatus;
private final List<TerrainBoundarySignature> columns;
SavedTerrainChunk(int chunkX, int chunkZ, String nativeStatus,
List<TerrainBoundarySignature> columns) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.nativeStatus = Objects.requireNonNull(nativeStatus, "native status");
this.columns = Collections.unmodifiableList(new ArrayList<>(columns));
}
public static SavedTerrainChunk read(Path dimensionRoot, int chunkX, int chunkZ,
int minimumY, int height) throws IOException {
return SavedTerrainChunkReader.read(dimensionRoot, chunkX, chunkZ, minimumY, height);
}
public static SavedTerrainChunk readNbt(byte[] bytes, int chunkX, int chunkZ,
int minimumY, int height) throws IOException {
return SavedTerrainChunkReader.readNbt(bytes, chunkX, chunkZ, minimumY, height);
}
public static void verifyCheckpoint(Path dimensionRoot, int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation)
throws IOException {
SavedTerrainChunkReader.verifyCheckpoint(dimensionRoot, chunkX, chunkZ, status, receipt, structureActivation);
}
public static byte[] readReceipt(Path dimensionRoot, int chunkX, int chunkZ) throws IOException {
return SavedTerrainChunkReader.readReceipt(dimensionRoot, chunkX, chunkZ);
}
public static String readStatus(Path dimensionRoot, int chunkX, int chunkZ) throws IOException {
return SavedTerrainChunkReader.readStatus(dimensionRoot, chunkX, chunkZ);
}
public static SavedTerrainChunk capture(int chunkX, int chunkZ, int minimumY, int height,
String nativeStatus, VoxelSource source) throws IOException {
return capture(chunkX, chunkZ, minimumY, height, nativeStatus, source, false);
}
public static SavedTerrainChunk captureBoundary(int chunkX, int chunkZ, int minimumY, int height,
String nativeStatus, VoxelSource source) throws IOException {
return capture(chunkX, chunkZ, minimumY, height, nativeStatus, source, true);
}
private static SavedTerrainChunk capture(int chunkX, int chunkZ, int minimumY, int height,
String nativeStatus, VoxelSource source, boolean boundaryOnly) throws IOException {
Objects.requireNonNull(source, "voxel source");
if (height <= 0 || height > 4096 || minimumY % 16 != 0 || height % 16 != 0) {
throw new IOException("Invalid saved terrain height: " + minimumY + "/" + height);
}
Math.addExact(minimumY, height);
List<TerrainBoundarySignature> columns = new ArrayList<>(256);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) {
columns.add(null);
continue;
}
List<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(height);
Map<String, Short> biomePalette = new LinkedHashMap<>();
short[] biomeIndices = new short[height / 4];
int surface = 0;
int floor = 0;
int fluid = -1;
for (int offset = 0; offset < height; offset++) {
int y = minimumY + offset;
BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel");
voxels.add(voxel);
if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) {
surface = offset;
}
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
floor = offset;
}
if (!voxel.fluidStateKey().isEmpty()) {
fluid = offset;
}
if (offset % 4 == 0) {
String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome");
Short index = biomePalette.get(biome);
if (index == null) {
index = (short) biomePalette.size();
biomePalette.put(biome, index);
}
biomeIndices[offset / 4] = index;
}
}
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(minimumY, voxels);
OptionalInt groundSurface = source.groundSurface(x, z);
if (groundSurface.isPresent()) {
floor = geometry.surfaceOffsetNear(groundSurface.getAsInt());
if (floor < 0 || floor >= height) {
throw new IOException("Natural terrain ground surface is outside the chunk height");
}
surface = Math.max(floor, fluid);
}
columns.add(new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(Math.addExact(Math.multiplyExact(chunkX, 16), x),
Math.addExact(Math.multiplyExact(chunkZ, 16), z), surface, floor,
fluid > floor ? OptionalInt.of(fluid) : OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(minimumY, 4, biomeIndices.length),
new TerrainBoundarySignature.BiomeEncoding(List.copyOf(biomePalette.keySet()), biomeIndices)),
geometry));
}
}
return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns);
}
public SavedTerrainChunk boundaryOnly() {
if (!hasColumn(1, 1)) {
return this;
}
List<TerrainBoundarySignature> boundary = new ArrayList<>(columns);
for (int x = 1; x < 15; x++) {
for (int z = 1; z < 15; z++) {
boundary.set(x * 16 + z, null);
}
}
return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, boundary);
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
if (Math.floorDiv(blockX, 16) != chunkX || Math.floorDiv(blockZ, 16) != chunkZ) {
throw new IllegalArgumentException("Boundary column is outside the captured chunk");
}
return Objects.requireNonNull(columns.get(Math.floorMod(blockX, 16) * 16 + Math.floorMod(blockZ, 16)),
"Interior columns are not retained in a boundary receipt");
}
public boolean hasColumn(int localX, int localZ) {
return columns.get(localX * 16 + localZ) != null;
}
public int chunkX() {
return chunkX;
}
public int chunkZ() {
return chunkZ;
}
public String nativeStatus() {
return nativeStatus;
}
public static int statusRank(String status) {
if (status == null) {
return -1;
}
String key = status.startsWith("minecraft:") ? status.substring(10) : status;
return switch (key) {
case "empty" -> 0;
case "structure_starts" -> 1;
case "structure_references" -> 2;
case "biomes" -> 3;
case "noise" -> 4;
case "surface" -> 5;
case "carvers" -> 6;
case "features" -> 7;
case "initialize_light" -> 8;
case "light" -> 9;
case "spawn" -> 10;
case "full" -> 11;
default -> -1;
};
}
public static boolean hasTerrain(String status) {
return statusRank(status) >= 4;
}
public static boolean isComplete(String status) {
return "minecraft:full".equals(status) || "full".equals(status);
}
public static void requireComplete(String status, int chunkX, int chunkZ) throws IOException {
if (!isComplete(status)) {
throw new IOException("Saved chunk " + chunkX + "," + chunkZ + " has unfinished native generation status "
+ status + "; finish its generation before changing the generator.");
}
}
public interface VoxelSource {
default OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.empty();
}
BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) throws IOException;
String biome(int localX, int worldY, int localZ) throws IOException;
}
}
@@ -0,0 +1,368 @@
package art.arcane.iris.engine.history;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.ByteArrayTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import art.arcane.volmlib.util.nbt.tag.LongArrayTag;
import art.arcane.volmlib.util.nbt.tag.LongTag;
import art.arcane.volmlib.util.nbt.tag.NumberTag;
import art.arcane.volmlib.util.nbt.tag.StringTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import net.jpountz.lz4.LZ4BlockInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
final class SavedTerrainChunkReader {
private static final int SECTOR_BYTES = 4096;
private static final int MAX_CHUNK_BYTES = 64 * 1024 * 1024;
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private SavedTerrainChunkReader() {
}
static SavedTerrainChunk read(Path dimensionRoot, int chunkX, int chunkZ, int minimumY, int height)
throws IOException {
return readRoot(readRoot(dimensionRoot, chunkX, chunkZ), chunkX, chunkZ, minimumY, height);
}
static SavedTerrainChunk readNbt(byte[] bytes, int chunkX, int chunkZ, int minimumY, int height) throws IOException {
if (bytes.length > MAX_CHUNK_BYTES) {
throw new IOException("Native chunk exceeds its size limit");
}
Tag<?> parsed = NBTUtil.read(new ByteArrayInputStream(bytes), false).getTag();
if (!(parsed instanceof CompoundTag root) || number(root, "xPos") != chunkX || number(root, "zPos") != chunkZ) {
throw new IOException("Native chunk identity differs from requested coordinates");
}
return readRoot(root, chunkX, chunkZ, minimumY, height);
}
private static SavedTerrainChunk readRoot(CompoundTag root, int chunkX, int chunkZ, int minimumY, int height)
throws IOException {
String status = string(root, "Status");
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Saved chunk " + chunkX + "," + chunkZ + " has no terrain at native status " + status);
}
Tag<?> receiptTag = root.get(NativeTerrainReceipt.NBT_KEY);
if (root.get("ChunkBukkitValues") instanceof CompoundTag bukkitValues) {
receiptTag = bukkitValues.get(NativeTerrainReceipt.NBT_KEY);
}
if (receiptTag != null) {
if (!(receiptTag instanceof ByteArrayTag bytes)) {
throw new IOException("Native terrain receipt is not a byte array");
}
return NativeTerrainReceipt.restore(bytes.getValue(), status, chunkX, chunkZ, minimumY, height);
}
Tag<?> sectionTag = root.get("sections");
if (!(sectionTag instanceof ListTag<?> sectionList)) {
throw new IOException("Saved chunk has no current-format sections: " + chunkX + "," + chunkZ);
}
Map<Integer, Section> sections = new HashMap<>();
for (Tag<?> entry : sectionList) {
if (!(entry instanceof CompoundTag section)) {
throw new IOException("Saved chunk section is not a compound");
}
int sectionY = number(section, "Y");
if (sectionY * 16 < minimumY || sectionY * 16 >= (long) minimumY + height) {
continue;
}
if (sections.put(sectionY, decodeSection(section)) != null) {
throw new IOException("Saved chunk contains duplicate section " + sectionY);
}
}
return SavedTerrainChunk.captureBoundary(chunkX, chunkZ, minimumY, height, status, new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) throws IOException {
Section section = requireSection(sections, y);
return section.blocks().get((Math.floorMod(y, 16) << 8) | (z << 4) | x);
}
@Override
public String biome(int x, int y, int z) throws IOException {
Section section = requireSection(sections, y);
return section.biomes().get(((Math.floorMod(y, 16) >> 2) << 4) | ((z >> 2) << 2) | (x >> 2));
}
});
}
static void verifyCheckpoint(Path dimensionRoot, int chunkX, int chunkZ, String status, byte[] expectedReceipt, long expectedStructureActivation)
throws IOException {
CompoundTag root = readRoot(dimensionRoot, chunkX, chunkZ);
if (SavedTerrainChunk.statusRank(string(root, "Status")) < SavedTerrainChunk.statusRank(status)) {
throw new IOException("Native terrain checkpoint did not persist stage " + status + " for " + chunkX + "," + chunkZ);
}
if (expectedStructureActivation > 0 && structureActivation(root) != expectedStructureActivation) {
throw new IOException("Native structure checkpoint differs for " + chunkX + "," + chunkZ);
}
if (expectedReceipt != null && !Arrays.equals(expectedReceipt, receipt(root))) {
throw new IOException("Native terrain receipt checkpoint differs for " + chunkX + "," + chunkZ);
}
}
static byte[] readReceipt(Path dimensionRoot, int chunkX, int chunkZ) throws IOException {
return receipt(readRoot(dimensionRoot, chunkX, chunkZ));
}
private static long structureActivation(CompoundTag root) throws IOException {
Tag<?> activation = root.get(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY);
if (root.get("ChunkBukkitValues") instanceof CompoundTag values) {
activation = values.get(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY);
}
if (activation == null) {
return 0;
}
if (!(activation instanceof LongTag value) || value.getValue() <= 0) {
throw new IOException("Invalid native structure activation");
}
return value.getValue();
}
private static byte[] receipt(CompoundTag root) throws IOException {
Tag<?> receipt = root.get(NativeTerrainReceipt.NBT_KEY);
if (root.get("ChunkBukkitValues") instanceof CompoundTag values) {
receipt = values.get(NativeTerrainReceipt.NBT_KEY);
}
if (receipt == null) {
return null;
}
if (!(receipt instanceof ByteArrayTag bytes)) {
throw new IOException("Native terrain receipt is not a byte array");
}
return bytes.getValue();
}
static String readStatus(Path dimensionRoot, int chunkX, int chunkZ) throws IOException {
return string(readRoot(dimensionRoot, chunkX, chunkZ), "Status");
}
private static Section requireSection(Map<Integer, Section> sections, int y) throws IOException {
Section section = sections.get(Math.floorDiv(y, 16));
if (section == null) {
throw new IOException("Saved terrain is missing section " + Math.floorDiv(y, 16));
}
return section;
}
private static Section decodeSection(CompoundTag section) throws IOException {
CompoundTag blocks = compound(section, "block_states");
CompoundTag biomes = compound(section, "biomes");
ListTag<?> blockPalette = list(blocks, "palette");
List<BoundaryColumnGeometry.Voxel> decodedBlocks = new ArrayList<>(blockPalette.size());
for (Tag<?> entry : blockPalette) {
if (!(entry instanceof CompoundTag state)) {
throw new IOException("Saved block palette entry is not a compound");
}
decodedBlocks.add(decodeBlock(state));
}
ListTag<?> biomePalette = list(biomes, "palette");
List<String> decodedBiomes = new ArrayList<>(biomePalette.size());
for (Tag<?> entry : biomePalette) {
if (!(entry instanceof StringTag biome) || biome.getValue().isBlank()) {
throw new IOException("Saved biome palette entry is not a resource key");
}
decodedBiomes.add(biome.getValue());
}
return new Section(new Palette<>(decodedBlocks, packedData(blocks), 4, 4096),
new Palette<>(decodedBiomes, packedData(biomes), 1, 64));
}
private static BoundaryColumnGeometry.Voxel decodeBlock(CompoundTag state) throws IOException {
String name = string(state, "Name");
TreeMap<String, String> properties = new TreeMap<>();
if (state.get("Properties") instanceof CompoundTag values) {
for (Map.Entry<String, Tag<?>> property : values) {
if (!(property.getValue() instanceof StringTag value)) {
throw new IOException("Saved block property is not a string");
}
properties.put(property.getKey(), value.getValue());
}
}
String stateKey = stateKey(name, properties);
if (name.equals("minecraft:air") || name.equals("minecraft:cave_air") || name.equals("minecraft:void_air")) {
return name.equals("minecraft:air") ? AIR
: new BoundaryColumnGeometry.Voxel(stateKey, BoundaryColumnGeometry.Phase.AIR, "", false);
}
boolean liquid = name.equals("minecraft:water") || name.equals("minecraft:lava");
String fluidKey = liquid ? stateKey : "true".equals(properties.get("waterlogged"))
? "minecraft:water[level=0]" : "";
return new BoundaryColumnGeometry.Voxel(stateKey,
liquid ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.SOLID, fluidKey, false);
}
private static String stateKey(String name, Map<String, String> properties) {
if (properties.isEmpty()) {
return name;
}
StringBuilder encoded = new StringBuilder(name).append('[');
for (Map.Entry<String, String> property : properties.entrySet()) {
if (encoded.charAt(encoded.length() - 1) != '[') {
encoded.append(',');
}
encoded.append(property.getKey()).append('=').append(property.getValue());
}
return encoded.append(']').toString();
}
private static CompoundTag readRoot(Path dimensionRoot, int chunkX, int chunkZ) throws IOException {
Path regionDirectory = dimensionRoot.toAbsolutePath().normalize().resolve("region");
Path region = regionDirectory.resolve("r." + Math.floorDiv(chunkX, 32) + "."
+ Math.floorDiv(chunkZ, 32) + ".mca");
if (!Files.isRegularFile(region, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Saved chunk region is unavailable: " + region);
}
byte[] compressed;
int compression;
try (RandomAccessFile input = new RandomAccessFile(region.toFile(), "r")) {
int slot = Math.floorMod(chunkZ, 32) * 32 + Math.floorMod(chunkX, 32);
input.seek((long) slot * 4);
int allocation = input.readInt();
long offset = (long) (allocation >>> 8) * SECTOR_BYTES;
int sectors = allocation & 255;
if (offset < 2L * SECTOR_BYTES || sectors == 0 || offset + 5 > input.length()) {
throw new IOException("Saved chunk allocation is unavailable: " + chunkX + "," + chunkZ);
}
input.seek(offset);
int length = input.readInt();
compression = input.readUnsignedByte();
if ((compression & 128) != 0) {
if (length != 1) {
throw new IOException("External saved chunk has an invalid allocation length");
}
Path external = regionDirectory.resolve("c." + chunkX + "." + chunkZ + ".mcc");
if (!Files.isRegularFile(external, LinkOption.NOFOLLOW_LINKS)
|| Files.size(external) > MAX_CHUNK_BYTES) {
throw new IOException("External saved chunk is unavailable or too large: " + external);
}
compressed = Files.readAllBytes(external);
compression &= 127;
} else {
if (length < 1 || length > MAX_CHUNK_BYTES || (long) length + 4 > (long) sectors * SECTOR_BYTES
|| offset + 4L + length > input.length()) {
throw new IOException("Saved chunk payload is truncated or too large: " + chunkX + "," + chunkZ);
}
compressed = new byte[length - 1];
input.readFully(compressed);
}
input.seek((long) slot * 4);
if (input.readInt() != allocation) {
throw new IOException("Saved chunk allocation changed during boundary capture: " + chunkX + "," + chunkZ);
}
}
try (InputStream decoded = decompress(compressed, compression)) {
byte[] bytes = decoded.readNBytes(MAX_CHUNK_BYTES + 1);
if (bytes.length > MAX_CHUNK_BYTES) {
throw new IOException("Saved chunk decompressed payload exceeds the size limit");
}
Tag<?> tag = NBTUtil.read(new ByteArrayInputStream(bytes), false).getTag();
if (!(tag instanceof CompoundTag root)) {
throw new IOException("Saved chunk root is not a compound");
}
if (number(root, "xPos") != chunkX || number(root, "zPos") != chunkZ) {
throw new IOException("Saved chunk coordinates do not match its region allocation");
}
return root;
} catch (IllegalArgumentException | ClassCastException exception) {
throw new IOException("Invalid saved chunk data at " + chunkX + "," + chunkZ, exception);
}
}
private static InputStream decompress(byte[] encoded, int compression) throws IOException {
InputStream input = new ByteArrayInputStream(encoded);
return switch (compression) {
case 1 -> new GZIPInputStream(input);
case 2 -> new InflaterInputStream(input);
case 3 -> input;
case 4 -> new LZ4BlockInputStream(input);
default -> throw new IOException("Unsupported saved chunk compression " + compression);
};
}
private static long[] packedData(CompoundTag tag) throws IOException {
Tag<?> data = tag.get("data");
if (data == null) {
return new long[0];
}
if (!(data instanceof LongArrayTag longs)) {
throw new IOException("Saved palette data is not a long array");
}
return longs.getValue();
}
private static CompoundTag compound(CompoundTag tag, String key) throws IOException {
if (!(tag.get(key) instanceof CompoundTag value)) {
throw new IOException("Saved chunk field " + key + " is not a compound");
}
return value;
}
private static ListTag<?> list(CompoundTag tag, String key) throws IOException {
if (!(tag.get(key) instanceof ListTag<?> value)) {
throw new IOException("Saved chunk field " + key + " is not a list");
}
return value;
}
private static String string(CompoundTag tag, String key) throws IOException {
if (!(tag.get(key) instanceof StringTag value) || value.getValue().isBlank()) {
throw new IOException("Saved chunk field " + key + " is not a nonempty string");
}
return value.getValue();
}
private static int number(CompoundTag tag, String key) throws IOException {
if (!(tag.get(key) instanceof NumberTag<?> value)) {
throw new IOException("Saved chunk field " + key + " is not a number");
}
return value.asInt();
}
private record Section(Palette<BoundaryColumnGeometry.Voxel> blocks, Palette<String> biomes) {
}
private static final class Palette<T> {
private final List<T> values;
private final long[] data;
private final int bits;
private final int valuesPerLong;
private final long mask;
private Palette(List<T> values, long[] data, int minimumBits, int count) throws IOException {
if (values.isEmpty() || values.size() > count) {
throw new IOException("Saved palette size is invalid");
}
this.values = List.copyOf(values);
this.data = data;
bits = Math.max(minimumBits, 32 - Integer.numberOfLeadingZeros(values.size() - 1));
valuesPerLong = 64 / bits;
mask = (1L << bits) - 1;
int expected = values.size() == 1 ? 0 : Math.ceilDiv(count, valuesPerLong);
if (data.length != expected) {
throw new IOException("Saved palette data length " + data.length + " differs from " + expected);
}
}
private T get(int index) throws IOException {
int paletteIndex = values.size() == 1 ? 0
: (int) ((data[index / valuesPerLong] >>> ((index % valuesPerLong) * bits)) & mask);
if (paletteIndex >= values.size()) {
throw new IOException("Saved block or biome palette index is outside its palette");
}
return values.get(paletteIndex);
}
}
}
@@ -8,10 +8,16 @@ import java.util.OptionalInt;
public final class TerrainBoundarySignature {
private final Column column;
private final Samples samples;
private final BoundaryColumnGeometry geometry;
public TerrainBoundarySignature(Column column, Samples samples) {
public TerrainBoundarySignature(Column column, Samples samples, BoundaryColumnGeometry geometry) {
this.column = Objects.requireNonNull(column, "Boundary column");
this.samples = Objects.requireNonNull(samples, "Boundary samples");
this.geometry = Objects.requireNonNull(geometry, "Boundary geometry");
}
public BoundaryColumnGeometry geometry() {
return geometry;
}
public Column column() {
@@ -53,9 +53,9 @@ public final class TerrainBoundarySignatureStore {
private static final String SHARD_FILE_SUFFIX = ".irtm";
private static final int MAGIC = 0x49525453;
private static final int SCHEMA_VERSION = 3;
private static final int SCHEMA_VERSION = 4;
private static final int SHARD_MAGIC = 0x4952544D;
private static final int SHARD_SCHEMA_VERSION = 2;
private static final int SHARD_SCHEMA_VERSION = 3;
private static final int IDENTITY_BYTES = 32;
private static final int CATALOG_FIXED_BODY_BYTES = 56;
private static final int CATALOG_ENTRY_BYTES = 44;
@@ -76,7 +76,7 @@ public final class TerrainBoundarySignatureStore {
+ CHECKSUM_BYTES;
private static final byte[] IDENTITY_DOMAIN = {
0x49, 0x72, 0x69, 0x73, 0x54, 0x65, 0x72, 0x72,
0x61, 0x69, 0x6E, 0x53, 0x69, 0x67, 0x03
0x61, 0x69, 0x6E, 0x53, 0x69, 0x67, 0x04
};
private final Path dimensionRoot;
@@ -376,7 +376,8 @@ public final class TerrainBoundarySignatureStore {
new TerrainBoundarySignature.Samples(
signature.samples().layout(),
new TerrainBoundarySignature.BiomeEncoding(palette, paletteIndices)
)
),
BoundaryColumnGeometry.fromVoxels(signature.geometry().minimumY(), signature.geometry().voxels())
);
validateSignature(canonical);
return canonical;
@@ -389,6 +390,10 @@ public final class TerrainBoundarySignatureStore {
if (signature.samples().biomes().palette().size() > MAX_PALETTE_COUNT) {
throw new IllegalArgumentException("Terrain boundary biome palette exceeds compact index capacity");
}
for (BoundaryColumnGeometry.Voxel voxel : signature.geometry().palette()) {
encodeBiome(voxel.stateKey());
encodeBiome(voxel.fluidStateKey());
}
for (String biome : signature.samples().biomes().palette()) {
encodeBiome(Objects.requireNonNull(biome, "Terrain boundary biome"));
}
@@ -614,7 +619,8 @@ public final class TerrainBoundarySignatureStore {
signature.fluidHeight(),
signature.upperCeilingDepth()
),
signature.samples()
signature.samples(),
signature.geometry()
));
}
return List.copyOf(relocated);
@@ -713,7 +719,7 @@ public final class TerrainBoundarySignatureStore {
TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException;
@Override
default void close() {
default void close() throws IOException {
}
}
@@ -1275,6 +1281,77 @@ public final class TerrainBoundarySignatureStore {
for (short paletteIndex : biomes.paletteIndices()) {
output.writeShort(paletteIndex);
}
writeGeometry(output, signature.geometry());
}
private static void writeGeometry(DataOutputStream output, BoundaryColumnGeometry geometry)
throws IOException {
output.writeInt(geometry.minimumY());
output.writeInt(geometry.palette().size());
for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) {
writeGeometryKey(output, voxel.stateKey());
output.writeByte(voxel.phase().ordinal());
writeGeometryKey(output, voxel.fluidStateKey());
output.writeBoolean(voxel.protectedContent());
}
int[] runEnds = geometry.runEnds();
short[] indices = geometry.paletteIndices();
output.writeInt(runEnds.length);
for (int run = 0; run < runEnds.length; run++) {
output.writeInt(runEnds[run]);
output.writeShort(indices[run]);
}
}
private static BoundaryColumnGeometry readGeometry(Path source, DataInputStream input)
throws IOException {
int minimumY = input.readInt();
int paletteCount = input.readInt();
if (paletteCount < 0 || paletteCount > MAX_PALETTE_COUNT) {
throw invalid(source, "invalid geometry palette count " + paletteCount);
}
ArrayList<BoundaryColumnGeometry.Voxel> palette = new ArrayList<>(paletteCount);
BoundaryColumnGeometry.Phase[] phases = BoundaryColumnGeometry.Phase.values();
for (int index = 0; index < paletteCount; index++) {
String stateKey = readGeometryKey(source, input);
int phase = input.readUnsignedByte();
if (phase >= phases.length) {
throw invalid(source, "invalid geometry phase " + phase);
}
String fluidKey = readGeometryKey(source, input);
int protectedFlag = input.readUnsignedByte();
if (protectedFlag > 1) {
throw invalid(source, "invalid geometry protection flag " + protectedFlag);
}
palette.add(new BoundaryColumnGeometry.Voxel(stateKey, phases[phase], fluidKey,
protectedFlag == 1));
}
int runCount = input.readInt();
if (runCount < 0 || runCount > BoundaryColumnGeometry.MAXIMUM_HEIGHT
|| (long) runCount * (Integer.BYTES + Short.BYTES) > input.available()) {
throw invalid(source, "invalid geometry run count " + runCount);
}
int[] runEnds = new int[runCount];
short[] indices = new short[runCount];
for (int run = 0; run < runCount; run++) {
runEnds[run] = input.readInt();
indices[run] = input.readShort();
}
return new BoundaryColumnGeometry(minimumY, palette, runEnds, indices);
}
private static void writeGeometryKey(DataOutputStream output, String key) throws IOException {
byte[] encoded = encodeBiome(key);
output.writeInt(encoded.length);
output.write(encoded);
}
private static String readGeometryKey(Path source, DataInputStream input) throws IOException {
int encodedLength = input.readInt();
if (encodedLength < 0 || encodedLength > MAX_BIOME_BYTES || encodedLength > input.available()) {
throw invalid(source, "invalid geometry state key length " + encodedLength);
}
return decodeBiome(source, input.readNBytes(encodedLength));
}
private static TerrainBoundarySignature readSignature(
@@ -1344,7 +1421,8 @@ public final class TerrainBoundarySignatureStore {
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(minimumY, sampleStep, sampleCount),
new TerrainBoundarySignature.BiomeEncoding(palette, paletteIndices)
)
),
readGeometry(source, input)
);
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
@@ -27,7 +28,7 @@ final class TransitionBoundarySampler {
throw new IllegalArgumentException("Terrain transition width must be positive");
}
this.terrainWidth = terrainWidth;
this.searchWidth = Math.multiplyExact(terrainWidth, 2);
this.searchWidth = terrainWidth;
this.signatures = Objects.requireNonNull(signatures, "Terrain boundary signatures");
this.chunkCandidates = new LinkedHashMap<>(MAXIMUM_CACHED_CHUNKS, 0.75F, true);
this.cache = ThreadLocal.withInitial(ChunkCache::new);
@@ -56,6 +57,34 @@ final class TransitionBoundarySampler {
return sampled;
}
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
CandidateIndex candidates = candidatesForChunk(
Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE),
Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE));
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
return BoundaryGeometryInfluence.none();
}
double distance = Math.sqrt(nearest.distancesSquared[0]);
double weight = GenerationBlend.newEpochWeight(Math.max(0D, distance - 1D),
Math.max(1, terrainWidth - 1));
int count = nearest.distancesSquared[0] <= 1D ? 1 : nearest.count;
double total = 0D;
for (int index = 0; index < count; index++) {
total += 1D / Math.max(1D, nearest.distancesSquared[index]);
}
ArrayList<BoundaryGeometryInfluence.Contribution> contributions = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
contributions.add(new BoundaryGeometryInfluence.Contribution(
nearest.signatures[index].geometry(),
(1D / Math.max(1D, nearest.distancesSquared[index])) / total));
}
int openingDepth = Math.min(8, Math.max(1, terrainWidth / 4));
double openingWeight = GenerationBlend.newEpochWeight(Math.max(0D, distance - openingDepth),
Math.max(1, terrainWidth - openingDepth));
return new BoundaryGeometryInfluence(weight, openingWeight, contributions);
}
boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) {
return signatures.intersectsTerrainBand(
new TerrainBoundarySignatureStore.BlockBounds(minimumX, minimumZ, maximumX, maximumZ),
@@ -89,11 +118,7 @@ final class TransitionBoundarySampler {
Math.min(distance, terrainWidth),
terrainWidth
);
double hydrologyDistance = Math.max(0D, distance - terrainWidth);
double hydrologyWeight = GenerationBlend.newEpochWeight(
Math.min(hydrologyDistance, terrainWidth),
terrainWidth
);
double hydrologyWeight = newEpochWeight;
return new TransitionGenerationPlan.TerrainSample(
Math.min(distance, searchWidth),
newEpochWeight,
@@ -0,0 +1,149 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;
public final class TransitionFluidContainment {
private static final Set<String> BANK_MATERIALS = Set.of(
"minecraft:stone", "minecraft:deepslate", "minecraft:granite", "minecraft:diorite",
"minecraft:andesite", "minecraft:tuff", "minecraft:calcite", "minecraft:bedrock",
"minecraft:dirt", "minecraft:coarse_dirt", "minecraft:grass_block", "minecraft:podzol",
"minecraft:mycelium", "minecraft:clay", "minecraft:terracotta", "minecraft:netherrack",
"minecraft:basalt", "minecraft:smooth_basalt", "minecraft:blackstone", "minecraft:end_stone",
"minecraft:obsidian", "minecraft:crying_obsidian");
private static final BoundaryColumnGeometry.Voxel STONE = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionFluidContainment() {
}
public static SavedTerrainChunk contain(SavedTerrainChunk raw, TransitionGenerationPlan plan, ColumnSource source)
throws IOException {
List<BoundaryColumnGeometry> columns = new ArrayList<>(256);
boolean changed = false;
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int x = Math.addExact(Math.multiplyExact(raw.chunkX(), 16), localX);
int z = Math.addExact(Math.multiplyExact(raw.chunkZ(), 16), localZ);
BoundaryColumnGeometry original = raw.column(x, z).geometry();
BoundaryColumnGeometry corrected = plan.isHistoricalBlock(x, z) || plan.newEpochWeightAt(x, z) == 1D
? original : containColumn(x, z, original, source);
columns.add(corrected);
changed |= corrected != original;
}
}
if (!changed) {
return raw;
}
BoundaryColumnGeometry first = columns.getFirst();
return SavedTerrainChunk.capture(raw.chunkX(), raw.chunkZ(), first.minimumY(), first.height(),
raw.nativeStatus(), new ContainedSource(raw, columns));
}
private static BoundaryColumnGeometry containColumn(int x, int z, BoundaryColumnGeometry original,
ColumnSource source) throws IOException {
List<BoundaryColumnGeometry.Voxel> voxels = original.voxels();
List<BoundaryColumnGeometry.Voxel> corrected = null;
BoundaryColumnGeometry.Voxel[] banks = null;
BoundaryColumnGeometry[] neighbors = null;
for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
if (voxel.phase() != BoundaryColumnGeometry.Phase.FLUID || voxel.protectedContent()) {
continue;
}
if (neighbors == null) {
neighbors = new BoundaryColumnGeometry[]{
source.column(Math.subtractExact(x, 1), z), source.column(Math.addExact(x, 1), z),
source.column(x, Math.subtractExact(z, 1)), source.column(x, Math.addExact(z, 1))};
}
int worldY = Math.addExact(original.minimumY(), offset);
if (!exposed(original, neighbors, worldY)) {
continue;
}
if (corrected == null) {
corrected = new ArrayList<>(voxels);
banks = nearestDrySolids(voxels);
}
corrected.set(offset, banks[offset]);
}
return corrected == null ? original : BoundaryColumnGeometry.fromVoxels(original.minimumY(), corrected);
}
private static boolean exposed(BoundaryColumnGeometry original, BoundaryColumnGeometry[] neighbors, int worldY) {
if (original.voxelAt(Math.subtractExact(worldY, 1)).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
for (BoundaryColumnGeometry neighbor : neighbors) {
if (neighbor.voxelAt(worldY).phase() == BoundaryColumnGeometry.Phase.AIR) {
return true;
}
}
return false;
}
private static BoundaryColumnGeometry.Voxel[] nearestDrySolids(List<BoundaryColumnGeometry.Voxel> voxels) {
BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()];
int[] distances = new int[voxels.size()];
BoundaryColumnGeometry.Voxel nearest = STONE;
int last = -voxels.size() - 1;
for (int offset = 0; offset < voxels.size(); offset++) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
materials[offset] = nearest;
distances[offset] = offset - last;
}
last = voxels.size() * 2;
nearest = null;
for (int offset = voxels.size() - 1; offset >= 0; offset--) {
if (drySolid(voxels.get(offset))) {
nearest = voxels.get(offset);
last = offset;
}
if (nearest != null && last - offset < distances[offset]) {
materials[offset] = nearest;
}
}
return materials;
}
private static boolean drySolid(BoundaryColumnGeometry.Voxel voxel) {
String state = voxel.stateKey();
int properties = state.indexOf('[');
String key = properties < 0 ? state : state.substring(0, properties);
return voxel.phase() == BoundaryColumnGeometry.Phase.SOLID
&& voxel.fluidStateKey().isEmpty() && !voxel.protectedContent() && BANK_MATERIALS.contains(key);
}
@FunctionalInterface
public interface ColumnSource {
BoundaryColumnGeometry column(int blockX, int blockZ) throws IOException;
}
private record ContainedSource(SavedTerrainChunk raw, List<BoundaryColumnGeometry> columns)
implements SavedTerrainChunk.VoxelSource {
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(signature(localX, localZ).oceanFloorHeight());
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
return columns.get(localX * 16 + localZ).voxelAt(worldY);
}
@Override
public String biome(int localX, int worldY, int localZ) {
TerrainBoundarySignature signature = signature(localX, localZ);
return signature.biomeAtSample((worldY - signature.samples().layout().minimumY()) / 4);
}
private TerrainBoundarySignature signature(int x, int z) {
return raw.column((raw.chunkX() << 4) + x, (raw.chunkZ() << 4) + z);
}
}
}
@@ -89,7 +89,25 @@ public final class TransitionGenerationPlan {
}
public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) {
return terrainSampleAt(blockX, blockZ).newEpochWeight() == 1D;
return !boundary.isHistoricalBlock(blockX, blockZ);
}
public boolean hasTransitionAtChunk(int chunkX, int chunkZ) {
if (boundary.isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int minimumX = Math.multiplyExact(chunkX, GenerationBoundary.CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, GenerationBoundary.CHUNK_SIZE);
return terrainSampler.intersectsTerrainBand(minimumX, minimumZ,
Math.addExact(minimumX, GenerationBoundary.CHUNK_SIZE - 1),
Math.addExact(minimumZ, GenerationBoundary.CHUNK_SIZE - 1));
}
public BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
if (boundary.isHistoricalBlock(blockX, blockZ)) {
return BoundaryGeometryInfluence.none();
}
return terrainSampler.geometryAt(blockX, blockZ);
}
public TerrainSample terrainSampleAt(int blockX, int blockZ) {
@@ -100,19 +118,30 @@ public final class TransitionGenerationPlan {
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
TerrainSample sample = terrainSampleAt(blockX, blockZ);
return historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ, terrainSampleAt(blockX, blockZ));
}
public Optional<String> historicalPhysicalBiomeKeyAt(
int blockX,
int blockY,
int blockZ,
TerrainSample sample
) {
Objects.requireNonNull(sample, "Terrain sample");
if (sample.newEpochWeight() == 1D) {
return Optional.empty();
}
return sample.historicalPhysicalBiomeKeyAt(blockY);
double weight = GenerationBlend.newEpochWeight(
Math.max(0D, sample.distanceToHistoricalTerrain() - 1D), Math.max(1, widthBlocks() - 1));
return GenerationBlend.usesHistoricalMaterial(blockX, blockY, blockZ, weight)
? sample.historicalPhysicalBiomeKeyAt(blockY) : Optional.empty();
}
public boolean allowsNewFootprint(int minimumX, int minimumZ, int maximumX, int maximumZ) {
if (minimumX > maximumX || minimumZ > maximumZ) {
throw new IllegalArgumentException("Generation footprint bounds are inverted");
}
return !boundary.intersectsHistoricalBlocks(minimumX, minimumZ, maximumX, maximumZ)
&& !terrainSampler.intersectsTerrainBand(minimumX, minimumZ, maximumX, maximumZ);
return !boundary.intersectsHistoricalBlocks(minimumX, minimumZ, maximumX, maximumZ);
}
long terrainCatalogProbeCount() {
@@ -0,0 +1,205 @@
package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class TransitionGeometryBlender {
private static final int MAXIMUM_OPENING_HEIGHT = 64;
private static final BoundaryColumnGeometry.Voxel AIR = new BoundaryColumnGeometry.Voxel(
"minecraft:air", BoundaryColumnGeometry.Phase.AIR, "", false);
private static final BoundaryColumnGeometry.Voxel FLUID_BARRIER = new BoundaryColumnGeometry.Voxel(
"minecraft:obsidian", BoundaryColumnGeometry.Phase.SOLID, "", false);
private TransitionGeometryBlender() {
}
public static BoundaryColumnGeometry blendColumn(
TransitionGenerationPlan plan,
int blockX,
int blockZ,
BoundaryColumnGeometry currentGeometry
) {
Objects.requireNonNull(plan, "Transition plan");
return blendColumn(plan.geometryAt(blockX, blockZ), blockX, blockZ, currentGeometry);
}
public static BoundaryColumnGeometry blendColumn(
BoundaryGeometryInfluence influence,
int blockX,
int blockZ,
BoundaryColumnGeometry currentGeometry
) {
Objects.requireNonNull(influence, "Boundary influence");
Objects.requireNonNull(currentGeometry, "Current geometry");
if (influence.newTerrainWeight() == 1D || influence.contributions().isEmpty()
|| currentGeometry.height() == 0) {
return currentGeometry;
}
ColumnProfile current = new ColumnProfile(currentGeometry);
ArrayList<WeightedProfile> historical = new ArrayList<>(influence.contributions().size());
for (BoundaryGeometryInfluence.Contribution contribution : influence.contributions()) {
BoundaryColumnGeometry geometry = contribution.geometry();
if (geometry.minimumY() != currentGeometry.minimumY() || geometry.height() != currentGeometry.height()) {
throw new IllegalArgumentException("Boundary geometry does not match the current vertical layout");
}
historical.add(new WeightedProfile(new ColumnProfile(geometry), contribution.weight()));
}
ArrayList<BoundaryColumnGeometry.Voxel> result = new ArrayList<>(currentGeometry.height());
boolean changed = false;
for (int offset = 0; offset < currentGeometry.height(); offset++) {
BoundaryColumnGeometry.Voxel value = blendVoxel(current, historical, offset,
influence.newTerrainWeight(), influence.openingWeight(), GenerationBlend.usesHistoricalMaterial(blockX,
currentGeometry.minimumY() + offset, blockZ, influence.newTerrainWeight()));
result.add(value);
changed |= !value.equals(current.voxels.get(offset));
}
return changed ? BoundaryColumnGeometry.fromVoxels(currentGeometry.minimumY(), result) : currentGeometry;
}
private static BoundaryColumnGeometry.Voxel blendVoxel(
ColumnProfile current,
List<WeightedProfile> historical,
int offset,
double currentWeight,
double openingWeight,
boolean historicalMaterial
) {
BoundaryColumnGeometry.Voxel currentVoxel = current.voxels.get(offset);
if (currentVoxel.protectedContent()) {
return currentVoxel;
}
boolean historicalOpening = false;
double solid = 0D;
double fluid = 0D;
double total = 0D;
double solidMaterialWeight = -1D;
double fluidMaterialWeight = -1D;
BoundaryColumnGeometry.Voxel oldSolid = null;
BoundaryColumnGeometry.Voxel oldFluid = null;
for (WeightedProfile contribution : historical) {
ColumnProfile profile = contribution.profile();
if (profile.voxels.get(offset).protectedContent()) {
continue;
}
historicalOpening |= profile.enclosedOpenings[offset];
double weight = contribution.weight();
total += weight;
solid += profile.solidDistances[offset] * weight;
fluid += profile.fluidDistances[offset] * weight;
if (profile.solidMaterials[offset] != null && weight > solidMaterialWeight) {
oldSolid = profile.solidMaterials[offset];
solidMaterialWeight = weight;
}
if (profile.fluidMaterials[offset] != null && weight > fluidMaterialWeight) {
oldFluid = profile.fluidMaterials[offset];
fluidMaterialWeight = weight;
}
}
if (total == 0D) {
return currentVoxel;
}
double blendedSolid = GenerationBlend.interpolate(solid / total,
current.solidDistances[offset], historicalOpening ? openingWeight : currentWeight);
if (blendedSolid > 0D) {
return selectMaterial(oldSolid, current.solidMaterials[offset], historicalMaterial);
}
double blendedFluid = GenerationBlend.interpolate(fluid / total,
current.fluidDistances[offset], historicalOpening ? openingWeight : currentWeight);
if (blendedFluid > 0D) {
BoundaryColumnGeometry.Voxel newFluid = current.fluidMaterials[offset];
if (oldFluid != null && newFluid != null && !fluidFamily(oldFluid).equals(fluidFamily(newFluid))) {
return FLUID_BARRIER;
}
return selectMaterial(oldFluid, newFluid, currentWeight <= 0.5D);
}
return AIR;
}
private static String fluidFamily(BoundaryColumnGeometry.Voxel voxel) {
String key = voxel.fluidStateKey();
int properties = key.indexOf('[');
return properties < 0 ? key : key.substring(0, properties);
}
private static BoundaryColumnGeometry.Voxel selectMaterial(
BoundaryColumnGeometry.Voxel historical,
BoundaryColumnGeometry.Voxel current,
boolean historicalMaterial
) {
if (historical == null) {
return current == null ? AIR : current;
}
return current == null || historicalMaterial ? historical : current;
}
private static BoundaryColumnGeometry.Voxel[] nearestMaterials(
List<BoundaryColumnGeometry.Voxel> voxels,
BoundaryColumnGeometry.Phase phase
) {
BoundaryColumnGeometry.Voxel[] materials = new BoundaryColumnGeometry.Voxel[voxels.size()];
int[] distances = new int[voxels.size()];
int last = -voxels.size() - 1;
BoundaryColumnGeometry.Voxel nearest = null;
for (int index = 0; index < voxels.size(); index++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
if (voxel.phase() == phase && !voxel.protectedContent()) {
nearest = voxel;
last = index;
}
materials[index] = nearest;
distances[index] = index - last;
}
last = voxels.size() * 2;
nearest = null;
for (int index = voxels.size() - 1; index >= 0; index--) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
if (voxel.phase() == phase && !voxel.protectedContent()) {
nearest = voxel;
last = index;
}
if (nearest != null && last - index < distances[index]) {
materials[index] = nearest;
}
}
return materials;
}
private static boolean[] enclosedOpenings(List<BoundaryColumnGeometry.Voxel> voxels) {
boolean[] openings = new boolean[voxels.size()];
int lowerSolid = -1;
for (int index = 0; index < voxels.size(); index++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) {
if (lowerSolid >= 0 && index - lowerSolid - 1 <= MAXIMUM_OPENING_HEIGHT) {
for (int opening = lowerSolid + 1; opening < index; opening++) {
openings[opening] = !voxels.get(opening).protectedContent();
}
}
lowerSolid = index;
}
}
return openings;
}
private static final class ColumnProfile {
private final List<BoundaryColumnGeometry.Voxel> voxels;
private final boolean[] enclosedOpenings;
private final double[] solidDistances;
private final double[] fluidDistances;
private final BoundaryColumnGeometry.Voxel[] solidMaterials;
private final BoundaryColumnGeometry.Voxel[] fluidMaterials;
private ColumnProfile(BoundaryColumnGeometry geometry) {
voxels = geometry.voxels();
enclosedOpenings = enclosedOpenings(voxels);
solidDistances = geometry.solidDistances();
fluidDistances = geometry.fluidDistances();
solidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.SOLID);
fluidMaterials = nearestMaterials(voxels, BoundaryColumnGeometry.Phase.FLUID);
}
}
private record WeightedProfile(ColumnProfile profile, double weight) {
}
}
@@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
@@ -61,6 +62,31 @@ public final class WorldChunkInventory {
return scanRegionDirectory(worldDirectory.resolve("region"));
}
public WorldChunkInventory filter(ChunkPredicate predicate) throws IOException {
Objects.requireNonNull(predicate, "chunk predicate");
Long2ObjectOpenHashMap<BitSet> selected = new Long2ObjectOpenHashMap<>(regionMasks.size());
for (long regionKey : regionKeys) {
int baseX = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkX(regionKey));
int baseZ = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkZ(regionKey));
BitSet source = regionMasks.get(regionKey);
BitSet accepted = new BitSet(REGION_SIDE * REGION_SIDE);
for (int index = source.nextSetBit(0); index >= 0; index = source.nextSetBit(index + 1)) {
if (predicate.test(baseX + (index & 31), baseZ + (index >> 5))) {
accepted.set(index);
}
}
if (!accepted.isEmpty()) {
selected.put(regionKey, accepted);
}
}
return new WorldChunkInventory(selected);
}
@FunctionalInterface
public interface ChunkPredicate {
boolean test(int chunkX, int chunkZ) throws IOException;
}
public static boolean isDurablyAllocated(Path worldDirectory, int chunkX, int chunkZ) throws IOException {
Path regionDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory")
.toAbsolutePath()
@@ -175,24 +201,36 @@ public final class WorldChunkInventory {
private static BitSet scanAllocationTable(Path file) throws IOException {
try (RandomAccessFile input = new RandomAccessFile(file.toFile(), "r")) {
long length = input.length();
if (length < HEADER_BYTES || length % SECTOR_BYTES != 0L) {
if (length == 0L) {
return new BitSet(REGION_SIDE * REGION_SIDE);
}
if (length < HEADER_BYTES) {
throw new IOException("Truncated world region file: " + file);
}
long sectorCapacity = length / SECTOR_BYTES;
byte[] header = new byte[SECTOR_BYTES];
input.readFully(header);
ByteBuffer locations = ByteBuffer.wrap(header);
length = input.length();
if (length < HEADER_BYTES) {
throw new IOException("Truncated world region file: " + file);
}
long sectorCapacity = length / SECTOR_BYTES + (length % SECTOR_BYTES == 0L ? 0L : 1L);
BitSet claimedSectors = new BitSet(Math.toIntExact(sectorCapacity));
claimedSectors.set(0, HEADER_SECTORS);
BitSet mask = new BitSet(REGION_SIDE * REGION_SIDE);
for (int localZ = 0; localZ < REGION_SIDE; localZ++) {
for (int localX = 0; localX < REGION_SIDE; localX++) {
int index = MCAFile.getChunkIndex(localX, localZ);
input.seek((long) index * Integer.BYTES);
int location = input.readInt();
int location = locations.getInt(index * Integer.BYTES);
if (location == 0) {
continue;
}
int sectorOffset = location >>> Byte.SIZE;
int sectorCount = location & 0xFF;
validateAllocation(file, sectorOffset, sectorCount, sectorCapacity, claimedSectors);
if (((long) sectorOffset + sectorCount) * SECTOR_BYTES > length) {
validateUnpaddedAllocation(file, input, sectorOffset, sectorCount, length);
}
mask.set(index);
}
}
@@ -202,6 +240,26 @@ public final class WorldChunkInventory {
}
}
private static void validateUnpaddedAllocation(
Path file,
RandomAccessFile input,
int sectorOffset,
int sectorCount,
long fileLength
) throws IOException {
long chunkStart = (long) sectorOffset * SECTOR_BYTES;
if (chunkStart + Integer.BYTES >= fileLength) {
throw new IOException("Truncated chunk payload in world region file: " + file);
}
input.seek(chunkStart);
int payloadLength = input.readInt();
long storedLength = Integer.BYTES + (long) payloadLength;
if (payloadLength < 1 || storedLength > (long) sectorCount * SECTOR_BYTES
|| chunkStart + storedLength > fileLength) {
throw new IOException("Truncated chunk payload in world region file: " + file);
}
}
private static void validateAllocation(
Path file,
int sectorOffset,
@@ -21,6 +21,8 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.UpperDimensionContext;
@@ -54,6 +56,8 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.TimeUnit;
public interface EngineMantle extends MatterGenerator {
@@ -109,6 +113,10 @@ public interface EngineMantle extends MatterGenerator {
}
default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z));
}
@@ -117,6 +125,21 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean isCarved(int x, int h, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, h, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
PlatformBlockState block = getMantle().get(x, h, z, PlatformBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(h + getEngine().getMinHeight());
}
}
if (cell != null && cell.cavernCaptured()) {
return getMantle().get(x, h, z, MatterCavern.class) != null;
}
return resolved.get().geometry().isEnclosedOpenAt(h + getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(getMantle(), x, h, z);
if (hydrology != null) {
return hydrology.carves();
@@ -126,9 +149,19 @@ public interface EngineMantle extends MatterGenerator {
default PlatformBlockState get(int x, int y, int z) {
PlatformBlockState block = getMantle().get(x, y, z, PlatformBlockState.class);
if (block == null)
return AIR;
return block;
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getMantle().get(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
String stateKey = resolved.get().geometry().voxelAt(y + getEngine().getMinHeight()).stateKey();
PlatformBlockState natural = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (natural == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return natural;
}
}
return block == null ? AIR : block;
}
default boolean isPreventingDecay() {
@@ -144,6 +177,10 @@ public interface EngineMantle extends MatterGenerator {
}
default int getFluidHeight(int x, int z) {
Optional<TerrainBoundarySignature> resolved = getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().orElse(-1);
}
return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
}
@@ -32,6 +32,10 @@ import org.jetbrains.annotations.NotNull;
public interface MantleComponent extends Comparable<MantleComponent> {
int getPriority();
default MatterGenerationPhase getGenerationPhase() {
return MatterGenerationPhase.CONTENT;
}
int getRadius();
default int getOutputRadius() {
@@ -30,6 +30,9 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.TileData;
@@ -59,6 +62,7 @@ import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -463,12 +467,34 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private Optional<TerrainBoundarySignature> resolvedColumn(int x, int z) {
IrisComplex complex = engineMantle.getComplex();
return complex == null ? Optional.empty() : complex.resolvedTerrainColumn(x, z);
}
private PlatformBlockState resolvedBlock(TerrainBoundarySignature column, int y) {
String stateKey = column.geometry().voxelAt(y + engineMantle.getEngine().getMinHeight()).stateKey();
PlatformBlockState state = IrisPlatforms.get().registries().blockOrNull(stateKey);
if (state == null) {
throw new IllegalStateException("Saved terrain state is unavailable: " + stateKey);
}
return state;
}
public PlatformBlockState getPrerequisiteBlock(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedBlock(resolved.get(), y);
}
PlatformBlockState block = getPrerequisiteDataIfPresent(x, y, z, PlatformBlockState.class);
return block == null ? AIR : block;
}
public boolean isPrerequisiteCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getPrerequisiteDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -477,6 +503,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public byte[] getPrerequisiteCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
return resolvedCarvedColumn(resolved.get(), height);
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
@@ -818,6 +848,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
// Read-only probe: getDataIfPresent returns the identical answer without materializing
// a 16^3 section + slice on a miss the way getData's getOrCreate path does.
PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class);
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell == null || !cell.blockCaptured()) {
return resolvedBlock(resolved.get(), y);
}
}
if (block == null)
return AIR;
return block;
@@ -830,6 +867,20 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public boolean isCarved(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class);
if (block != null) {
return (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight());
}
}
return cell != null && cell.cavernCaptured()
? getDataIfPresent(x, y, z, MatterCavern.class) != null
: resolved.get().geometry().isEnclosedOpenAt(y + engineMantle.getEngine().getMinHeight());
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.carves();
@@ -837,7 +888,45 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return getDataIfPresent(x, y, z, MatterCavern.class) != null;
}
private byte[] resolvedCarvedColumn(TerrainBoundarySignature column, int height) {
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carved = new byte[cappedHeight];
int minimumY = engineMantle.getEngine().getMinHeight();
BoundaryColumnGeometry geometry = column.geometry();
boolean ceiling = false;
for (int worldY = geometry.minimumY() + geometry.height() - 1; worldY >= minimumY; worldY--) {
BoundaryColumnGeometry.Voxel voxel = geometry.voxelAt(worldY);
int internalY = worldY - minimumY;
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID) {
ceiling = true;
} else if (ceiling && internalY < cappedHeight) {
carved[internalY] = 1;
}
}
return carved;
}
public byte[] getCarvedColumn(int x, int z, int height) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
if (resolved.isPresent()) {
byte[] carved = resolvedCarvedColumn(resolved.get(), height);
for (int y = 0; y < carved.length; y++) {
PreObjectMatterCell cell = getDataIfPresent(x, y, z, PreObjectMatterCell.class);
if (cell != null && cell.blockCaptured()) {
PlatformBlockState block = getDataIfPresent(x, y, z, PlatformBlockState.class);
if (block != null) {
carved[y] = (block.isAir() || block.isFluid())
&& resolved.get().geometry().hasSolidAbove(y + engineMantle.getEngine().getMinHeight())
? (byte) 1 : 0;
continue;
}
}
if (cell != null && cell.cavernCaptured()) {
carved[y] = getDataIfPresent(x, y, z, MatterCavern.class) != null ? (byte) 1 : 0;
}
}
return carved;
}
int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight());
byte[] carvedColumn = new byte[cappedHeight];
if (cappedHeight <= 0) {
@@ -888,6 +977,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return carvedColumn;
}
@Override
public boolean isSurfaceSolid(int x, int y, int z) {
Optional<TerrainBoundarySignature> resolved = resolvedColumn(x, z);
return resolved.isEmpty() || resolved.get().geometry()
.voxelAt(y + engineMantle.getEngine().getMinHeight()).phase() == BoundaryColumnGeometry.Phase.SOLID;
}
@Override
public boolean isSolid(int x, int y, int z) {
return B.isSolid(get(x, y, z));
@@ -0,0 +1,11 @@
package art.arcane.iris.engine.mantle;
public enum MatterGenerationPhase {
ALL,
TERRAIN,
CONTENT;
public boolean includes(MantleComponent component) {
return this == ALL || component.getGenerationPhase() == this;
}
}
@@ -43,6 +43,21 @@ public interface MatterGenerator {
@ChunkCoordinates
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.ALL);
}
@ChunkCoordinates
default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) {
generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.TERRAIN);
}
@ChunkCoordinates
default void generateContentMatter(int x, int z, boolean multicore, ChunkContext context) {
generateMatterPhase(x, z, multicore, context, MatterGenerationPhase.CONTENT);
}
private void generateMatterPhase(int x, int z, boolean multicore, ChunkContext context,
MatterGenerationPhase phase) {
if (!getEngine().getDimension().isUseMantle()) {
return;
}
@@ -53,7 +68,7 @@ public interface MatterGenerator {
if (complex != null && !complex.allowsMantleChunkWrite(x, z)) {
return;
}
generateMatterWindow(x, z, multicore, context, complex);
generateMatterWindow(x, z, multicore, context, complex, phase);
}
private void generateMatterWindow(
@@ -61,16 +76,19 @@ public interface MatterGenerator {
int z,
boolean multicore,
ChunkContext context,
IrisComplex complex
IrisComplex complex,
MatterGenerationPhase phase
) {
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context, phase);
MatterPassPlan[] passPlans = generationPlan.passPlans();
if (passPlans.length == 0) {
return;
}
int prefetchRadius = passPlans[0].passChunkRadius();
LongOpenHashSet partialChunks = new LongOpenHashSet();
List<MantleComponent> requiredComponents = enabledComponents();
List<MantleFlag> terrainFlags = terrainFlags(requiredComponents);
try (MantleWriter writer = new MantleWriter(
getEngine().getMantle(),
@@ -94,7 +112,7 @@ public interface MatterGenerator {
int[] componentPassRadii = new int[passComponents.size()];
int enabledComponentCount = 0;
for (MantleComponent component : passComponents) {
if (shouldGenerateComponent(component)) {
if (phase.includes(component) && shouldGenerateComponent(component)) {
// A component must cover its own reach plus every later pass' reach, or a
// later pass reads this component's data from chunks it never wrote.
int componentReach = component.getOutputRadius() + passPlan.downstreamBlockRadius();
@@ -162,6 +180,9 @@ public interface MatterGenerator {
continue;
}
if (phase == MatterGenerationPhase.CONTENT) {
requireTerrainPhase(chunk, passX, passZ, terrainFlags);
}
int eligibleComponentCount = 0;
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
MantleComponent component = enabledComponents[componentIndex];
@@ -217,6 +238,9 @@ public interface MatterGenerator {
}
}
if (phase == MatterGenerationPhase.TERRAIN) {
return;
}
int realRadius = passPlans[passPlans.length - 1].passChunkRadius();
for (int i = -realRadius; i <= realRadius; i++) {
for (int j = -realRadius; j <= realRadius; j++) {
@@ -229,7 +253,10 @@ public interface MatterGenerator {
if (complex != null && !complex.allowsMantleChunkWrite(realX, realZ)) {
continue;
}
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
MantleChunk<Matter> chunk = writer.acquireChunk(realX, realZ);
if (hasCompletedComponents(chunk, requiredComponents)) {
chunk.flag(MantleFlag.PLANNED, true);
}
}
}
} finally {
@@ -242,7 +269,7 @@ public interface MatterGenerator {
return (((long) x) << 32) ^ (z & 0xffffffffL);
}
private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context) {
private MatterGenerationPlan resolveGenerationPlan(int x, int z, ChunkContext context, MatterGenerationPhase phase) {
List<MantlePass> passes = getComponents();
MatterPassPlan[] plans = new MatterPassPlan[passes.size()];
int accessDownstreamBlockRadius = 0;
@@ -278,7 +305,48 @@ public interface MatterGenerator {
int writerChunkRadius = accessDownstreamBlockRadius > 0
? Math.ceilDiv(accessDownstreamBlockRadius, 16)
: 0;
return new MatterGenerationPlan(plans, writerChunkRadius * 2);
return new MatterGenerationPlan(plans, writerChunkRadius * 2, phase);
}
private List<MantleComponent> enabledComponents() {
List<MantleComponent> components = new ArrayList<>();
for (MantlePass pass : getComponents()) {
for (MantleComponent component : pass.components()) {
if (shouldGenerateComponent(component)) {
components.add(component);
}
}
}
return List.copyOf(components);
}
private static List<MantleFlag> terrainFlags(List<MantleComponent> components) {
List<MantleFlag> flags = new ArrayList<>();
for (MantleComponent component : components) {
if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) {
flags.add(component.getFlag());
}
}
return List.copyOf(flags);
}
private static void requireTerrainPhase(MantleChunk<Matter> chunk, int chunkX, int chunkZ,
List<MantleFlag> flags) {
for (MantleFlag flag : flags) {
if (!chunk.isFlagged(flag)) {
throw new IllegalStateException("Content generation at " + chunkX + "," + chunkZ
+ " requires completed terrain component " + flag.name());
}
}
}
private static boolean hasCompletedComponents(MantleChunk<Matter> chunk, List<MantleComponent> components) {
for (MantleComponent component : components) {
if (!chunk.isFlagged(component.getFlag())) {
return false;
}
}
return true;
}
private boolean shouldGenerateComponent(MantleComponent component) {
@@ -535,7 +603,7 @@ public interface MatterGenerator {
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
}
record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius) {
record MatterGenerationPlan(MatterPassPlan[] passPlans, int writerAccessRadius, MatterGenerationPhase phase) {
}
final class MatterTaskKey {
@@ -0,0 +1,94 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import java.util.Objects;
import java.util.ArrayList;
import java.util.List;
public final class TerrainMatterView {
private TerrainMatterView() {
}
public static <T> T get(Mantle<Matter> mantle, int x, int y, int z, Class<T> type) {
if (!hasJournal(type)) {
return mantle.get(x, y, z, type);
}
MantleChunk<Matter> chunk = mantle.getChunk(x >> 4, z >> 4);
synchronized (chunk) {
PreObjectMatterCell cell = mantle.get(x, y, z, PreObjectMatterCell.class);
return cell != null && cell.captures(type) ? cell.original(type) : mantle.get(x, y, z, type);
}
}
public static <T> T get(MantleChunk<Matter> chunk, int x, int y, int z, Class<T> type) {
Objects.requireNonNull(type, "Terrain matter type");
if (chunk == null || y < 0) {
return null;
}
if (!hasJournal(type)) {
return raw(chunk, x, y, z, type);
}
synchronized (chunk) {
PreObjectMatterCell cell = raw(chunk, x, y, z, PreObjectMatterCell.class);
return cell != null && cell.captures(type) ? cell.original(type) : raw(chunk, x, y, z, type);
}
}
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
Consumer4<Integer, Integer, Integer, T> consumer) {
Objects.requireNonNull(chunk, "Terrain mantle chunk");
Objects.requireNonNull(type, "Terrain matter type");
Objects.requireNonNull(consumer, "Terrain matter consumer");
if (!hasJournal(type)) {
chunk.iterate(type, consumer);
return;
}
List<TerrainEntry<T>> entries = new ArrayList<>();
synchronized (chunk) {
chunk.iterate(type, (x, y, z, value) -> {
T original = get(chunk, x, y, z, type);
if (original != null) {
entries.add(new TerrainEntry<>(x, y, z, original));
}
});
chunk.iterate(PreObjectMatterCell.class, (x, y, z, cell) -> {
if (cell == null || !cell.captures(type) || raw(chunk, x, y, z, type) != null) {
return;
}
T original = cell.original(type);
if (original != null) {
entries.add(new TerrainEntry<>(x, y, z, original));
}
});
}
for (TerrainEntry<T> entry : entries) {
consumer.accept(entry.x(), entry.y(), entry.z(), entry.value());
}
}
private record TerrainEntry<T>(int x, int y, int z, T value) {
}
private static boolean hasJournal(Class<?> type) {
return type == MatterCavern.class || type == PlatformBlockState.class || type == String.class
|| type == HydrologyCaveCell.class;
}
private static <T> T raw(MantleChunk<Matter> chunk, int x, int y, int z, Class<T> type) {
int section = y >> 4;
if (!chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
return matter == null || !matter.hasSlice(type) ? null
: matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.MatterGenerationPhase;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -84,6 +85,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
super(engineMantle, ReservedFlag.CARVED, 0);
}
@Override
public MatterGenerationPhase getGenerationPhase() {
return MatterGenerationPhase.TERRAIN;
}
@Override
public void hotload() {
super.hotload();
@@ -98,7 +104,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
if (!complex.allowsNewGenerationChunk(x, z)) {
if (!complex.allowsMantleChunkWrite(x, z)) {
return;
}
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.mantle.TerrainMatterView;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -203,7 +204,7 @@ public final class MantleHydrologyCaveVoxelView implements CaveVoxelView {
if (loadedChunks.add(chunkKey)) {
chunkLoader.accept(chunkX, chunkZ);
}
return mantle.get(position.x(), position.y(), position.z(), type);
return TerrainMatterView.get(mantle, position.x(), position.y(), position.z(), type);
}
private static int plannedSurfaceHeight(
@@ -12,6 +12,7 @@ import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.mantle.MatterGenerationPhase;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -60,6 +61,11 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY);
}
@Override
public MatterGenerationPhase getGenerationPhase() {
return MatterGenerationPhase.TERRAIN;
}
@Override
public MantleFlag[] getPrerequisiteFlags() {
return PREREQUISITES;
@@ -74,7 +80,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !complex.allowsNewGenerationChunk(chunkX, chunkZ)) {
if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)) {
return;
}
int minimumX = chunkX << 4;
@@ -218,6 +224,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
addAcceptedGuards(
caveCells,
cavePlans,
samples,
publishedCaveCells,
minimumX,
minimumZ,
@@ -529,6 +536,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
private static void addAcceptedGuards(
Map<CavePosition, PlannedCell> caveCells,
Collection<HydrologyCavePlan> cavePlans,
Map<Long, HydrologyColumnSample> samples,
Map<CavePosition, HydrologyCaveCell> published,
int minimumX,
int minimumZ,
@@ -550,6 +558,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
for (CavePosition position : guardPositions) {
PlannedCell owner = guardOwner(position, plan, caveCells);
if (owner == null) {
if (guardOutsidePublication(position, plan, samples)) {
continue;
}
throw new IllegalStateException("Accepted containment guard has no adjacent cave volume at "
+ position);
}
@@ -592,6 +603,34 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
return null;
}
private static boolean guardOutsidePublication(
CavePosition guard,
HydrologyCavePlan plan,
Map<Long, HydrologyColumnSample> samples
) {
boolean hasPlannedVolume = false;
for (int[] offset : NEIGHBORS) {
CavePosition position = guard.offset(-offset[0], -offset[1], -offset[2]);
HydrologyCaveAction action = plan.actions().get(position);
if (action == null || action == HydrologyCaveAction.SEAL_GUARD) {
continue;
}
hasPlannedVolume = true;
HydrologyColumnSample sample = samples.get(pack(position.x(), position.z()));
if (sample == null) {
continue;
}
for (HydrologyColumnLayer layer : sample.layers()) {
if (layer.feature().courseId() == plan.source().sourceId()
&& layer.channel() && layer.terrainOwned() && !layer.oceanApron()
&& isCaveLayer(layer)) {
return false;
}
}
}
return hasPlannedVolume;
}
private static boolean preconditionsHold(
Collection<HydrologyCavePlan> cavePlans,
CaveVoxelView caveView,
@@ -23,6 +23,9 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import java.util.Optional;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
@@ -771,12 +774,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
: writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class);
if (candidateY < 0
|| caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey)
|| !acceptsCaveAnchorFluid(
underwater,
hydrology == null ? cavern : hydrology.asCavern(),
hydrology,
candidateY,
getDimension().getCaveLavaHeight())) {
|| !acceptsCaveAnchorAt(candidateX, candidateY, candidateZ, underwater, cavern, hydrology)) {
continue;
}
return new CavePlacementAnchor(candidateX, candidateY, candidateZ);
@@ -784,6 +782,22 @@ public class MantleObjectComponent extends IrisMantleComponent {
return null;
}
private boolean acceptsCaveAnchorAt(int x, int y, int z, boolean underwater,
MatterCavern cavern, HydrologyCaveCell hydrology) {
Optional<TerrainBoundarySignature> resolved = getEngineMantle().getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return acceptsResolvedCaveAnchor(underwater,
resolved.get().geometry().voxelAt(y + getEngineMantle().getEngine().getMinHeight()));
}
return acceptsCaveAnchorFluid(underwater, hydrology == null ? cavern : hydrology.asCavern(),
hydrology, y, getDimension().getCaveLavaHeight());
}
static boolean acceptsResolvedCaveAnchor(boolean underwater, BoundaryColumnGeometry.Voxel voxel) {
return !voxel.protectedContent() && (voxel.phase() == BoundaryColumnGeometry.Phase.AIR
|| underwater && voxel.phase() == BoundaryColumnGeometry.Phase.FLUID);
}
static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) {
return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight);
}
@@ -140,12 +140,16 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override
public boolean isCarved(int x, int y, int z) {
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
Object block = overlay.get(new DataKey(x, y, z, PlatformBlockState.class));
if (block instanceof PlatformBlockState state && !state.isAir() && !state.isFluid()) {
return false;
}
Object hydrologyValue = overlay.get(new DataKey(x, y, z, HydrologyCaveCell.class));
if (hydrologyValue instanceof HydrologyCaveCell hydrology) {
return hydrology.carves();
}
MatterCavern cavern = getDataIfPresent(x, y, z, MatterCavern.class);
return cavern != null || writer.isPrerequisiteCarved(x, y, z);
Object cavern = overlay.get(new DataKey(x, y, z, MatterCavern.class));
return cavern == null ? writer.isPrerequisiteCarved(x, y, z) : cavern != CLEARED;
}
@Override
@@ -225,6 +229,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
carved = carved.clone();
}
for (int y = 0; y < cappedHeight; y++) {
Object block = overlay.get(new DataKey(x, y, z, PlatformBlockState.class));
if (block instanceof PlatformBlockState state && !state.isAir() && !state.isFluid()) {
carved[y] = 0;
continue;
}
DataKey hydrologyKey = new DataKey(x, y, z, HydrologyCaveCell.class);
Object hydrologyValue = overlay.get(hydrologyKey);
if (hydrologyValue instanceof HydrologyCaveCell hydrology) {
@@ -30,7 +30,7 @@ public class ModeEnclosure extends IrisEngineMode implements EngineMode {
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
registerTerrainStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
(x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c)
));
@@ -30,7 +30,7 @@ public class ModeIslands extends IrisEngineMode implements EngineMode {
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
registerTerrainStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
(x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c)
));
@@ -57,7 +57,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
if (shouldBypassMantleStages()) {
return;
}
generateMatter(
generateTerrainMatter(
x >> 4,
z >> 4,
m || getEngine().isStudio(),
@@ -101,18 +101,24 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
// Matter runs on the calling thread so its window fans out across the burst pool (a pool
// thread would run every chunk of the window inline) while biome and terrain run alongside.
registerStage(burstAround(
registerTerrainStage(burstAround(
sGenMatter,
sBiome,
sTerrain
));
registerStage(sCave);
registerStage(sPost);
registerStage(sFloatingTerrainSolid);
registerTerrainStage(sCave);
registerTerrainStage(sPost);
registerTerrainStage(sFloatingTerrainSolid);
registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k));
// Never burst these three: all of them write the same block hunk (and sDecorant reads
// the surface sInsertMatter writes), so parallel order is scheduler-dependent. The
// production path already runs them inline in this order; sequential registration
// makes studio (the only multicore path) match production and the goldenhash baseline.
registerStage((x, z, k, p, m, c) -> {
if (!shouldBypassMantleStages()) {
generateContentMatter(x >> 4, z >> 4, m || getEngine().isStudio(), c);
}
});
registerStage(sDeposit);
registerStage(sInsertMatter);
registerStage(sDecorant);
@@ -30,7 +30,7 @@ public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
registerStage(burst(
registerTerrainStage(burst(
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
(x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c)
));
@@ -25,6 +25,7 @@ import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.mantle.TerrainMatterView;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
@@ -55,6 +56,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import lombok.Data;
import art.arcane.iris.spi.PlatformBlockState;
import java.util.HashMap;
import java.util.Map;
public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState> {
@@ -129,14 +131,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
chunkBlockZ
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
TerrainMatterView.iterate(mantleChunk, MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
xx,
yy,
zz,
cavern,
dataIfPresent(mantleChunk, xx, yy, zz, HydrologyCaveCell.class)
));
mantleChunk.iterate(HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> {
TerrainMatterView.iterate(mantleChunk, HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> {
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
carveResolver.apply(xx, yy, zz, null, hydrology);
}
@@ -570,15 +572,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
int section = y >> 4;
if (y < 0 || !mantleChunk.exists(section)) {
return null;
}
Matter matter = mantleChunk.get(section);
if (matter == null || !matter.hasSlice(type)) {
return null;
}
return matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
return TerrainMatterView.get(mantleChunk, x, y, z, type);
}
private void processColumnFromMask(
@@ -802,17 +796,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
output.setRaw(rx, zone.ceiling, rz, AIR);
}
// Seed-derived and position-keyed, never Math.random(): these markers persist into the
// mantle and drive cave spawning, so the same seed must stamp the same markers on
// every run and every platform. Inline mix, no allocation on this hot path.
if (markerRoll(xx, zone.ceiling, zz, 0x9E3779B97F4A7C15L)) {
mantle.set(xx, zone.ceiling, zz, MarkerMatter.CAVE_CEILING);
}
if (markerRoll(xx, zone.floor, zz, 0xC2B2AE3D27D4EB4FL)) {
mantle.set(xx, zone.floor, zz, MarkerMatter.CAVE_FLOOR);
}
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
@@ -882,6 +865,58 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
}
public void decorateNaturalCaves(int blockX, int blockZ, Hunk<PlatformBlockState> output) {
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
MantleChunk<Matter> chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use();
IrisDimensionCarvingResolver.State resolver = new IrisDimensionCarvingResolver.State();
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
Map<String, IrisBiome> customBiomes = new HashMap<>();
try {
for (int localX = 0; localX < output.getWidth(); localX++) {
for (int localZ = 0; localZ < output.getDepth(); localZ++) {
int worldX = blockX + localX;
int worldZ = blockZ + localZ;
int floor = -1;
for (int y = 1; y < output.getHeight(); y++) {
PlatformBlockState state = output.getRaw(localX, y, localZ);
if (B.isSolid(state)) {
if (floor >= 0) {
CaveZone zone = new CaveZone();
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(getEngine())) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) {
mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR);
}
IrisBiome floorBiome = resolveCaveBoundaryBiome(chunk, localX, floor, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(chunk, localX, y - 1, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
decorateZone(output, zone, localX, localZ, worldX, worldZ,
floorBiome, ceilingBiome);
}
floor = -1;
}
} else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) {
floor = y;
}
}
}
}
} finally {
chunk.release();
}
}
private void decorateZone(Hunk<PlatformBlockState> output, CaveZone zone,
int rx, int rz, int xx, int zz, IrisBiome floorBiome, IrisBiome ceilingBiome) {
int maxY = output.getHeight();
IrisDecorator[] surfaceDecorators = floorBiome == null
? new IrisDecorator[0]
: floorBiome.getDecoratorBucket(IrisDecorationPart.NONE);
@@ -896,7 +931,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness());
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
}
private void normalizeCaveZoneWaterlogging(
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.object.IrisBiome;
@@ -32,6 +33,7 @@ import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisProceduralBlocks;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.data.HeightMap;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -68,17 +70,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
try {
for (IrisDepositGenerator k : getDimension().getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
}
for (IrisDepositGenerator k : region.getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
}
for (IrisDepositGenerator k : biome.getDeposits()) {
long finalSeed = seed * ++mask;
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
burst.queue(scopedDepositTask(
() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context), context));
}
} finally {
// complete() must run before release() even when queueing throws — already
@@ -91,6 +96,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
}
private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
IrisEngine.GenerationRuntimeBinding binding = generationEngine == null
? null : generationEngine.captureGenerationRuntimeBinding();
return () -> {
try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null
? null : generationEngine.openGenerationRuntimeScope(binding);
IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
task.run();
}
};
}
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<PlatformBlockState> data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) {
generate(k, chunk, data, rng, cx, cz, safe, null, context);
}
@@ -44,11 +44,13 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import java.util.Optional;
import java.util.IdentityHashMap;
@@ -294,7 +296,8 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
sample,
chunkHeight,
data,
context.getDimensionStackLayout(xf, zf)
context.getDimensionStackLayout(xf, zf),
context
);
}
}
@@ -388,10 +391,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
FloatingIslandSample sample,
int chunkHeight,
IrisData data,
DimensionStackLayout stackLayout
DimensionStackLayout stackLayout,
ChunkContext context
) {
try {
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, PlatformBiome> matterByEntry = new IdentityHashMap<>();
for (int k = 0; k <= sample.topIdx; k++) {
if (!sample.solidMask[k]) {
continue;
@@ -404,16 +408,27 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
MatterBiomeInject matter = matterByEntry.get(entry);
PlatformBiome matter = matterByEntry.get(entry);
if (matter == null) {
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
if (target == null) {
continue;
}
matter = createSkyBiomeMatter(target, wx, wz);
matter = createSkyBiome(target, wx, wz);
matterByEntry.put(entry, matter);
}
getEngine().getMantle().getMantle().set(wx, y, wz, matter);
PlatformBiome selected = matter;
TransitionGenerationPlan transition = context.getComplex().getTransitionGenerationPlan();
if (transition != null) {
Optional<String> historical = transition.historicalPhysicalBiomeKeyAt(wx, y + getEngine().getMinHeight(), wz);
if (historical.isPresent()) {
selected = IrisPlatforms.get().registries().biome(historical.get());
}
}
context.setNaturalBiome(wx & 15, y, wz & 15, selected);
if (!context.isSpeculativeTerrain()) {
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -424,16 +439,16 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
return stackLayout == null || !stackLayout.isHostFeatureProtectedY(y);
}
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
private PlatformBiome createSkyBiome(IrisBiome target, int wx, int wz) {
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
String resourceKey = getEngine().getData().customBiomeResourceKey(
getEngine().getDimension(),
custom
);
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(resourceKey));
return IrisPlatforms.get().registries().biome(resourceKey);
}
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
return IrisPlatforms.get().registries().biome(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz));
}
}
@@ -20,6 +20,9 @@ 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.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.iris.spi.PlatformBlockState;
import org.jetbrains.annotations.Nullable;
@@ -56,6 +59,11 @@ public interface IObjectPlacer {
return getFluidHeight();
}
int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight();
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return resolved.get().fluidHeight().isPresent()
? coordinateShift + resolved.get().fluidHeight().getAsInt() : -1;
}
return coordinateShift + (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(x, z)
);
@@ -29,7 +29,6 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
@@ -128,7 +127,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final boolean studio;
private final AtomicBoolean studioEntryBootstrapActive;
private final AtomicInteger a = new AtomicInteger(0);
private volatile long lastChunkGenTime = 0L;
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
private final CompletableFuture<Void> initialSpawnReady = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
@@ -168,7 +166,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.folder = new ReactiveFolder(
dataLocation,
(_a, _b, _c) -> hotloadFromWatcher(),
new KList<>(".iob", ".json"),
new KList<>(".iob", ".json", ".png"),
new KList<>(".iris"),
new KList<>()
);
@@ -390,6 +388,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private void setupEngine() {
lastMode = StudioMode.NORMAL;
lastJigsawStudioRequestId = null;
if (generationHistory != null) {
try {
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
targetCache.reset();
} catch (IOException failure) {
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
}
}
EngineTarget engineTarget = getTarget();
String packKey = engineTarget.getDimension().getLoadKey();
JigsawStudioActivation.Request request = studio
@@ -483,12 +489,16 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} catch (IOException failure) {
throw new IllegalStateException("Unable to load the active Iris generation transition.", failure);
}
return new IrisEngine(
IrisEngine createdEngine = new IrisEngine(
engineTarget,
mode,
generationHistory.paths().activationMantleRoot(active.activationId()),
epoch.kernelVersion(),
plan);
if (studio) {
createdEngine.configureStudioGenerationSource(dataLocation.toPath());
}
return createdEngine;
}
private static RuntimeException propagateEngineSetupFailure(Throwable failure) {
@@ -520,6 +530,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (engine != null) return engine.getTarget();
return targetCache.aquireOrThrow(() -> {
if (generationHistory != null) {
try {
return loadActiveGenerationHistoryTarget();
} catch (IOException failure) {
throw new IllegalStateException("Unable to load the active Iris generation pack.", failure);
}
}
IrisData data = IrisData.openRuntime(dataLocation);
data.dump();
data.clearLists();
@@ -577,7 +594,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
&& irisEngine.isGenerationCacheWarmPending()) {
return HOTLOAD_LOOP_DELAY_MS;
}
if (shouldThrottleHotload()) {
if (isMaintenanceActive()) {
return HOTLOAD_MAINTENANCE_DELAY_MS;
}
@@ -978,7 +995,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
try {
Engine engine = getEngine(world);
lastChunkGenTime = System.currentTimeMillis();
computeStudioGenerator();
TerrainChunk tc = TerrainChunk.create(d);
if (studioGenerator != null) {
@@ -1070,20 +1086,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
private boolean shouldThrottleHotload() {
if (isMaintenanceActive()) {
return true;
}
if (System.currentTimeMillis() - lastChunkGenTime < 2000L) {
return true;
}
World realWorld = BukkitWorldBinding.world(this.world);
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldIdentity(WorldIdentity.serialize(realWorld));
}
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
Engine currentEngine = getEngine(worldInfo);
@@ -20,12 +20,17 @@ package art.arcane.iris.util.common.director;
import art.arcane.volmlib.util.director.DirectorExecutorBase;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.common.plugin.VolmitSender;
import org.bukkit.entity.Player;
import java.io.File;
import java.util.Arrays;
public interface DirectorExecutor extends DirectorExecutorBase {
default VolmitSender sender() {
return DirectorContext.get();
@@ -42,11 +47,32 @@ public interface DirectorExecutor extends DirectorExecutorBase {
}
default IrisData data() {
var access = access();
if (access != null) {
return access.getData();
return authoringData(engine());
}
static IrisData authoringData(Engine engine) {
if (engine == null) {
return null;
}
return null;
if (engine.isStudio()) {
return IrisData.get(engine.getPackSource().toFile());
}
if (engine.getDimension() == null) {
return null;
}
String dimensionKey = engine.getDimension().getLoadKey();
IrisData matching = null;
for (File pack : PackDirectoryResolver.listVisiblePackDirectories(IrisPlatforms.get().packsFolder())) {
IrisData candidate = IrisData.get(pack);
if (!Arrays.asList(candidate.getDimensionLoader().getPossibleKeys()).contains(dimensionKey)) {
continue;
}
if (matching != null) {
return null;
}
matching = candidate;
}
return matching;
}
default Engine engine() {
@@ -19,9 +19,12 @@
package art.arcane.iris.util.common.director.context;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
public class BiomeContextHandler implements DirectorContextHandler<IrisBiome> {
@@ -33,7 +36,13 @@ public class BiomeContextHandler implements DirectorContextHandler<IrisBiome> {
if (sender.isPlayer()
&& IrisToolbelt.isIrisWorld(sender.player().getWorld())
&& IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) {
return EngineBukkitOps.getBiomeOrMantle(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation());
Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine();
IrisData source = DirectorExecutor.authoringData(engine);
if (source == null) {
return null;
}
IrisBiome biome = EngineBukkitOps.getBiomeOrMantle(engine, sender.player().getLocation());
return biome == null ? null : source.getBiomeLoader().load(biome.getLoadKey());
}
return null;
@@ -19,8 +19,11 @@
package art.arcane.iris.util.common.director.context;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
public class DimensionContextHandler implements DirectorContextHandler<IrisDimension> {
@@ -32,7 +35,9 @@ public class DimensionContextHandler implements DirectorContextHandler<IrisDimen
if (sender.isPlayer()
&& IrisToolbelt.isIrisWorld(sender.player().getWorld())
&& IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) {
return IrisToolbelt.access(sender.player().getWorld()).getEngine().getDimension();
Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine();
IrisData source = DirectorExecutor.authoringData(engine);
return source == null ? null : source.getDimensionLoader().load(engine.getDimension().getLoadKey());
}
return null;
@@ -19,10 +19,12 @@
package art.arcane.iris.util.common.director.context;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
public class GeneratorContextHandler implements DirectorContextHandler<IrisGenerator> {
@@ -37,7 +39,9 @@ public class GeneratorContextHandler implements DirectorContextHandler<IrisGener
&& IrisToolbelt.isIrisWorld(sender.player().getWorld())
&& IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) {
Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine();
return engine.getData().getGeneratorLoader().load(EngineBukkitOps.getBiome(engine, sender.player().getLocation()).getGenerators().getRandom().getGenerator());
IrisData source = DirectorExecutor.authoringData(engine);
return source == null ? null : source.getGeneratorLoader().load(
EngineBukkitOps.getBiome(engine, sender.player().getLocation()).getGenerators().getRandom().getGenerator());
}
return null;
@@ -19,9 +19,12 @@
package art.arcane.iris.util.common.director.context;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.common.director.DirectorContextHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
public class RegionContextHandler implements DirectorContextHandler<IrisRegion> {
@@ -33,7 +36,13 @@ public class RegionContextHandler implements DirectorContextHandler<IrisRegion>
if (sender.isPlayer()
&& IrisToolbelt.isIrisWorld(sender.player().getWorld())
&& IrisToolbelt.access(sender.player().getWorld()).getEngine() != null) {
return EngineBukkitOps.getRegion(IrisToolbelt.access(sender.player().getWorld()).getEngine(), sender.player().getLocation());
Engine engine = IrisToolbelt.access(sender.player().getWorld()).getEngine();
IrisData source = DirectorExecutor.authoringData(engine);
if (source == null) {
return null;
}
IrisRegion region = EngineBukkitOps.getRegion(engine, sender.player().getLocation());
return region == null ? null : source.getRegionLoader().load(region.getLoadKey());
}
return null;
@@ -10,6 +10,8 @@ import art.arcane.iris.engine.object.FloatingIslandBoundarySampler;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.common.parallel.MultiBurst;
import java.util.ArrayList;
@@ -17,6 +19,9 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
public class ChunkContext {
private boolean naturalTerrain = true;
private boolean speculativeTerrain;
private Hunk<PlatformBiome> terrainBiomes;
private final int x;
private final int z;
private final IrisComplex complex;
@@ -66,7 +71,7 @@ public class ChunkContext {
this.z = z;
this.complex = complex;
this.generationSessionId = generationSessionId;
this.height = new ChunkedDoubleDataCache(complex.getHeightStream(), x, z, cache);
this.height = new ChunkedDoubleDataCache(complex.getRawHeightStream(), x, z, cache);
this.roundedHeight = new int[cache ? 256 : 0];
this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache);
this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache);
@@ -105,7 +110,7 @@ public class ChunkContext {
fillTasks.add(new PrefillFillTask(cave));
}
if (!shouldPrefillAsync(fillTasks.size())) {
if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) {
for (Runnable fillTask : fillTasks) {
fillTask.run();
}
@@ -124,14 +129,40 @@ public class ChunkContext {
}
}
static boolean shouldPrefillAsync(int fillTaskCount) {
if (fillTaskCount <= 1 || !IrisPlatforms.isBound()) {
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, int fillTaskCount) {
if (prefillPlan == PrefillPlan.NATURAL_TERRAIN || fillTaskCount <= 1 || !IrisPlatforms.isBound()) {
return false;
}
return !MultiBurst.burst.ownsCurrentThread();
}
public void setTerrainBiomeOutput(Hunk<PlatformBiome> terrainBiomes) {
this.terrainBiomes = terrainBiomes;
}
public void setNaturalBiome(int localX, int y, int localZ, PlatformBiome biome) {
if (terrainBiomes != null) {
terrainBiomes.setRaw(localX, y, localZ, biome);
}
}
public boolean isSpeculativeTerrain() {
return speculativeTerrain;
}
public void beginSpeculativeTerrain() {
speculativeTerrain = true;
}
public boolean isNaturalTerrain() {
return naturalTerrain;
}
public void beginContent() {
naturalTerrain = false;
}
public int getX() {
return x;
}
@@ -156,6 +187,13 @@ public class ChunkContext {
return height;
}
public void setTerrainHeight(int localX, int localZ, int terrainHeight) {
height.setDouble(localX, localZ, terrainHeight);
if (roundedHeight.length > 0) {
roundedHeight[(localZ << 4) + localX] = terrainHeight;
}
}
public int getRoundedHeight(int x, int z) {
if (roundedHeight.length == 0) {
return (int) Math.round(height.getDouble(x, z));
@@ -200,6 +238,7 @@ public class ChunkContext {
public enum PrefillPlan {
ALL(true, true, true, true, true, true),
NO_CAVE(true, true, false, true, true, true),
NATURAL_TERRAIN(true, true, false, true, true, true),
NONE(false, false, false, false, false, false);
private final boolean height;
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff. Show more