mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
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();
|
||||
}
|
||||
|
||||
+14
-54
@@ -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);
|
||||
}
|
||||
}
|
||||
+72
@@ -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) {
|
||||
}
|
||||
}
|
||||
+34
-11
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+59
-10
@@ -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) {
|
||||
|
||||
+157
@@ -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");
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+15
-15
@@ -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 + "."
|
||||
|
||||
+146
-104
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+20
-3
@@ -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) {
|
||||
|
||||
+70
-146
@@ -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() {
|
||||
|
||||
+85
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -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();
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+40
-1
@@ -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,
|
||||
|
||||
+20
-6
@@ -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);
|
||||
}
|
||||
|
||||
+13
-4
@@ -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);
|
||||
}
|
||||
|
||||
+25
-10
@@ -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() {
|
||||
|
||||
+10
-1
@@ -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;
|
||||
|
||||
+6
-1
@@ -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;
|
||||
|
||||
+5
-1
@@ -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;
|
||||
|
||||
+10
-1
@@ -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
Reference in new issue
Block a user