Merged generation history

- Immutable generation epochs now coexist with dimension stacks
- Historical terrain, biomes, structures, and locate behavior remain stable across updates
- Versioned generation kernels preserve the combined runtime contract
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-04 15:06:55 -04:00
commit 56b5f4dd0b
263 files changed
+39624 -1849

No files matched your search

@@ -9,8 +9,14 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
@@ -40,13 +46,14 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 2;
private static final int INPUT_FINGERPRINT_SCHEMA = 3;
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/._-]+");
@@ -90,6 +97,7 @@ public final class IrisDatapackCompiler {
) throws IOException {
LinkedHashMap<Path, File> roots = new LinkedHashMap<>();
collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack);
collectCanonicalWorldPackRoot(serverRoot, roots, validateWholePack);
collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack);
return new ArrayList<>(roots.values());
}
@@ -179,14 +187,18 @@ public final class IrisDatapackCompiler {
Objects.requireNonNull(packRoots, "packRoots");
Objects.requireNonNull(fixer, "fixer");
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Set<String> installedBiomeKeys = new LinkedHashSet<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
for (File packRoot : packRoots) {
PackDirectoryResolver.requireSafePackTree(packRoot);
if (!hasDimensions(packRoot.toPath())) {
continue;
}
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot);
try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) {
@@ -199,14 +211,16 @@ public final class IrisDatapackCompiler {
}
collectRegistryRequirements(
requirements,
installedBiomeKeys,
biomeTags,
dimension,
fixer);
fixer,
aliasContext);
}
} finally {
data.close();
}
}
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
@@ -216,23 +230,35 @@ public final class IrisDatapackCompiler {
) throws IOException {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
IrisData data = Objects.requireNonNull(
requiredDimension.getLoader(),
"Iris dimension loader"
);
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
BiomeAliasContext aliasContext = BiomeAliasContext.contentAddressedOnly();
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
biomeTags,
requiredDimension,
requiredFixer);
requiredFixer,
aliasContext);
collectDimensionStackRegistryRequirements(
requirements,
biomeTags,
requiredDimension,
requiredFixer);
requiredFixer,
aliasContext);
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
private static void collectDimensionStackRegistryRequirements(
Map<String, String> requirements,
Map<String, Set<String>> biomeTags,
IrisDimension hostDimension,
IDataFixer fixer
IDataFixer fixer,
BiomeAliasContext aliasContext
) throws IOException {
if (!hostDimension.hasDimensionStack()) {
return;
@@ -255,21 +281,31 @@ public final class IrisDatapackCompiler {
}
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
biomeTags,
sourceDimension,
fixer);
fixer,
aliasContext);
}
}
private static void collectRegistryRequirements(
Map<String, String> requirements,
Set<String> installedBiomeKeys,
Map<String, Set<String>> biomeTags,
IrisDimension dimension,
IDataFixer fixer
) {
requirements.put(
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
IDataFixer fixer,
BiomeAliasContext aliasContext
) throws IOException {
String dimensionTypeResourceKey = "iris:" + dimension.getDimensionTypeKey();
String dimensionTypeSource = aliasContext.generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
dimensionTypeResourceKey,
dimension.getDimensionType().toJson(fixer),
fixer
);
putRegistryRequirement(
requirements,
"dimension_type/" + dimensionTypeResourceKey,
fingerprintContent(dimensionTypeSource));
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
@@ -281,13 +317,25 @@ public final class IrisDatapackCompiler {
if (customBiome == null) {
continue;
}
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
if (!installedBiomeKeys.add(biomeKey)) {
continue;
String json = customBiome.generateJson(fixer, contentGate);
String currentPhysicalKey = GenerationRegistryContractFactory.customBiomeResourceKey(
dimension.getLoadKey(),
customBiome,
contentGate
);
String physicalKey = aliasContext.physicalResourceKey(
dimension,
biome,
customBiome,
currentPhysicalKey
);
LinkedHashSet<String> resourceKeys = new LinkedHashSet<>();
resourceKeys.add(physicalKey);
Collection<String> aliases = aliasContext.aliases(dimension, biome, customBiome, physicalKey);
if (!aliases.isEmpty() && physicalKey.equals(dimension.getLoader()
.customBiomeResourceKey(dimension, customBiome.getId()))) {
resourceKeys.addAll(aliases);
}
requirements.put(
"worldgen/biome/" + biomeKey,
fingerprintContent(customBiome.generateJson(fixer, contentGate)));
TreeSet<String> tags = new TreeSet<>();
for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
String normalizedTag = normalizeRegistryKey(tag);
@@ -295,13 +343,48 @@ public final class IrisDatapackCompiler {
tags.add(normalizedTag);
}
}
requirements.put(
"worldgen/biome_tags/" + biomeKey,
fingerprintContent(String.join("\n", tags)));
for (String resourceKey : resourceKeys) {
String emittedJson = aliasContext.generatedSource(
GenerationRegistryContractFactory.BIOME_REGISTRY,
resourceKey,
json,
fixer
);
putRegistryRequirement(
requirements,
"worldgen/biome/" + resourceKey,
fingerprintContent(emittedJson)
);
biomeTags.computeIfAbsent(resourceKey, ignored -> new TreeSet<>()).addAll(tags);
}
}
}
}
private static void addBiomeTagRequirements(
Map<String, String> requirements,
Map<String, Set<String>> biomeTags
) throws IOException {
for (Map.Entry<String, Set<String>> entry : biomeTags.entrySet()) {
putRegistryRequirement(
requirements,
"worldgen/biome_tags/" + entry.getKey(),
fingerprintContent(String.join("\n", entry.getValue()))
);
}
}
private static void putRegistryRequirement(
Map<String, String> requirements,
String resourceKey,
String contentFingerprint
) throws IOException {
String previous = requirements.putIfAbsent(resourceKey, contentFingerprint);
if (previous != null && !previous.equals(contentFingerprint)) {
throw new IOException("Conflicting Iris datapack registry resource '" + resourceKey + "'.");
}
}
private static String normalizeRegistryKey(String key) {
if (key == null || key.isBlank()) {
return null;
@@ -359,8 +442,12 @@ public final class IrisDatapackCompiler {
if (!hasDimensions(packRoot.toPath())) {
continue;
}
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot);
try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) {
@@ -379,11 +466,22 @@ public final class IrisDatapackCompiler {
.add(new DimensionCandidate(
dimension,
packRoot.toPath().toAbsolutePath().normalize(),
dimension.getDimensionType().toJson(fixer)
aliasContext.generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
"iris:" + dimension.getDimensionTypeKey(),
dimension.getDimensionType().toJson(fixer),
fixer
)
));
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
dimension.installDimensionType(fixer, datapackRoots);
dimension.installBiomes(
fixer,
dimension::getLoader,
datapackRoots,
seenBiomes,
aliasContext
);
dimension.installDimensionType(fixer, datapackRoots, aliasContext);
installedDimensions++;
dimensionCount++;
}
@@ -501,6 +599,23 @@ public final class IrisDatapackCompiler {
}
}
private static void collectCanonicalWorldPackRoot(
Path worldRoot,
Map<Path, File> roots,
boolean validateWholePack
) throws IOException {
if (Files.isSymbolicLink(worldRoot)
|| !Files.isDirectory(worldRoot, LinkOption.NOFOLLOW_LINKS)) {
return;
}
List<Path> candidates = new ArrayList<>();
collectWorldRootPackRoots(worldRoot, candidates);
candidates.sort(Comparator.comparing(Path::toString));
for (Path candidate : candidates) {
addPackRoot(candidate, roots, validateWholePack);
}
}
private static void collectNamespaceWorldPackRoots(
Path namespace,
List<Path> candidates
@@ -510,18 +625,12 @@ public final class IrisDatapackCompiler {
public FileVisitResult preVisitDirectory(
Path directory,
BasicFileAttributes attributes
) {
) throws IOException {
if (!directory.equals(namespace)
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
Path irisRoot = directory.resolve("iris");
Path candidate = irisRoot.resolve("pack");
if (!Files.isSymbolicLink(irisRoot)
&& !Files.isSymbolicLink(candidate)
&& Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS)
&& hasDimensions(candidate)) {
candidates.add(candidate);
if (collectWorldRootPackRoots(directory, candidates)) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
@@ -529,6 +638,35 @@ public final class IrisDatapackCompiler {
});
}
private static boolean collectWorldRootPackRoots(
Path worldRoot,
List<Path> candidates
) throws IOException {
Path irisRoot = worldRoot.resolve("iris");
Path generationRoot = irisRoot.resolve("generation");
if (!Files.isSymbolicLink(irisRoot)
&& (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);
}
}
return true;
}
Path legacyPack = irisRoot.resolve("pack");
if (!Files.isSymbolicLink(irisRoot)
&& !Files.isSymbolicLink(legacyPack)
&& Files.isDirectory(legacyPack, LinkOption.NOFOLLOW_LINKS)
&& hasDimensions(legacyPack)) {
candidates.add(legacyPack);
return true;
}
return false;
}
private static List<Path> visibleDirectories(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) {
return entries
@@ -562,6 +700,43 @@ public final class IrisDatapackCompiler {
roots.putIfAbsent(identity, normalized.toFile());
}
private static BiomeAliasContext biomeAliasContext(Path packRoot) throws IOException {
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
Path rootParent = normalizedRoot.getParent();
if (rootParent == null || !"pack".equals(normalizedRoot.getFileName().toString())) {
return BiomeAliasContext.contentAddressedOnly();
}
if ("iris".equals(rootParent.getFileName().toString())) {
return BiomeAliasContext.legacyPack();
}
Path epochsRoot = rootParent.getParent();
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
if (epochsRoot == null
|| generationRoot == null
|| irisRoot == null
|| dimensionRoot == null
|| !"epochs".equals(epochsRoot.getFileName().toString())
|| !"generation".equals(generationRoot.getFileName().toString())
|| !"iris".equals(irisRoot.getFileName().toString())) {
return BiomeAliasContext.contentAddressedOnly();
}
String epochId = rootParent.getFileName().toString();
GenerationHistory history = GenerationHistory.open(dimensionRoot);
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
() -> new IOException("Retained Iris pack is not referenced by its generation history: "
+ normalizedRoot)
);
Path expectedRoot = history.paths().packRoot(epochId).toAbsolutePath().normalize();
if (!expectedRoot.equals(normalizedRoot)) {
throw new IOException("Retained Iris pack does not match its generation history: " + normalizedRoot);
}
return BiomeAliasContext.retained(epoch.registryContract());
}
private static boolean hasDimensions(Path root) {
Path dimensions = root.resolve("dimensions");
if (Files.isSymbolicLink(dimensions)
@@ -704,6 +879,75 @@ public final class IrisDatapackCompiler {
) {
}
private record BiomeAliasContext(
boolean retainAllLegacyAliases,
GenerationRegistryContract retainedContract
) implements IrisCustomBiomeAliasResolver {
private static BiomeAliasContext contentAddressedOnly() {
return new BiomeAliasContext(false, null);
}
private static BiomeAliasContext legacyPack() {
return new BiomeAliasContext(true, null);
}
private static BiomeAliasContext retained(GenerationRegistryContract registryContract) {
return new BiomeAliasContext(
false,
Objects.requireNonNull(registryContract, "registryContract")
);
}
@Override
public String physicalResourceKey(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String currentPhysicalResourceKey
) throws IOException {
if (retainedContract == null) {
return currentPhysicalResourceKey;
}
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
}
@Override
public Collection<String> aliases(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String physicalResourceKey
) {
String alias = dimension.getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
if (retainAllLegacyAliases) {
return List.of(alias);
}
if (retainedContract == null) {
return List.of();
}
PhysicalResourceKey key = new PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
alias
);
return retainedContract.definitions().containsKey(key) ? List.of(alias) : List.of();
}
@Override
public String generatedSource(
String registryKey,
String resourceKey,
String currentSource,
IDataFixer fixer
) throws IOException {
if (retainedContract == null) {
return currentSource;
}
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
return GenerationRegistryContractFactory.renderGeneratedSource(retainedContract, key, fixer);
}
}
public static final class DimensionHeight {
private final IDataFixer fixer;
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
@@ -349,6 +349,11 @@ public class IrisSettings {
public static class IrisSettingsGenerator {
public String defaultWorldType = "overworld";
public boolean preventLeafDecay = true;
public int generationTransitionWidthBlocks = 256;
public int getGenerationTransitionWidthBlocks() {
return Math.max(16, Math.min(generationTransitionWidthBlocks, 8_192));
}
}
@Data
@@ -1,5 +1,6 @@
package art.arcane.iris.core;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@@ -404,10 +405,6 @@ public final class IrisWorldStorage {
return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key));
}
public static File packRoot(NamespacedKey key) {
return new File(dimensionRoot(key), "iris/pack");
}
public static File requireFrozenDimensionRoot(
File worldContainer,
File levelRoot,
@@ -485,26 +482,28 @@ public final class IrisWorldStorage {
return configuredRoot;
}
public static File requireFrozenPackRoot(File dimensionRoot) {
public static File requireActiveGenerationPackRoot(File dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
Path irisRoot = root.resolve("iris");
Path packRoot = irisRoot.resolve("pack");
for (Path path : new Path[]{root, irisRoot, packRoot}) {
if (Files.isSymbolicLink(path)) {
throw new IllegalStateException("Frozen Iris pack path contains a symbolic link: " + path);
}
if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)
&& !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack path is not a directory: " + path);
}
try {
return GenerationHistory.open(root).activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
if (!Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack snapshot is missing: " + packRoot);
}
public static File requireActiveGenerationPackRoot(File dimensionRoot, long expectedWorldSeed) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
try {
return GenerationHistory.open(root, expectedWorldSeed).activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
return packRoot.toFile();
}
private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
@@ -367,7 +367,7 @@ public class IrisWorlds {
configuredWorldName,
worldKey
);
return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot);
return dimensionRoot.map(IrisWorldStorage::requireActiveGenerationPackRoot);
}
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) {
@@ -376,7 +376,7 @@ public class IrisWorlds {
}
IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s",
worldIdentity, failure.getMessage());
IrisLogging.error("Restore its iris/pack snapshot, or drop the world with /iris remove world=%s delete=true.",
IrisLogging.error("Restore its iris/generation history, or drop the world with /iris remove world=%s delete=true.",
worldIdentity);
}
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
keys.add(dimension.getCustomBiomeKey(j.getId()));
keys.add(dimension.getLoader().customBiomeResourceKey(dimension, j));
}
}
}
@@ -3,6 +3,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
@@ -229,8 +230,20 @@ public final class BukkitWorldConfiguration {
}
Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize();
try {
IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
} catch (IllegalStateException unusablePack) {
GenerationHistoryPaths historyPaths = GenerationHistoryPaths.forDimension(resolvedRoot);
boolean generationHistoryExists = Files.exists(
historyPaths.generationRoot(),
LinkOption.NOFOLLOW_LINKS
) || Files.isSymbolicLink(historyPaths.generationRoot());
boolean adoptableLegacyPack = !generationHistoryExists
&& Files.isDirectory(historyPaths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS)
&& !Files.isSymbolicLink(historyPaths.legacyPackRoot());
if (adoptableLegacyPack) {
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
IrisWorldStorageState.PRESENT, null);
}
// Only a folder that owns something can be corrupted by a vanilla generator writing into it.
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
hasWorldData(resolvedRoot)
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.IOException;
@@ -94,7 +95,7 @@ public final class WorldReplacementFilesystem {
State state = inspect(requiredPaths);
if (state.stagePresent()) {
requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
if (originalTargetPresent) {
Path retainedWorld = state.targetPresent()
? requiredPaths.target()
@@ -118,7 +119,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement backup state does not match the original target state.");
}
requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
move(requiredPaths.stage(), requiredPaths.target());
state = inspect(requiredPaths);
}
@@ -130,11 +131,11 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement publication lost or unexpectedly created its backup.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
if (originalTargetPresent) {
preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target());
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
}
@@ -224,7 +225,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Committed replacement storage does not contain one exact target directory.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
public static void validatePublishedTarget(
@@ -240,7 +241,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Published replacement storage does not match its retained-world contract.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
public static String fingerprintPack(Path packRoot) throws IOException {
@@ -279,6 +280,10 @@ public final class WorldReplacementFilesystem {
return HexFormat.of().formatHex(digest.digest());
}
private static Path activePackRoot(Path worldRoot) throws IOException {
return GenerationHistory.open(worldRoot).activePackRoot();
}
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
Path relative = root.relativize(path);
return new FingerprintEntry(path, relative, relative.toString());
@@ -39,9 +39,14 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.core.structure.authoring.StructureRecoveryResult;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
@@ -74,6 +79,7 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.context.IrisContext;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
@@ -87,7 +93,9 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -137,6 +145,17 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>();
private final transient Map<IrisDimension, Map<String, String>> dimensionCustomBiomeResourceKeys =
Collections.synchronizedMap(new IdentityHashMap<>());
private transient volatile String generationPackFingerprint;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContract generationRegistryContract;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
private transient volatile Engine soleEngine;
private IrisData(File dataFolder) {
@@ -170,6 +189,90 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return Optional.ofNullable(dataLoaders.get(dataFolder));
}
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
if (resolver != null) {
try {
return resolver.resolve(
requiredDimension.getLoadKey(),
requiredBiome
);
} catch (IOException failure) {
throw new IllegalStateException("Immutable Iris epoch has no registry mapping for custom biome '"
+ requiredDimension.getLoadKey() + ":" + requiredBiome.getId() + "'.", failure);
}
}
return GenerationRegistryContractFactory.customBiomeResourceKey(
requiredDimension.getLoadKey(),
requiredBiome,
getContentGate()
);
}
public String customBiomeResourceKey(IrisDimension dimension, String customBiomeId) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
String logicalKey = requiredDimension.getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + Objects.requireNonNull(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT);
String physicalKey = customBiomeResourceKeys(requiredDimension).get(logicalKey);
if (physicalKey == null) {
throw new IllegalArgumentException("Unknown custom biome '" + logicalKey + "'.");
}
return physicalKey;
}
public String physicalBiomeResourceKey(IrisDimension dimension, String resourceKey) {
if (resourceKey == null || resourceKey.isBlank()) {
return resourceKey;
}
String normalizedKey = resourceKey.trim().toLowerCase(Locale.ROOT);
if (!normalizedKey.contains(":")) {
normalizedKey = "minecraft:" + normalizedKey;
}
return customBiomeResourceKeys(Objects.requireNonNull(dimension, "dimension"))
.getOrDefault(normalizedKey, normalizedKey);
}
public String generationPackFingerprint() {
String fingerprint = generationPackFingerprint;
if (fingerprint != null) {
return fingerprint;
}
synchronized (this) {
fingerprint = generationPackFingerprint;
if (fingerprint != null) {
return fingerprint;
}
try {
fingerprint = GenerationPackFingerprint.compute(
dataFolder.toPath(),
GenerationPackFingerprint.CURRENT_VERSION
);
} catch (IOException failure) {
throw new IllegalStateException("Unable to fingerprint Iris generation pack at "
+ dataFolder + ".", failure);
}
generationPackFingerprint = fingerprint;
return fingerprint;
}
}
public synchronized void bindGenerationRegistryContract(GenerationRegistryContract contract) {
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
throw new IllegalStateException("Iris data is already bound to another generation registry contract.");
}
generationRegistryContract = required;
customBiomeResourceResolver = new GenerationRegistryContractFactory.CustomBiomeResourceResolver(
required,
PlatformGenerationRegistry::contentAddressedCustomBiomeResourceKey
);
synchronized (dimensionCustomBiomeResourceKeys) {
dimensionCustomBiomeResourceKeys.clear();
}
}
public static boolean invalidateLoadedStructureResources(File dataFolder) {
Path requested = dataFolderIdentity(Objects.requireNonNull(
dataFolder,
@@ -273,7 +376,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
try {
if (nearest != null) {
T t = nearest.load(type, key, false);
if (t != null) {
if (t != null || nearest.generationRegistryContract != null) {
return t;
}
}
@@ -516,6 +619,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate(this);
contentGate = null;
generationPackFingerprint = null;
synchronized (dimensionCustomBiomeResourceKeys) {
dimensionCustomBiomeResourceKeys.clear();
}
compatReport.clear();
closed = false;
possibleSnippets = new KMap<>();
@@ -831,4 +938,34 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
}
private Map<String, String> customBiomeResourceKeys(IrisDimension dimension) {
synchronized (dimensionCustomBiomeResourceKeys) {
Map<String, String> cached = dimensionCustomBiomeResourceKeys.get(dimension);
if (cached != null) {
return cached;
}
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
Map<String, String> resolved = new LinkedHashMap<>();
List<IrisBiome> registryBiomes = new ArrayList<>(dimension.getAllBiomes(() -> this));
registryBiomes.addAll(dimension.getReachableBiomes(() -> this));
for (IrisBiome biome : registryBiomes) {
if (biome == null || !biome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : biome.getCustomDerivitives()) {
if (customBiome == null || customBiome.getId() == null || customBiome.getId().isBlank()) {
continue;
}
String logicalKey = namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT);
String physicalKey = customBiomeResourceKey(dimension, customBiome);
resolved.merge(logicalKey, physicalKey,
(first, second) -> first.compareTo(second) <= 0 ? first : second);
}
}
Map<String, String> immutable = Map.copyOf(resolved);
dimensionCustomBiomeResourceKeys.put(dimension, immutable);
return immutable;
}
}
}
@@ -172,15 +172,13 @@ public final class BukkitRuntimeMessages {
+ " (dimension blockFallbacks, block backup)."
);
public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of(
"iris.bukkit.runtime.commanddeveloper.update_world_warning",
C.RED + "You should always make a backup before using this",
C.YELLOW + "Issues caused by this can be, but are not limited to:",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
C.YELLOW + " - Structures not spawning again when regenerating",
C.YELLOW + " - Caves not lining up",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning",
C.RED + "Back up the complete world before staging this update.",
C.YELLOW + "Existing chunks will not be regenerated or rewritten.",
C.YELLOW + "New chunks use the staged pack after restart and blend across the frozen boundary.",
C.YELLOW + "The world seed, height, environment, and dimension type cannot change.",
C.YELLOW + "Every backup must retain the complete iris/generation directory.",
C.RED + "To stage the update and request a clean restart:",
C.RED + "/iris developer update-world {world} {pack} confirm=true"
);
public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of(
@@ -60,9 +60,9 @@ public final class DirectorCommandMessages {
"iris.director.commanddeveloper.param.center_chunk_z",
"Center chunk Z"
);
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE = TextKey.of(
"iris.director.commanddeveloper.director.update_pack_world_unsafe",
"Update the pack of a world (UNSAFE!)"
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE = TextKey.of(
"iris.director.commanddeveloper.director.stage_world_generation_update",
"Stage a world generation update"
);
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of(
"iris.director.commanddeveloper.param.world_update",
@@ -889,7 +889,7 @@ public final class DirectorCommandMessages {
COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z,
COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE,
COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE,
COMMAND_DEVELOPER_PARAM_WORLD_UPDATE,
COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD,
COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST,
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
@@ -57,9 +58,14 @@ import java.awt.Color;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public interface INMSBinding {
default PlatformGenerationRegistry generationRegistry() {
throw new UnsupportedOperationException(
"The active NMS binding does not expose generation registry definitions."
);
}
boolean hasTile(Material material);
boolean hasTile(Location l);
@@ -273,8 +279,18 @@ public interface INMSBinding {
default void uninjectBukkit() {
}
default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
return true;
default void deferPluginClassLoaderClose() {
}
default void releasePluginClassLoaderClose() {
}
default boolean isServerStopping() {
return false;
}
default ServerShutdownBoundary createServerShutdownBoundary() {
return new ServerShutdownBoundary(() -> true, Thread.currentThread());
}
KMap<Material, List<BlockProperty>> getBlockProperties();
@@ -7,33 +7,32 @@ import java.util.function.BooleanSupplier;
public final class ServerShutdownBoundary {
private static final long MAX_JOIN_SLICE_MILLIS = 1000L;
private ServerShutdownBoundary() {
private final BooleanSupplier boundaryReached;
private final Thread serverThread;
public ServerShutdownBoundary(BooleanSupplier boundaryReached, Thread serverThread) {
this.boundaryReached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
this.serverThread = Objects.requireNonNull(serverThread, "Server thread");
this.boundaryReached.getAsBoolean();
}
public static boolean await(
BooleanSupplier boundaryReached,
Thread serverThread,
long timeout,
TimeUnit unit
) {
BooleanSupplier reached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
Thread activeServerThread = Objects.requireNonNull(serverThread, "Server thread");
public boolean await(long timeout, TimeUnit unit) {
TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit");
if (reached.getAsBoolean()) {
if (boundaryReached.getAsBoolean()) {
return true;
}
if (activeServerThread == Thread.currentThread()) {
if (serverThread == Thread.currentThread()) {
return false;
}
long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout));
long started = System.nanoTime();
boolean interrupted = false;
while (!reached.getAsBoolean()) {
while (!boundaryReached.getAsBoolean()) {
long remaining = timeoutNanos - (System.nanoTime() - started);
if (remaining <= 0L || !activeServerThread.isAlive()) {
if (remaining <= 0L || !serverThread.isAlive()) {
restoreInterrupt(interrupted);
return reached.getAsBoolean();
return boundaryReached.getAsBoolean();
}
long joinMillis = Math.max(
@@ -41,7 +40,7 @@ public final class ServerShutdownBoundary {
Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining))
);
try {
activeServerThread.join(joinMillis);
serverThread.join(joinMillis);
} catch (InterruptedException e) {
interrupted = true;
}
@@ -7,6 +7,7 @@ import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.message.Message;
import java.util.List;
@@ -21,14 +22,15 @@ public class LogFilterSVC implements IrisService, Filter {
// so a mutable static here was both a CME hazard and grew by three entries per enable.
private static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
private boolean installed = false;
private LoggerConfig installedLogger;
public void onEnable() {
if (installed) {
if (installedLogger != null) {
return;
}
((Logger) LogManager.getRootLogger()).addFilter(this);
installed = true;
LoggerConfig logger = ((Logger) LogManager.getRootLogger()).get();
logger.addFilter(this);
installedLogger = logger;
}
public void initialize() {
@@ -41,11 +43,12 @@ public class LogFilterSVC implements IrisService, Filter {
}
public void onDisable() {
if (!installed) {
LoggerConfig logger = installedLogger;
if (logger == null) {
return;
}
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
installed = false;
logger.removeFilter(this);
installedLogger = null;
}
public boolean isStarted() {
@@ -69,10 +69,27 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
public void dereference() {
IrisData.dereference();
threads.removeIf((i) -> !i.isAlive());
services.removeIf(ExecutorService::isShutdown);
services.removeIf(ExecutorService::isTerminated);
updateCaches();
}
public boolean hasActiveResources() {
if (dereferencer != null && dereferencer.isAlive()) {
return true;
}
for (Thread thread : threads) {
if (thread.isAlive()) {
return true;
}
}
for (ExecutorService executor : services) {
if (!executor.isTerminated()) {
return true;
}
}
return false;
}
@Override
public void onEnable() {
/*
@@ -19,6 +19,7 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
@@ -46,6 +47,14 @@ import art.arcane.iris.core.runtime.WorldDeletionQueue;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.history.GenerationActivation;
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.GenerationHistoryPaths;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
@@ -166,18 +175,197 @@ public class StudioSVC implements IrisService {
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
}
public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), false);
public IrisDimension installIntoWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed
) {
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, false);
}
public IrisDimension replaceIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), true);
public IrisDimension replaceIntoWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed
) {
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, true);
}
public IrisDimension installIntoTransientWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot
) {
return installIntoDirectory(sender, dimension, new File(dimensionRoot, "iris/pack"), false);
}
public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, folder, true);
}
private IrisDimension publishGenerationHistory(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed,
boolean stageUpdate
) {
if (J.isPrimaryThread()) {
sender.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD
));
return null;
}
String dimensionKey = dimension.getLoadKey();
try {
Path source = resolveSafePackSource(dimension.getLoader().getDataFolder());
GenerationCandidate candidate = captureGenerationCandidate(source, dimensionKey);
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
if (!stageUpdate && !Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
Path parent = Objects.requireNonNull(root.getParent(), "dimensionRoot parent");
Files.createDirectories(parent);
Files.createDirectory(root);
}
GenerationHistory history;
long previousActivationId = 0L;
boolean restartRequired = false;
if (stageUpdate) {
history = openOrAdoptGenerationHistory(root, dimensionKey, worldSeed);
previousActivationId = history.activeActivation().activationId();
GenerationActivation activation = history.stageUpdate(
source,
candidate.packFingerprint(),
candidate.dimensionContract(),
candidate.registryContract(),
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
restartRequired = activation.activationId() != previousActivationId;
} else {
history = GenerationHistory.create(
root,
source,
candidate.packFingerprint(),
worldSeed,
candidate.dimensionContract(),
candidate.registryContract()
);
}
Path installedPack = stageUpdate && history.pendingActivation().isPresent()
? history.packRoot(history.pendingActivation().orElseThrow().activationId())
: history.activePackRoot();
IrisDimension installed = loadInstalledDimension(installedPack, dimensionKey);
if (restartRequired) {
ServerConfigurator.restart("An Iris generation epoch update is pending activation.");
}
return installed;
} catch (Throwable failure) {
IrisLogging.reportError("Failed to publish generation history for dimension '"
+ dimensionKey + "' into " + dimensionRoot.getPath(), failure);
sender.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED,
MessageArgument.untrusted("dimension", dimensionKey),
MessageArgument.untrusted("error", errorDetail(failure))
));
return null;
}
}
private static GenerationHistory openOrAdoptGenerationHistory(
Path dimensionRoot,
String dimensionKey,
long worldSeed
) throws IOException {
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(dimensionRoot, worldSeed);
if (current.isPresent()) {
return current.get();
}
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
PackValidationResult validation = validatePublishedPack(legacyPack);
if (!validation.isLoadable()) {
throw new BrokenPackException(legacyPack.toString(), validation.getBlockingErrors());
}
GenerationCandidate legacy = captureGenerationCandidate(
legacyPack,
dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
);
return GenerationHistory.adoptLegacyPack(
dimensionRoot,
legacy.packFingerprint(),
worldSeed,
legacy.dimensionContract(),
legacy.registryContract()
);
}
private static GenerationCandidate captureGenerationCandidate(Path packRoot, String dimensionKey)
throws IOException {
return captureGenerationCandidate(
packRoot,
dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.CONTENT_ADDRESSED_ONLY
);
}
private static GenerationCandidate captureGenerationCandidate(
Path packRoot,
String dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy aliasPolicy
) throws IOException {
String fingerprint = GenerationPackFingerprint.compute(
packRoot,
GenerationPackFingerprint.CURRENT_VERSION
);
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IOException("Generation pack does not contain a loadable dimension '"
+ dimensionKey + "'.");
}
String dimensionTypeKey = IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
data.getDataFolder().getName(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
data,
dimension,
fingerprint,
aliasPolicy
);
return new GenerationCandidate(fingerprint, dimensionContract, registryContract);
} finally {
data.close();
}
}
private static IrisDimension loadInstalledDimension(Path packRoot, String dimensionKey) throws IOException {
PackValidationResult validation = validatePublishedPack(packRoot);
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
}
IrisDimension installed = IrisData.get(packRoot.toFile()).getDimensionLoader().load(dimensionKey, false);
if (installed == null) {
throw new IOException("Immutable generation pack does not contain dimension '"
+ dimensionKey + "'.");
}
return installed;
}
private IrisDimension installIntoDirectory(
VolmitSender sender,
IrisDimension dimension,
@@ -1534,6 +1722,13 @@ public class StudioSVC implements IrisService {
RESTART
}
private record GenerationCandidate(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) {
}
public IrisProject getActiveProject() {
return activeProject;
}
@@ -263,8 +263,10 @@ public class IrisCreator {
IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
installedDimension = IrisServices.get(StudioSVC.class)
.installIntoWorld(sender, resolvedDimension, dimensionRoot);
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());
}
@@ -28,6 +28,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
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.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
@@ -57,7 +58,6 @@ import org.jetbrains.annotations.ApiStatus;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -74,7 +74,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class IrisToolbelt {
@ApiStatus.Internal
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
/**
* Finds an installed dimension or returns null.
@@ -582,26 +581,7 @@ public class IrisToolbelt {
}
public static boolean isServerStopping() {
Method method = BUKKIT_IS_STOPPING_METHOD;
if (method != null) {
try {
Object value = method.invoke(null);
if (value instanceof Boolean) {
return (Boolean) value;
}
} catch (Throwable ignored) {
}
}
return !BukkitPlatform.hasPlugin() || !BukkitPlatform.plugin().isEnabled();
}
private static Method resolveBukkitIsStoppingMethod() {
try {
return Bukkit.class.getMethod("isStopping");
} catch (Throwable ignored) {
return null;
}
return INMS.isBound() && INMS.get().isServerStopping();
}
public static void beginWorldMaintenance(World world, String reason) {
@@ -23,6 +23,7 @@ import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import org.bukkit.NamespacedKey;
@@ -31,6 +32,7 @@ import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.io.IOException;
public class IrisWorldCreator {
private String name;
@@ -100,15 +102,42 @@ public class IrisWorldCreator {
.seed(seed)
.worldFolder(worldFolder)
.build();
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio
? dim.getLoader().getDataFolder() :
new File(w.worldFolder(), "iris/pack"), dimensionName);
GenerationHistory generationHistory = persistent
? requireGenerationHistory(w.worldFolder(), seed)
: null;
File packRoot = generationHistory != null
? requireActivePack(generationHistory)
: studio
? dim.getLoader().getDataFolder()
: new File(w.worldFolder(), "iris/pack");
ChunkGenerator g = new BukkitChunkGenerator(
w,
studio,
packRoot,
dimensionName,
generationHistory);
return creator.environment(environment)
.generateStructures(true)
.generator(g).seed(seed);
}
private static GenerationHistory requireGenerationHistory(File dimensionRoot, long seed) {
try {
return GenerationHistory.open(dimensionRoot.toPath(), seed);
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + dimensionRoot + ".", failure);
}
}
private static File requireActivePack(GenerationHistory history) {
try {
return history.activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris active generation pack is unusable.", failure);
}
}
private World.Environment findEnvironment() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
@@ -121,10 +121,10 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
fluidBlockSampler,
@@ -20,7 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -53,32 +53,40 @@ final class EngineHotloader {
void hotloadComplex() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
engine.awaitNativeStructureBootstrap("complex hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation();
IrisComplex nextComplex = null;
try {
engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
previousGeneration.target(),
previousGeneration.mantleStorageDirectory(),
previousGeneration.runtimeKernel(),
previousGeneration.transitionPlan());
engine.runtimeAssembly.set(assembly);
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = new IrisComplex(engine);
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
nextComplex = assembly.complex;
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
next = previous.withComplex(
GenerationRuntime nextGeneration = previousGeneration.withComplex(
assembly.cacheId,
assembly.complex,
assembly.upperContext,
assembly.dimensionStackContext,
biomeMaxes);
next = previous.withGeneration(nextGeneration);
} finally {
engine.runtimeAssembly.remove();
}
Throwable retirementFailure = runCleanup(null, previous.complex()::close);
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null;
@@ -91,7 +99,7 @@ final class EngineHotloader {
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) {
if (nextComplex != null && nextComplex != previous.complex()) {
if (nextComplex != null && nextComplex != previousGeneration.complex()) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
@@ -108,6 +116,7 @@ final class EngineHotloader {
void hotloadSilently() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
engine.awaitNativeStructureBootstrap("hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previousRuntime = engine.runtime;
@@ -183,6 +192,13 @@ final class EngineHotloader {
}
}
private void requireMutableGenerationRuntime(String operation) {
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
throw new IllegalStateException("Cannot " + operation
+ " while immutable Iris generation history is attached; stage a world update and restart instead.");
}
}
/**
* Never throws. It is called from the hotload failure handler, where a frame-cap
* {@code IllegalStateException} out of the codec (a long pack key or exception message overruns
@@ -19,48 +19,26 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import java.util.concurrent.CompletableFuture;
import java.util.Objects;
/**
* Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
*/
record EngineRuntime(
int cacheId,
EngineTarget target,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
GenerationRuntime generation,
EngineEffects effects,
EngineMode mode,
EngineWorldManager worldManager,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
EngineWorldManager worldManager
) {
EngineRuntime withComplex(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new EngineRuntime(
nextCacheId,
target,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
effects,
mode,
worldManager,
hash32,
nextBiomeMaxes);
EngineRuntime {
Objects.requireNonNull(generation, "generation runtime");
Objects.requireNonNull(effects, "engine effects");
Objects.requireNonNull(worldManager, "engine world manager");
}
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
EngineRuntime withGeneration(GenerationRuntime nextGeneration) {
return new EngineRuntime(nextGeneration, effects, worldManager);
}
}
@@ -20,24 +20,22 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.modifier.IrisCustomModifier;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionMode;
import art.arcane.iris.engine.object.IrisDimensionModeType;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
@@ -45,6 +43,9 @@ import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
@@ -64,22 +65,56 @@ final class EngineRuntimeBuilder {
}
EngineRuntime buildRuntime() {
return buildRuntime(engine.getTarget());
return buildRuntime(
engine.getTarget(),
engine.getInitialMantleStorageDirectory(),
engine.getInitialKernelVersion(),
engine.getInitialTransitionPlan(),
null);
}
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), runtimeTarget);
GenerationRuntime active = requireRuntime("hotload the engine runtime").generation();
return buildRuntime(
runtimeTarget,
active.mantleStorageDirectory(),
active.kernelVersion(),
active.transitionPlan(),
active.mantle());
}
private EngineRuntime buildRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan,
EngineMantle transferredMantle
) {
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
runtimeTarget,
mantleStorageDirectory,
selectRuntimeKernel(kernelVersion),
transitionPlan);
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Engine " + assembly.cacheId);
long started = M.ms();
assembly.complex = new IrisComplex(engine);
if (transferredMantle == null) {
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
assembly.ownsMantle = true;
} else {
assembly.mantle = transferredMantle;
}
IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.dimensionStackContext = buildDimensionStackContext();
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.upperContext = buildUpperContext();
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
@@ -89,21 +124,16 @@ final class EngineRuntimeBuilder {
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
assembly.hash32 = new CompletableFuture<>();
started = M.ms();
engine.getMantle().hotload();
assembly.mantle.hotload();
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.mode = createMode();
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks));
if (assembly.dimensionStackContext != null) {
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
dimensionStack.actuate(x, z, blocks, multicore, context));
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
stackCustom.modify(x, z, blocks, multicore, context));
}
assembly.mode = assembly.runtimeKernel.createMode(engine);
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
assembly.runtimeKernel.registerDimensionStack(
engine,
assembly.mode,
assembly.dimensionStackContext
);
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
@@ -112,7 +142,7 @@ final class EngineRuntimeBuilder {
}
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
BiomeMaxes biomeMaxes = computeBiomeMaxes();
return assembly.freeze(biomeMaxes);
return assembly.freezeRuntime(biomeMaxes);
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
@@ -124,8 +154,60 @@ final class EngineRuntimeBuilder {
}
}
GenerationRuntime buildDetachedGenerationRuntime(EngineTarget runtimeTarget, Path mantleStorageDirectory) {
return buildDetachedGenerationRuntime(
runtimeTarget,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
null);
}
GenerationRuntime buildDetachedGenerationRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan
) {
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
runtimeTarget,
mantleStorageDirectory,
selectRuntimeKernel(kernelVersion),
transitionPlan);
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
assembly.ownsMantle = true;
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
assembly.hash32 = CompletableFuture.completedFuture(computePackHash(runtimeTarget.getData()));
assembly.mantle.hotload();
assembly.mode = assembly.runtimeKernel.createMode(engine);
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
assembly.runtimeKernel.registerDimensionStack(
engine,
assembly.mode,
assembly.dimensionStackContext
);
return assembly.freezeGeneration(computeBiomeMaxes());
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
e.addSuppressed(cleanupFailure);
}
throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e);
} finally {
engine.runtimeAssembly.remove();
}
}
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null);
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
previous,
next.generation().mantle(),
null);
if (retirementFailure != null) {
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
engine.lifecycleState = LifecycleState.FAILED;
@@ -136,7 +218,7 @@ final class EngineRuntimeBuilder {
}
engine.runtime = next;
engine.publishedTarget = next.target();
engine.publishedTarget = next.generation().target();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
@@ -163,33 +245,11 @@ final class EngineRuntimeBuilder {
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
scheduleRuntimeTasks(next);
IrisLogging.debug("Engine Setup Complete " + next.cacheId());
IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
}
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
try {
File[] roots = engine.getData().getLoaders()
.values()
.stream()
.map(ResourceLoader::getFolderName)
.map(name -> new File(engine.getData().getDataFolder(), name))
.filter(File::exists)
.filter(File::isDirectory)
.toArray(File[]::new);
engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots));
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
throw propagate(e);
}
})) {
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
}
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
}
schedulePackHash(engineRuntime.generation());
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
@@ -199,71 +259,34 @@ final class EngineRuntimeBuilder {
}
}
UpperDimensionContext buildUpperContext() {
IrisDimension dim = engine.getDimension();
if (!dim.hasUpperDimension()) {
return null;
}
String upperKey = dim.getUpperDimension();
IrisDimension upperDim = upperKey.equals(dim.getLoadKey())
? dim
: IrisData.loadAnyDimension(upperKey, engine.getData());
if (upperDim != null) {
UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim);
IrisLogging.info("Upper dimension enabled: " + upperKey
+ (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
return ctx;
}
IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain.");
return null;
}
DimensionStackContext buildDimensionStackContext() {
IrisDimension dimension = engine.getDimension();
if (!dimension.hasDimensionStack()) {
return null;
}
if (dimension.hasUpperDimension()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
+ "' cannot enable both dimensionStack and upperDimension.");
}
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
IrisLogging.info("Dimension stack enabled: "
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
return context;
}
private EngineMode createMode() {
Throwable configuredFailure = null;
private void schedulePackHash(GenerationRuntime generationRuntime) {
try {
IrisDimensionMode configuredMode = engine.getDimension().getMode();
if (configuredMode == null) {
configuredMode = new IrisDimensionMode();
engine.getDimension().setMode(configuredMode);
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
try {
generationRuntime.hash32().complete(computePackHash(generationRuntime.data()));
} catch (Throwable e) {
generationRuntime.hash32().completeExceptionally(e);
throw propagate(e);
}
})) {
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
}
EngineMode configured = configuredMode.create(engine);
if (configured == null) {
throw new IllegalStateException("Dimension mode factory returned null");
}
return configured;
} catch (Throwable e) {
configuredFailure = e;
generationRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
}
}
}
try {
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine);
if (fallback == null) {
throw new IllegalStateException("OVERWORLD mode factory returned null");
}
return fallback;
} catch (Throwable fallbackFailure) {
fallbackFailure.addSuppressed(configuredFailure);
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure);
}
private long computePackHash(IrisData data) {
File[] roots = data.getLoaders()
.values()
.stream()
.map(ResourceLoader::getFolderName)
.map(name -> new File(data.getDataFolder(), name))
.filter(File::exists)
.filter(File::isDirectory)
.toArray(File[]::new);
return IO.hashRecursiveMeta(roots);
}
BiomeMaxes computeBiomeMaxes() {
@@ -317,11 +340,27 @@ final class EngineRuntimeBuilder {
return current;
}
static GenerationKernelRegistry.RuntimeKernel selectRuntimeKernel(
GenerationKernelRegistry.Version version
) {
try {
return GenerationKernelRegistry.standard().select(version);
} catch (IOException failure) {
throw new IllegalArgumentException("Unsupported executable Iris generation kernel "
+ version + ".", failure);
}
}
static final class RuntimeAssembly {
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
final int cacheId;
final EngineTarget target;
final Path mantleStorageDirectory;
final GenerationKernelRegistry.Version kernelVersion;
final GenerationKernelRegistry.RuntimeKernel runtimeKernel;
final SeedManager seedManager;
final TransitionGenerationPlan transitionPlan;
static int nextRuntimeId() {
return RUNTIME_IDS.incrementAndGet();
@@ -331,29 +370,76 @@ final class EngineRuntimeBuilder {
DimensionStackContext dimensionStackContext;
EngineEffects effects;
EngineMode mode;
EngineMantle mantle;
EngineWorldManager worldManager;
CompletableFuture<Long> hash32;
boolean ownsMantle;
RuntimeAssembly(int cacheId, EngineTarget target) {
this.cacheId = cacheId;
this.target = target;
}
EngineRuntime freeze(BiomeMaxes biomeMaxes) {
if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
}
return new EngineRuntime(
this(
cacheId,
target,
null,
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
null);
}
RuntimeAssembly(int cacheId, EngineTarget target, Path mantleStorageDirectory) {
this(
cacheId,
target,
mantleStorageDirectory,
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
null);
}
RuntimeAssembly(
int cacheId,
EngineTarget target,
Path mantleStorageDirectory,
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
TransitionGenerationPlan transitionPlan
) {
this.cacheId = cacheId;
this.target = target;
this.mantleStorageDirectory = mantleStorageDirectory == null
? null
: IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
this.runtimeKernel = Objects.requireNonNull(runtimeKernel, "executable generation kernel");
this.seedManager = this.runtimeKernel.createSeedManager(target.getWorld().getRawWorldSeed());
this.kernelVersion = this.runtimeKernel.version();
this.transitionPlan = transitionPlan;
}
GenerationRuntime freezeGeneration(BiomeMaxes biomeMaxes) {
if (complex == null || mode == null || mantle == null || mantleStorageDirectory == null || hash32 == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris generation runtime.");
}
return new GenerationRuntime(
cacheId,
target,
target.getData(),
target.getDimension(),
kernelVersion,
runtimeKernel,
seedManager,
transitionPlan,
complex,
upperContext,
dimensionStackContext,
effects,
mode,
worldManager,
mantle,
mantleStorageDirectory,
hash32,
biomeMaxes);
}
EngineRuntime freezeRuntime(BiomeMaxes biomeMaxes) {
if (effects == null || worldManager == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
}
return new EngineRuntime(freezeGeneration(biomeMaxes), effects, worldManager);
}
}
}
@@ -22,8 +22,10 @@ import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -38,7 +40,6 @@ final class EngineShutdownSequence {
private final IrisEngine engine;
private boolean runtimeReleased;
private boolean targetReleased;
private boolean mantleReleased;
private boolean engineDataReleased;
private boolean preservationReleased;
@@ -47,6 +48,17 @@ final class EngineShutdownSequence {
}
void close() {
if (!engine.beginShutdown()) {
return;
}
Throwable routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
if (routerFailure != null) {
synchronized (engine.lifecycleLock) {
engine.lifecycleState = LifecycleState.FAILED;
}
reportIncompleteClose(routerFailure);
return;
}
Throwable failure;
synchronized (engine.lifecycleLock) {
if (engine.closed) {
@@ -122,14 +134,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) {
failure = releaseTarget(failure);
}
if (targetReleased) {
failure = releaseMantle(failure);
}
if (prefetchFailure == null
&& engineDataFailure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased) {
&& targetReleased) {
failure = releaseEngineDataForShutdown(failure);
}
if (engineDataReleased) {
@@ -140,7 +148,6 @@ final class EngineShutdownSequence {
if (failure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased
&& engineDataReleased
&& preservationReleased) {
engine.closed = true;
@@ -185,10 +192,10 @@ final class EngineShutdownSequence {
() -> NativeStructureOwnershipStore.close(engine));
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
if (ownershipFailure == null) {
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure);
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
engine.runtime = null;
cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close);
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
engine.closed = true;
cleanupFailure = runCleanup(cleanupFailure, () -> {
@@ -232,18 +239,69 @@ final class EngineShutdownSequence {
assembly.hash32.cancel(true);
}
});
if (assembly.mantle != null && assembly.ownsMantle) {
assembly.ownsMantle = false;
failure = runCleanup(failure, assembly.mantle::saveAllNow);
failure = runCleanup(failure, assembly.mantle::close);
}
return failure;
}
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
return closeRuntime(engineRuntime, null, failure);
}
Throwable closeRuntime(
EngineRuntime engineRuntime,
EngineMantle retainedMantle,
Throwable failure
) {
if (engineRuntime == null) {
return failure;
}
failure = runCleanup(failure, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close);
failure = runCleanup(failure, engineRuntime.mode()::close);
failure = runCleanup(failure, engineRuntime.complex()::close);
failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true));
return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
}
Throwable closeGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
return closeGenerationRuntime(generationRuntime, null, failure);
}
Throwable closeGenerationRuntime(
GenerationRuntime generationRuntime,
EngineMantle retainedMantle,
Throwable failure
) {
if (generationRuntime == null) {
return failure;
}
failure = runCleanup(failure, generationRuntime.mode()::close);
failure = runCleanup(failure, generationRuntime.complex()::close);
failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true));
if (generationRuntime.mantle() != retainedMantle) {
failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow);
failure = runCleanup(failure, generationRuntime.mantle()::close);
}
return failure;
}
Throwable closeDetachedGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
engine,
generationRuntime);
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) {
failure = closeGenerationRuntime(generationRuntime, failure);
} catch (Throwable scopeFailure) {
failure = appendFailure(failure, scopeFailure);
}
return closeDetachedTarget(generationRuntime.target(), failure);
}
Throwable closeDetachedTarget(EngineTarget target, Throwable failure) {
failure = runCleanup(failure, () -> target.getData().unregisterEngine(engine));
failure = runCleanup(failure, target::close);
failure = runCleanup(failure, target.getData()::close);
return failure;
}
@@ -251,6 +309,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) {
return failure;
}
Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
if (detachedFailure != null) {
return appendFailure(failure, detachedFailure);
}
Throwable runtimeFailure = closeRuntime(engine.runtime, null);
if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure);
@@ -260,11 +322,23 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
for (GenerationRuntime detached : detachedRuntimes) {
Throwable detachedFailure = closeDetachedGenerationRuntime(detached, null);
failure = appendFailure(failure, detachedFailure);
if (detachedFailure == null) {
engine.detachedGenerationRuntimes.remove(detached);
}
}
return failure;
}
private Throwable releaseTarget(Throwable failure) {
if (targetReleased) {
return failure;
}
Throwable targetFailure = runCleanup(null, engine.getTarget()::close);
Throwable targetFailure = runCleanup(null, engine.publishedTarget::close);
if (targetFailure != null) {
return appendFailure(failure, targetFailure);
}
@@ -272,18 +346,6 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable releaseMantle(Throwable failure) {
if (mantleReleased) {
return failure;
}
Throwable mantleFailure = runCleanup(null, engine.getMantle()::close);
if (mantleFailure != null) {
return appendFailure(failure, mantleFailure);
}
mantleReleased = true;
return failure;
}
private Throwable releaseEngineDataForShutdown(Throwable failure) {
if (engineDataReleased) {
return failure;
@@ -0,0 +1,101 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDimension;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
record GenerationRuntime(
int cacheId,
EngineTarget target,
IrisData data,
IrisDimension dimension,
GenerationKernelRegistry.Version kernelVersion,
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
SeedManager seedManager,
TransitionGenerationPlan transitionPlan,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineMode mode,
EngineMantle mantle,
Path mantleStorageDirectory,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
) {
GenerationRuntime {
Objects.requireNonNull(target, "generation target");
Objects.requireNonNull(data, "generation data");
Objects.requireNonNull(dimension, "generation dimension");
Objects.requireNonNull(kernelVersion, "generation kernel version");
Objects.requireNonNull(runtimeKernel, "executable generation kernel");
Objects.requireNonNull(seedManager, "generation seed manager");
if (!kernelVersion.equals(runtimeKernel.version())) {
throw new IllegalArgumentException("Generation kernel version does not match its executable runtime.");
}
Objects.requireNonNull(complex, "generation complex");
Objects.requireNonNull(mode, "generation mode");
Objects.requireNonNull(mantle, "generation mantle");
mantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
Objects.requireNonNull(hash32, "generation pack hash");
Objects.requireNonNull(biomeMaxes, "generation biome maximums");
if (target.getData() != data || target.getDimension() != dimension) {
throw new IllegalArgumentException("Generation target, data, and dimension must describe one runtime.");
}
}
GenerationRuntime withComplex(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new GenerationRuntime(
nextCacheId,
target,
data,
dimension,
kernelVersion,
runtimeKernel,
seedManager,
transitionPlan,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
mode,
mantle,
mantleStorageDirectory,
hash32,
nextBiomeMaxes);
}
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
}
}
@@ -0,0 +1,55 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeBinding;
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeScope;
final class GenerationRuntimeScopeState {
private final ThreadLocal<GenerationRuntimeBinding> current = new ThreadLocal<>();
GenerationRuntimeBinding current() {
return current.get();
}
GenerationRuntimeScope open(GenerationRuntimeBinding binding) {
Thread owner = Thread.currentThread();
GenerationRuntimeBinding previous = current.get();
current.set(binding);
return new GenerationRuntimeScope(this, owner, previous, binding);
}
void close(
Thread owner,
GenerationRuntimeBinding previous,
GenerationRuntimeBinding installed
) {
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris generation runtime scope closed from a different thread.");
}
if (current.get() != installed) {
throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order.");
}
if (previous == null) {
current.remove();
return;
}
current.set(previous);
}
}
@@ -23,6 +23,8 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
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.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
@@ -66,6 +68,7 @@ import lombok.ToString;
import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
@@ -75,6 +78,7 @@ import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -114,6 +118,7 @@ public class IrisComplex implements DataProvider {
private final transient IrisGenerator[][] frozenGenerators;
@Getter(AccessLevel.NONE)
private final transient Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> inferredBiomeStreams;
private final TransitionGenerationPlan transitionGenerationPlan;
private RNG rng;
private double fluidHeight;
private IrisData data;
@@ -130,9 +135,11 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisBiome> baseBiomeStream;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
private ProceduralStream<IrisBiome> trueBiomeStream;
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
private ProceduralStream<Double> unblendedNaturalHeightStream;
private ProceduralStream<Double> heightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
@@ -168,10 +175,19 @@ public class IrisComplex implements DataProvider {
private final Object childSelectionPlanLock = new Object();
public IrisComplex(Engine engine) {
this(engine, false);
this(engine, false, null);
}
public IrisComplex(Engine engine, boolean simple) {
this(engine, simple, null);
}
public IrisComplex(Engine engine, TransitionGenerationPlan transitionGenerationPlan) {
this(engine, false, transitionGenerationPlan);
}
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
this.transitionGenerationPlan = transitionGenerationPlan;
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
@@ -294,10 +310,15 @@ public class IrisComplex implements DataProvider {
return mapped == null ? biome : mapped;
})
.cache2D("imageMappedBaseBiomeStream", engine, cacheSize);
naturalHeightStream = ProceduralStream.of(
(x, z) -> sampleNaturalTerrainHeight(engine, x, z),
unblendedNaturalHeightStream = ProceduralStream.of(
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
Interpolated.DOUBLE
).cache2DDouble("naturalHeightStream", engine, cacheSize);
).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);
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
@@ -308,6 +329,14 @@ public class IrisComplex implements DataProvider {
: mapped;
})
.cache2D("naturalTrueBiomeStream", engine, cacheSize);
unblendedNaturalTrueBiomeStream = transitionGenerationPlan == null || focusBiome != null
? naturalTrueBiomeStream
: unblendedNaturalHeightStream.convertAware2D((h, x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
return mapped == null
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight)
: mapped;
}).cache2D("unblendedNaturalTrueBiomeStream", engine, cacheSize);
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
if (hydrologyActive(configuredHydrology)) {
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
@@ -451,10 +480,35 @@ 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);
}
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight());
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;
@@ -687,7 +741,9 @@ public class IrisComplex implements DataProvider {
if (layer == null) {
return 0D;
}
return layer.feature().flowDeltaX() == 0 && layer.feature().flowDeltaZ() == 0 ? 0D : 1D;
return layer.feature().flowDeltaX() == 0 && layer.feature().flowDeltaZ() == 0
? 0D
: transitionHydrologyWeight(x, z);
}
private double resolveHydrologyCarveWeight(double x, double z) {
@@ -695,10 +751,11 @@ public class IrisComplex implements DataProvider {
if (layer == null) {
return 0D;
}
double hydrologyWeight = transitionHydrologyWeight(x, z);
if (layer.channel()) {
return 1D;
return hydrologyWeight;
}
return layer.shore() ? 0.75D : 0.5D;
return hydrologyWeight * (layer.shore() ? 0.75D : 0.5D);
}
private double resolveHydrologyFluidSurface(double x, double z) {
@@ -706,13 +763,144 @@ public class IrisComplex implements DataProvider {
return layer == null ? fluidHeight : layer.fluidHeadY();
}
public HydrologyColumnSample sampleHydrologyColumn(double x, double z) {
if (hydrologyRuntime == null) {
return null;
}
if (transitionGenerationPlan == null) {
return hydrologyRuntime.sample(x, z).orElse(null);
}
double hydrologyWeight = transitionHydrologyWeight(x, z);
if (hydrologyWeight == 0D) {
return null;
}
HydrologyColumnSample sample = hydrologyRuntime.sample(x, z).orElse(null);
if (sample == null || hydrologyWeight == 1D) {
return sample;
}
return taperHydrologySample(sample, hydrologyWeight);
}
static HydrologyColumnSample taperHydrologySample(
HydrologyColumnSample sample,
double hydrologyWeight
) {
Objects.requireNonNull(sample, "hydrology sample");
if (hydrologyWeight == 1D) {
return sample;
}
ArrayList<HydrologyColumnLayer> taperedLayers = new ArrayList<>(sample.layers().size());
for (HydrologyColumnLayer layer : sample.layers()) {
taperedLayers.add(taperHydrologyLayer(layer, sample.naturalHeight(), hydrologyWeight));
}
return new HydrologyColumnSample(
sample.x(),
sample.z(),
sample.naturalHeight(),
sample.seaLevel(),
sample.ocean(),
sample.parentBiomeKey(),
taperedLayers);
}
private static HydrologyColumnLayer taperHydrologyLayer(
HydrologyColumnLayer layer,
int naturalHeight,
double hydrologyWeight
) {
return new HydrologyColumnLayer(
layer.feature(),
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
GenerationBlend.interpolateHeight(naturalHeight, layer.fluidHeadY(), hydrologyWeight),
GenerationBlend.interpolateHeight(naturalHeight, layer.ceilingY(), hydrologyWeight),
layer.channel(),
layer.shore(),
layer.grading(),
layer.connectedFluid(),
layer.fallingFluid(),
layer.receivingPool(),
layer.terrainOwned(),
layer.fluidOwned(),
layer.oceanApron(),
layer.profileKey(),
layer.surfaceBiomeKey(),
layer.mouthBiomeKey(),
layer.shoreBiomeKey(),
layer.bankBiomeKey(),
layer.floodedCaveBiomeKey());
}
private HydrologyColumnSample hydrologySample(double x, double z) {
return hydrologyRuntime == null ? null : hydrologyRuntime.sample(x, z).orElse(null);
return sampleHydrologyColumn(x, z);
}
private double transitionHydrologyWeight(double x, double z) {
return transitionGenerationPlan == null
? 1D
: transitionGenerationPlan.hydrologyWeightAt(blockCoordinate(x), blockCoordinate(z));
}
/** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */
public boolean isHydrologyPlanned(int x, int z) {
return hydrologyRuntime == null || hydrologyRuntime.isPlanned(x, z);
return hydrologyRuntime == null
|| transitionHydrologyWeight(x, z) == 0D
|| hydrologyRuntime.isPlanned(x, z);
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
return transitionGenerationPlan != null
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
}
public boolean allowsMantleWrite(int blockX, int blockZ) {
return transitionGenerationPlan == null
|| !transitionGenerationPlan.isHistoricalBlock(blockX, blockZ);
}
public boolean allowsMantleChunkWrite(int chunkX, int chunkZ) {
return transitionGenerationPlan == null
|| !transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
}
public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) {
return transitionGenerationPlan == null
|| transitionGenerationPlan.allowsNewDiscreteContentAt(blockX, blockZ);
}
public boolean allowsNewGenerationChunk(int chunkX, int chunkZ) {
if (transitionGenerationPlan == null) {
return true;
}
int minimumX = Math.multiplyExact(chunkX, 16);
int minimumZ = Math.multiplyExact(chunkZ, 16);
return transitionGenerationPlan.allowsNewFootprint(
minimumX,
minimumZ,
Math.addExact(minimumX, 15),
Math.addExact(minimumZ, 15));
}
public boolean allowsNewGenerationFootprint(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
return transitionGenerationPlan == null
|| transitionGenerationPlan.allowsNewFootprint(minimumX, minimumZ, maximumX, maximumZ);
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
return transitionGenerationPlan == null
? Optional.empty()
: transitionGenerationPlan.historicalPhysicalBiomeKeyAt(blockX, blockY, blockZ);
}
private static int blockCoordinate(double coordinate) {
if (!Double.isFinite(coordinate)) {
throw new IllegalArgumentException("Generation coordinate must be finite");
}
return (int) Math.floor(coordinate);
}
/** The natural terrain height of a column, from the natural height stream only: never touches hydrology or its caches. */
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMetrics;
@@ -33,10 +34,16 @@ import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
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.GenerationKernelRegistry;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
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;
@@ -59,6 +66,7 @@ import art.arcane.volmlib.util.atomics.AtomicRollingSequence;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.M;
@@ -71,17 +79,23 @@ 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;
import java.util.function.IntConsumer;
import java.util.function.Supplier;
@Data
@@ -96,7 +110,9 @@ public class IrisEngine implements Engine {
private final AtomicLong lastGPS;
private final EngineTarget target;
private final InitializationMode initializationMode;
private final EngineMantle mantle;
private final Path initialMantleStorageDirectory;
private final GenerationKernelRegistry.Version initialKernelVersion;
private final TransitionGenerationPlan initialTransitionPlan;
private final ChronoLatch perSecondLatch;
private final ChronoLatch perSecondBudLatch;
private final EngineMetrics metrics;
@@ -108,9 +124,26 @@ public class IrisEngine implements Engine {
final Object lifecycleLock = new Object();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final Object generationHistoryRuntimeRouterLock = new Object();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final ThreadLocal<RuntimeAssembly> runtimeAssembly = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final GenerationRuntimeScopeState generationRuntimeScopes = new GenerationRuntimeScopeState();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final Set<GenerationRuntime> detachedGenerationRuntimes = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final Set<GenerationRuntime> retiringGenerationRuntimes = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final Set<IntConsumer> generationRuntimeRetirementListeners = new CopyOnWriteArraySet<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final EngineBackgroundTasks backgroundTasks = new EngineBackgroundTasks();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -147,6 +180,12 @@ public class IrisEngine implements Engine {
volatile EngineRuntime runtime;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private boolean generationHistoryRoutingRequired;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
volatile EngineTarget publishedTarget;
private volatile int parallelism;
private volatile boolean failing;
@@ -176,11 +215,57 @@ public class IrisEngine implements Engine {
}
public IrisEngine(EngineTarget target, InitializationMode initializationMode) {
this(
target,
initializationMode,
IrisEngineMantle.legacyStorageDirectory(target),
GenerationKernelRegistry.standard().current(),
null);
}
public IrisEngine(
EngineTarget target,
InitializationMode initializationMode,
Path mantleStorageDirectory
) {
this(
target,
initializationMode,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
null);
}
public IrisEngine(
EngineTarget target,
InitializationMode initializationMode,
Path mantleStorageDirectory,
TransitionGenerationPlan transitionPlan
) {
this(
target,
initializationMode,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
transitionPlan);
}
public IrisEngine(
EngineTarget target,
InitializationMode initializationMode,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan
) {
EngineTarget requiredTarget = Objects.requireNonNull(target, "engine target");
InitializationMode requiredMode = Objects.requireNonNull(initializationMode, "initialization mode");
this.initializationMode = requiredMode;
this.studio = requiredMode.studio();
this.target = target;
this.publishedTarget = target;
this.target = requiredTarget;
this.publishedTarget = requiredTarget;
this.initialMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version");
this.initialTransitionPlan = transitionPlan;
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
this.generationSessions = new GenerationSessionManager();
this.closing = new AtomicBoolean(true);
@@ -190,7 +275,8 @@ public class IrisEngine implements Engine {
this.failing = false;
getEngineData();
verifySeed();
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
this.seedManager = EngineRuntimeBuilder.selectRuntimeKernel(initialKernelVersion)
.createSeedManager(requiredTarget.getWorld().getRawWorldSeed());
bud = new AtomicInteger(0);
buds = new AtomicInteger(0);
metrics = new EngineMetrics(32);
@@ -203,9 +289,7 @@ public class IrisEngine implements Engine {
wallClock = new AtomicRollingSequence(32);
lastGPS = new AtomicLong(M.ms());
generated = new AtomicInteger(0);
long _t0 = M.ms();
mantle = new IrisEngineMantle(this);
IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms");
long _t0;
cleaning = new AtomicBoolean(false);
modeFallbackLogged = new AtomicBoolean(false);
prefetchSaveStarted = new AtomicBoolean(false);
@@ -235,7 +319,7 @@ public class IrisEngine implements Engine {
} catch (Throwable e) {
IrisLogging.reportError(e);
}
IrisLogging.notice("Engine init: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed());
IrisLogging.notice("Engine init: " + requiredTarget.getWorld().name() + "/" + requiredTarget.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed());
_t0 = M.ms();
phaseStartedAt = System.nanoTime();
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime();
@@ -258,7 +342,7 @@ public class IrisEngine implements Engine {
EngineTickRegistry.registerTicking(this);
} catch (Throwable e) {
shutdownSequence.cleanupFailedConstruction(e);
throw new IllegalStateException("Failed to initialize Iris engine for world '" + target.getWorld().name() + "'.", e);
throw new IllegalStateException("Failed to initialize Iris engine for world '" + requiredTarget.getWorld().name() + "'.", e);
}
IrisLogging.debug("Engine Initialized " + getCacheID());
}
@@ -349,7 +433,7 @@ public class IrisEngine implements Engine {
IrisLogging.warn("Studio packs changed during runtime compilation; shared generation caches are disabled.");
return;
}
runtime.complex().enableStudioHydrologyCache(initialIdentity, studioHydrologyCacheRoot());
runtime.generation().complex().enableStudioHydrologyCache(initialIdentity, studioHydrologyCacheRoot());
IrisLogging.debug("Enabled shared Studio hydrology cache identity="
+ initialIdentity.substring(0, Math.min(12, initialIdentity.length())));
}
@@ -451,7 +535,9 @@ public class IrisEngine implements Engine {
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("matter_generate");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(x << 4, z << 4);
GenerationSessionLease lease = acquireGenerationLease("matter_generate");
IrisContext.Scope ignored = IrisContext.open(this, lease.sessionId(), context)) {
IrisComplex activeComplex = getComplex();
if (context == null || context.getComplex() != activeComplex) {
@@ -463,21 +549,114 @@ public class IrisEngine implements Engine {
+ context.getGenerationSessionId() + " instead of " + lease.sessionId() + ".");
}
getMantle().generateMatter(x, z, multicore, context);
} catch (GenerationSessionException e) {
} catch (GenerationSessionException | IOException e) {
throw new IllegalStateException("Matter generation was rejected by the Iris lifecycle.", e);
}
}
@BlockCoordinates
@Override
public Set<String> getObjectsAt(int x, int z) {
return getMantle().getObjectComponent().guess(x, z);
public IrisRegion getRegion(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a region")) {
return Engine.super.getRegion(x, z);
}
}
@BlockCoordinates
@Override
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkY) {
Set<Pair<String, BlockPos>> pois = new HashSet<>();
getMantle().getMantle().iterateChunk(chunkX, chunkY, MatterStructurePOI.class, (x, y, z, d) -> pois.add(new Pair<>(d.getType(), new BlockPos(x, y, z))));
return pois;
public IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave or mantle biome")) {
return Engine.super.getCaveOrMantleBiome(x, y, z);
}
}
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
return Engine.super.getCaveBiome(x, z);
}
}
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(int x, int y, int z) {
return getCaveBiome(x, y, z, null);
}
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(
int x,
int y,
int z,
IrisDimensionCarvingResolver.State state
) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
return Engine.super.getCaveBiome(x, y, z, state);
}
}
@BlockCoordinates
@Override
public IrisBiome getSurfaceBiome(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
return Engine.super.getSurfaceBiome(x, z);
}
}
@BlockCoordinates
@Override
public double getSlope(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain slope")) {
return Engine.super.getSlope(x, z);
}
}
@BlockCoordinates
@Override
public int getHeight(int x, int z) {
return getHeight(x, z, true);
}
@BlockCoordinates
@Override
public int getHeight(int x, int z, boolean ignoreFluid) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain height")) {
return Engine.super.getHeight(x, z, ignoreFluid);
}
}
@ChunkCoordinates
@Override
public Set<String> getObjectsAt(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
return getMantle().getObjectComponent().guess(x, z);
}
}
@ChunkCoordinates
@Override
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkZ) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
Set<Pair<String, BlockPos>> pois = new HashSet<>();
getMantle().getMantle().iterateChunk(
chunkX,
chunkZ,
MatterStructurePOI.class,
(x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(x, y, z))));
return pois;
}
}
private void warmupChunk(int x, int z) {
@@ -514,7 +693,50 @@ public class IrisEngine implements Engine {
if (!nativeStructureVolumeQueriesEnabled.get()) {
return NativeStructureVolume.NONE;
}
return nativeStructureVolumeMemo.volumes(this, platformHooks, minX, minZ, maxX, maxZ);
int minimumX = Math.min(minX, maxX);
int maximumX = Math.max(minX, maxX);
int minimumZ = Math.min(minZ, maxZ);
int maximumZ = Math.max(minZ, maxZ);
int fromChunkX = minimumX >> 4;
int toChunkX = maximumX >> 4;
int fromChunkZ = minimumZ >> 4;
int toChunkZ = maximumZ >> 4;
KList<NativeStructureVolume> matches = null;
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
int chunkMinimumX = chunkX << 4;
int chunkMinimumZ = chunkZ << 4;
int queryMinimumX = Math.max(minimumX, chunkMinimumX);
int queryMaximumX = Math.min(maximumX, chunkMinimumX + 15);
int queryMinimumZ = Math.max(minimumZ, chunkMinimumZ);
int queryMaximumZ = Math.min(maximumZ, chunkMinimumZ + 15);
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
chunkMinimumX,
chunkMinimumZ,
"resolve native structure volumes")) {
KList<NativeStructureVolume> resolved = nativeStructureVolumeMemo.volumes(
this,
platformHooks,
queryMinimumX,
queryMinimumZ,
queryMaximumX,
queryMaximumZ);
for (NativeStructureVolume volume : resolved) {
if (!volume.intersectsRect(minimumX, minimumZ, maximumX, maximumZ)) {
continue;
}
if (matches == null) {
matches = new KList<>();
}
if (!matches.contains(volume)) {
matches.add(volume);
}
}
}
}
}
return matches == null ? NativeStructureVolume.NONE : matches;
}
public void setNativeStructureVolumeQueriesEnabled(boolean enabled) {
@@ -561,6 +783,312 @@ public class IrisEngine implements Engine {
shutdownSequence.close();
}
public GenerationHistoryRuntimeRouter attachGenerationHistory(
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler
) throws IOException {
return GenerationHistoryRuntimeRouter.attachAndPromotePending(this, history, signatureSampler);
}
public GenerationHistoryRuntimeRouter attachGenerationHistory(
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
int transitionWidthBlocks
) throws IOException {
return GenerationHistoryRuntimeRouter.attachAndPromotePending(
this,
history,
signatureSampler,
transitionWidthBlocks
);
}
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
synchronized (generationHistoryRuntimeRouterLock) {
return Optional.ofNullable(generationHistoryRuntimeRouter);
}
}
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
int blockX,
int blockZ
) throws IOException {
GenerationHistoryRuntimeRouter router;
synchronized (generationHistoryRuntimeRouterLock) {
router = generationHistoryRuntimeRouter;
if (router == null && generationHistoryRoutingRequired) {
throw new IllegalStateException("Iris generation-history runtime router is detached.");
}
}
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
}
public boolean hasGenerationRuntimeScope() {
return generationRuntimeScopes.current() != null;
}
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
int blockX,
int blockZ,
String operation
) {
try {
return openGenerationHistoryCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Unable to " + operation + " through Iris generation history at "
+ blockX + "," + blockZ + ".", failure);
}
}
public void attachGenerationHistoryRuntimeRouter(GenerationHistoryRuntimeRouter router) {
GenerationHistoryRuntimeRouter required = Objects.requireNonNull(router, "generation-history runtime router");
if (required.engine() != this) {
throw new IllegalArgumentException("Generation-history runtime router belongs to a different Iris engine.");
}
synchronized (lifecycleLock) {
requireRunning("attach a generation-history runtime router");
synchronized (generationHistoryRuntimeRouterLock) {
if (generationHistoryRuntimeRouter != null) {
throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
}
generationHistoryRuntimeRouter = required;
generationHistoryRoutingRequired = true;
}
}
}
public void detachGenerationHistoryRuntimeRouter(GenerationHistoryRuntimeRouter router) {
synchronized (generationHistoryRuntimeRouterLock) {
if (generationHistoryRuntimeRouter == router) {
generationHistoryRuntimeRouter = null;
}
}
}
void closeAttachedGenerationHistoryRuntimeRouter() {
GenerationHistoryRuntimeRouter attached;
synchronized (generationHistoryRuntimeRouterLock) {
attached = generationHistoryRuntimeRouter;
if (attached == null) {
return;
}
}
attached.close();
synchronized (generationHistoryRuntimeRouterLock) {
if (generationHistoryRuntimeRouter == attached) {
generationHistoryRuntimeRouter = null;
}
}
}
boolean beginShutdown() {
synchronized (lifecycleLock) {
if (closed) {
return false;
}
lifecycleState = LifecycleState.CLOSING;
closing.set(true);
return true;
}
}
public GenerationRuntimeBinding getActiveGenerationRuntimeBinding() {
EngineRuntime current = runtimeBuilder.requireRuntime("capture the active generation runtime");
return new GenerationRuntimeBinding(this, current.generation());
}
public GenerationRuntimeBinding captureGenerationRuntimeBinding() {
GenerationRuntimeBinding scoped = generationRuntimeScopes.current();
return scoped == null ? getActiveGenerationRuntimeBinding() : scoped;
}
public GenerationRuntimeScope openGenerationRuntimeScope(GenerationRuntimeBinding binding) {
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
synchronized (lifecycleLock) {
if (required.engine != this) {
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
}
if (retiringGenerationRuntimes.contains(required.runtime)) {
throw new IllegalStateException("Iris generation runtime binding is retiring.");
}
if (!isGenerationRuntimeBindingLive(required)) {
throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned.");
}
return generationRuntimeScopes.open(required);
}
}
public GenerationRuntimeBinding buildDetachedGenerationRuntime(EngineTarget runtimeTarget) {
return buildDetachedGenerationRuntime(
runtimeTarget,
IrisEngineMantle.legacyStorageDirectory(runtimeTarget));
}
public GenerationRuntimeBinding buildDetachedGenerationRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory
) {
return buildDetachedGenerationRuntime(
runtimeTarget,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
null);
}
public GenerationRuntimeBinding buildDetachedGenerationRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
TransitionGenerationPlan transitionPlan
) {
return buildDetachedGenerationRuntime(
runtimeTarget,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
transitionPlan);
}
public GenerationRuntimeBinding buildDetachedGenerationRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan
) {
EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target");
Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
synchronized (lifecycleLock) {
requireRunning("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.");
}
IrisData detachedData = requiredTarget.getData();
if (detachedData == active.data()) {
throw new IllegalArgumentException("Detached generation runtime requires a detached Iris data loader.");
}
if (requiredMantleStorageDirectory.equals(active.mantleStorageDirectory())) {
throw new IllegalArgumentException("Detached generation runtime requires an exclusive mantle storage directory.");
}
for (GenerationRuntime detached : detachedGenerationRuntimes) {
if (detached.data() == detachedData) {
throw new IllegalArgumentException("Detached Iris data loader already belongs to a generation runtime.");
}
if (detached.mantleStorageDirectory().equals(requiredMantleStorageDirectory)) {
throw new IllegalArgumentException("Mantle storage directory already belongs to a detached generation runtime.");
}
}
GenerationRuntime detached = null;
try {
detachedData.registerEngine(this);
GenerationKernelRegistry.Version requiredKernelVersion = Objects.requireNonNull(
kernelVersion,
"generation kernel version");
detached = transitionPlan == null
&& requiredKernelVersion.equals(GenerationKernelRegistry.standard().current())
? runtimeBuilder.buildDetachedGenerationRuntime(
requiredTarget,
requiredMantleStorageDirectory)
: runtimeBuilder.buildDetachedGenerationRuntime(
requiredTarget,
requiredMantleStorageDirectory,
requiredKernelVersion,
transitionPlan);
detachedGenerationRuntimes.add(detached);
return new GenerationRuntimeBinding(this, detached);
} catch (Throwable e) {
Throwable cleanupFailure = detached == null
? shutdownSequence.closeDetachedTarget(requiredTarget, null)
: shutdownSequence.closeDetachedGenerationRuntime(detached, null);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
}
throw EngineShutdownSequence.propagate(e);
}
}
}
public void closeDetachedGenerationRuntime(GenerationRuntimeBinding binding) {
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "detached generation runtime binding");
GenerationRuntime retired;
synchronized (lifecycleLock) {
if (required.engine != this) {
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
}
EngineRuntime current = runtime;
if (current != null && current.generation() == required.runtime) {
throw new IllegalArgumentException("The active Iris generation runtime cannot be closed as detached.");
}
if (!detachedGenerationRuntimes.contains(required.runtime)) {
return;
}
if (!retiringGenerationRuntimes.add(required.runtime)) {
throw new IllegalStateException("Detached Iris generation runtime is already retiring.");
}
retired = required.runtime;
}
Throwable failure = retireGenerationRuntimeCaches(retired.cacheId(), null);
try {
failure = shutdownSequence.closeDetachedGenerationRuntime(retired, failure);
} finally {
synchronized (lifecycleLock) {
detachedGenerationRuntimes.remove(retired);
retiringGenerationRuntimes.remove(retired);
}
}
if (failure != null) {
throw new IllegalStateException("Failed to close a detached Iris generation runtime.", failure);
}
}
public void addGenerationRuntimeRetirementListener(IntConsumer listener) {
generationRuntimeRetirementListeners.add(Objects.requireNonNull(listener, "generation runtime retirement listener"));
}
public void removeGenerationRuntimeRetirementListener(IntConsumer listener) {
generationRuntimeRetirementListeners.remove(listener);
}
Throwable retireGenerationRuntimeCaches(int runtimeId, Throwable failure) {
nativeStructureVolumeMemo.evictRuntime(runtimeId);
for (IntConsumer listener : generationRuntimeRetirementListeners) {
try {
listener.accept(runtimeId);
} catch (Throwable listenerFailure) {
if (failure == null) {
failure = listenerFailure;
} else if (failure != listenerFailure) {
failure.addSuppressed(listenerFailure);
}
}
}
return failure;
}
public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) {
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
synchronized (lifecycleLock) {
requireRunning("select the default generation runtime");
if (required.engine != this) {
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
}
EngineRuntime current = runtime;
GenerationRuntime previous = current.generation();
if (previous == required.runtime) {
return;
}
if (!detachedGenerationRuntimes.contains(required.runtime)) {
throw new IllegalStateException("Default Iris generation runtime must be live and engine-owned.");
}
if (retiringGenerationRuntimes.contains(required.runtime)) {
throw new IllegalStateException("Default Iris generation runtime cannot be retiring.");
}
detachedGenerationRuntimes.add(previous);
detachedGenerationRuntimes.remove(required.runtime);
runtime = current.withGeneration(required.runtime);
publishedTarget = required.runtime.target();
nativeStructureVolumeMemo.clear();
}
}
private boolean isPregeneratorActiveForThisWorld() {
return platformHooks.isPregeneratorActive(this);
}
@@ -576,13 +1104,23 @@ public class IrisEngine implements Engine {
}
}
@Override
public SeedManager getSeedManager() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.seedManager;
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? seedManager : current.seedManager();
}
@Override
public IrisComplex getComplex() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null && assembly.complex != null) {
return assembly.complex;
}
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? null : current.complex();
}
@@ -592,17 +1130,47 @@ public class IrisEngine implements Engine {
if (assembly != null) {
return assembly.target;
}
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? publishedTarget : current.target();
}
@Override
public IrisData getData() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.target.getData();
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? publishedTarget.getData() : current.data();
}
@Override
public IrisDimension getDimension() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.target.getDimension();
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? publishedTarget.getDimension() : current.dimension();
}
@Override
public EngineMantle getMantle() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null && assembly.mantle != null) {
return assembly.mantle;
}
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? null : current.mantle();
}
@Override
public EngineMode getMode() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null && assembly.mode != null) {
return assembly.mode;
}
EngineRuntime current = runtimeBuilder.requireRuntime("access the engine mode");
GenerationRuntime current = requireSelectedGenerationRuntime("access the engine mode");
return current.mode();
}
@@ -632,7 +1200,7 @@ public class IrisEngine implements Engine {
if (assembly != null) {
return assembly.upperContext;
}
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? null : current.upperContext();
}
@@ -642,7 +1210,7 @@ public class IrisEngine implements Engine {
if (assembly != null) {
return assembly.dimensionStackContext;
}
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? null : current.dimensionStackContext();
}
@@ -652,25 +1220,25 @@ public class IrisEngine implements Engine {
if (assembly != null && assembly.hash32 != null) {
return assembly.hash32;
}
EngineRuntime current = runtimeBuilder.requireRuntime("access the pack hash");
GenerationRuntime current = requireSelectedGenerationRuntime("access the pack hash");
return current.hash32();
}
@Override
public double getMaxBiomeObjectDensity() {
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? 0D : current.biomeMaxes().objectDensity();
}
@Override
public double getMaxBiomeLayerDensity() {
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? 0D : current.biomeMaxes().layerDensity();
}
@Override
public double getMaxBiomeDecoratorDensity() {
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? 0D : current.biomeMaxes().decoratorDensity();
}
@@ -717,7 +1285,9 @@ public class IrisEngine implements Engine {
@Override
public void generate(int x, int z, Hunk<PlatformBlockState> vblocks, Hunk<PlatformBiome> vbiomes, boolean multicore) throws WrongEngineBroException {
awaitGenerationCacheWarm();
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(x, z);
GenerationSessionLease lease = acquireGenerationLease("chunk_generate");
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
PrecisionStopwatch p = PrecisionStopwatch.start();
@@ -810,10 +1380,58 @@ public class IrisEngine implements Engine {
if (assembly != null) {
return assembly.cacheId;
}
EngineRuntime current = runtime;
GenerationRuntime current = selectedGenerationRuntime();
return current == null ? -1 : current.cacheId();
}
private GenerationRuntime selectedGenerationRuntime() {
GenerationRuntimeBinding scoped = generationRuntimeScopes.current();
if (scoped != null && isGenerationRuntimeBindingLive(scoped)) {
return scoped.runtime;
}
EngineRuntime current = runtime;
return current == null ? null : current.generation();
}
public GenerationKernelRegistry.Version getGenerationKernelVersion() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.kernelVersion;
}
return requireSelectedGenerationRuntime("resolve the generation kernel version").kernelVersion();
}
public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.runtimeKernel;
}
return requireSelectedGenerationRuntime("resolve the executable generation kernel").runtimeKernel();
}
public TransitionGenerationPlan getTransitionGenerationPlan() {
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.transitionPlan;
}
return requireSelectedGenerationRuntime("resolve the generation transition plan").transitionPlan();
}
private boolean isGenerationRuntimeBindingLive(GenerationRuntimeBinding binding) {
EngineRuntime current = runtime;
return (current != null && current.generation() == binding.runtime)
|| detachedGenerationRuntimes.contains(binding.runtime);
}
private GenerationRuntime requireSelectedGenerationRuntime(String operation) {
GenerationRuntime current = selectedGenerationRuntime();
if (current == null) {
throw new IllegalStateException("Cannot " + operation + " without an active Iris runtime for "
+ getWorld().name() + ".");
}
return current;
}
void requireRunning(String operation) {
if (closed || closing.get() || lifecycleState != LifecycleState.RUNNING || runtime == null) {
throw new IllegalStateException("Cannot " + operation + " while Iris engine " + getWorld().name()
@@ -867,4 +1485,71 @@ public class IrisEngine implements Engine {
}
}
public static final class GenerationRuntimeBinding {
private final IrisEngine engine;
private final GenerationRuntime runtime;
GenerationRuntimeBinding(IrisEngine engine, GenerationRuntime runtime) {
this.engine = Objects.requireNonNull(engine, "Iris engine");
this.runtime = Objects.requireNonNull(runtime, "generation runtime");
}
GenerationRuntime generationRuntime() {
return runtime;
}
public TransitionGenerationPlan transitionPlan() {
return runtime.transitionPlan();
}
public GenerationKernelRegistry.Version kernelVersion() {
return runtime.kernelVersion();
}
public GenerationKernelRegistry.RuntimeKernel runtimeKernel() {
return runtime.runtimeKernel();
}
public EngineTarget target() {
return runtime.target();
}
public Path mantleStorageDirectory() {
return runtime.mantleStorageDirectory();
}
public int runtimeId() {
return runtime.cacheId();
}
}
public static final class GenerationRuntimeScope implements AutoCloseable {
private final GenerationRuntimeScopeState state;
private final Thread owner;
private final GenerationRuntimeBinding previous;
private final GenerationRuntimeBinding installed;
private boolean closed;
GenerationRuntimeScope(
GenerationRuntimeScopeState state,
Thread owner,
GenerationRuntimeBinding previous,
GenerationRuntimeBinding installed
) {
this.state = state;
this.owner = owner;
this.previous = previous;
this.installed = installed;
}
@Override
public void close() {
if (closed) {
return;
}
state.close(owner, previous, installed);
closed = true;
}
}
}
@@ -23,6 +23,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MantlePass;
@@ -63,11 +64,13 @@ import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
@Data
@@ -76,6 +79,9 @@ import java.util.function.Supplier;
public class IrisEngineMantle implements EngineMantle {
public static final String STORAGE_FOLDER_NAME = "mantle-hydrology";
private final Engine engine;
private final Path storageDirectory;
@Getter(AccessLevel.NONE)
private final AtomicReference<IrisData> runtimeData;
private final Mantle<Matter> mantle;
@Getter(AccessLevel.NONE)
private final KMap<Integer, KList<MantleComponent>> components;
@@ -85,15 +91,32 @@ public class IrisEngineMantle implements EngineMantle {
private final MantleObjectComponent object;
public IrisEngineMantle(Engine engine) {
this.engine = engine;
this.mantle = createMantle(engine);
this(engine, legacyStorageDirectory(engine.getTarget()));
}
public IrisEngineMantle(Engine engine, Path storageDirectory) {
this.engine = Objects.requireNonNull(engine, "Iris engine");
this.storageDirectory = normalizeStorageDirectory(storageDirectory);
this.runtimeData = new AtomicReference<>(Objects.requireNonNull(engine.getData(), "Iris mantle data"));
this.mantle = createMantle(engine, this.storageDirectory, runtimeData::get);
components = new KMap<>();
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleHydrologyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
registerComponent(new IrisStructureComponent(this));
MantleObjectComponent createdObject;
try {
registerComponent(new MantleCarvingComponent(this));
registerComponent(new MantleHydrologyComponent(this));
createdObject = new MantleObjectComponent(this);
registerComponent(createdObject);
registerComponent(new MantleFloatingObjectComponent(this));
registerComponent(new IrisStructureComponent(this));
} catch (Throwable failure) {
try {
mantle.close();
} catch (Throwable closeFailure) {
failure.addSuppressed(closeFailure);
}
throw EngineShutdownSequence.propagate(failure);
}
object = createdObject;
}
@Override
@@ -163,6 +186,7 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public void hotload() {
runtimeData.set(Objects.requireNonNull(engine.getData(), "Iris mantle data"));
disabledFlags.reset();
for (MantleComponent component : registeredComponents.values()) {
component.hotload();
@@ -195,11 +219,24 @@ public class IrisEngineMantle implements EngineMantle {
return object;
}
private static Mantle<Matter> createMantle(Engine engine) {
static Path legacyStorageDirectory(EngineTarget target) {
EngineTarget requiredTarget = Objects.requireNonNull(target, "Iris engine target");
return normalizeStorageDirectory(requiredTarget.getWorld().worldFolder().toPath().resolve(STORAGE_FOLDER_NAME));
}
static Path normalizeStorageDirectory(Path storageDirectory) {
return Objects.requireNonNull(storageDirectory, "mantle storage directory").toAbsolutePath().normalize();
}
private static Mantle<Matter> createMantle(
Engine engine,
Path storageDirectory,
Supplier<IrisData> dataSupplier
) {
IrisMatterSupport.ensureRegistered();
File dataFolder = new File(engine.getWorld().worldFolder(), STORAGE_FOLDER_NAME);
File dataFolder = storageDirectory.toFile();
int worldHeight = engine.getTarget().getHeight();
MantleDataAdapter<Matter> adapter = createDataAdapter(engine::getData);
MantleDataAdapter<Matter> adapter = createDataAdapter(dataSupplier);
MantleHooks hooks = createHooks(EnginePanic.scoped("world " + engine.getWorld().name()));
art.arcane.volmlib.util.mantle.Mantle.RegionIO<TectonicPlate<Matter>> regionIO =
createRegionIO(dataFolder, worldHeight, adapter, hooks);
@@ -20,26 +20,36 @@ 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;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.project.stream.ProceduralStream;
public class UpperDimensionContext implements DataProvider {
private final DimensionTerrainContext terrainContext;
private final int chunkHeight;
private final ProceduralStream<Double> lowerHeightStream;
private final ProceduralStream<Double> unblendedLowerHeightStream;
private final TransitionGenerationPlan transitionPlan;
private final CeilingLayout ceilingLayout;
private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
this.terrainContext = terrainContext;
this.chunkHeight = chunkHeight;
IrisComplex complex = engine.getComplex();
lowerHeightStream = complex.getHeightStream();
unblendedLowerHeightStream = complex.getUnblendedNaturalHeightStream();
transitionPlan = complex.getTransitionGenerationPlan();
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
}
public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
return new UpperDimensionContext(
DimensionTerrainContext.forUpper(engine, upperDimension),
engine.getHeight()
engine
);
}
@@ -84,7 +94,47 @@ public class UpperDimensionContext implements DataProvider {
}
public int getUpperSurfaceY(int x, int z) {
return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
return ceilingLayout.height() - 1
- (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
}
public int getEffectiveSurfaceY(int x, int z) {
double depth = Math.max(
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))
);
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();
long surface = Math.max(0L, Math.max(mirroredSurface, gapSurface));
return surface >= layout.height() - 1L ? layout.height() : (int) surface;
}
public IrisBiome getUpperBiome(int x, int z) {
@@ -115,4 +165,12 @@ public class UpperDimensionContext implements DataProvider {
public boolean isSelfReferencing() {
return terrainContext.isSelfReferencing();
}
record CeilingLayout(int height, int minimumGap) {
CeilingLayout {
if (height <= 0 || minimumGap < 0) {
throw new IllegalArgumentException("Upper ceiling height must be positive and gap non-negative");
}
}
}
}
@@ -20,16 +20,21 @@ package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
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.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
@@ -37,16 +42,15 @@ import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import java.util.List;
import java.util.Objects;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
private final KMap<String, ResolvedBiome> resolvedPhysicalBiomes = new KMap<>();
public IrisBiomeActuator(Engine engine) {
super(engine, "Biome");
@@ -56,8 +60,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
@BlockCoordinates
@Override
public void onActuate(int x, int z, Hunk<PlatformBiome> h, boolean multicore, ChunkContext context) {
// No catch here: a partial biome write must fail the chunk (EngineAssignedActuator
// rethrows), or the chunk is flagged REAL with default plains columns forever.
PrecisionStopwatch p = PrecisionStopwatch.start();
int width = h.getWidth();
int depth = h.getDepth();
@@ -65,12 +67,28 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
for (int xf = 0; xf < width; xf++) {
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,
@@ -81,12 +99,8 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
worldZ
));
PlatformBiome platformBiome = resolved.biome();
writeColumn(h, mantle, xf, zf, worldX, worldZ, height, resolved);
if (platformBiome != null) {
h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
}
mantle.set(worldX, 0, worldZ, resolved.matter());
if (dimensionStackContext != null) {
PlatformBiome bottomBiome = platformBiome == null
? h.getRaw(xf, 0, zf)
@@ -108,6 +122,66 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
engine.getMetrics().getBiome().put(p.getMilliseconds());
}
private void writeHistoricalColumn(
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
int localX,
int localZ,
int worldX,
int worldZ,
int height,
Engine engine,
IrisComplex complex
) {
TransitionGenerationPlan transitionPlan = complex.getTransitionGenerationPlan();
if (transitionPlan == null) {
return;
}
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(worldX, worldZ);
int minimumWorldY = engine.getMinHeight();
String activeKey = sample.historicalPhysicalBiomeKeyAt(minimumWorldY).orElse(null);
int rangeStart = 0;
for (int internalY = 1; internalY <= height; internalY++) {
String nextKey = internalY == height
? null
: sample.historicalPhysicalBiomeKeyAt(minimumWorldY + internalY).orElse(null);
if (Objects.equals(activeKey, nextKey)) {
continue;
}
if (activeKey != null) {
applyBiomeRange(
localX,
localZ,
worldX,
worldZ,
rangeStart,
internalY - 1,
output,
mantle,
resolvePhysicalKey(activeKey)
);
}
activeKey = nextKey;
rangeStart = internalY;
}
}
private void writeColumn(
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
int localX,
int localZ,
int worldX,
int worldZ,
int height,
ResolvedBiome resolved
) {
if (resolved.biome() != null) {
output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome());
}
mantle.set(worldX, 0, worldZ, resolved.matter());
}
private void applyDimensionStackBiomes(
int localX,
int localZ,
@@ -221,31 +295,39 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
) {
if (biome.isCustom()) {
IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, x, y, z);
return dimension.getCustomBiomeKey(custom.getId());
return engine.getData().customBiomeResourceKey(dimension, custom);
}
return biome.getSkyBiomeKey(rng, engine, x, y, z);
}
/**
* The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair
* instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a
* biome registered later still resolves.
*/
private ResolvedBiome resolve(String key) {
ResolvedBiome cached = key == null ? null : resolvedBiomes.get(key);
if (cached != null) {
return cached;
}
IrisPlatform platform = IrisPlatforms.get();
PlatformBiome biome = platform.registries().biome(key);
ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key)));
ResolvedBiome resolved = new ResolvedBiome(
biome,
BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key))
);
if (key != null && biome != null) {
resolvedBiomes.put(key, resolved);
}
return resolved;
}
private ResolvedBiome resolvePhysicalKey(String key) {
ResolvedBiome cached = resolvedPhysicalBiomes.get(key);
if (cached != null) {
return cached;
}
PlatformBiome biome = IrisPlatforms.get().registries().biome(key);
ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(key));
if (biome != null) {
resolvedPhysicalBiomes.put(key, resolved);
}
return resolved;
}
@@ -163,12 +163,13 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
int emptyFor = 0;
int lastSolid = 0;
realZ = Math.round(z + j);
if (!getComplex().allowsNewDiscreteContentAt(realX, realZ)) {
continue;
}
height = context.getRoundedHeight(i, j);
biome = context.getBiome().get(i, j);
cave = shouldRay ? context.getCave().get(i, j) : null;
HydrologyColumnSample hydrology = getComplex().getHydrologyRuntime() == null
? null
: getComplex().getHydrologyRuntime().sample(realX, realZ).orElse(null);
HydrologyColumnSample hydrology = getComplex().sampleHydrologyColumn(realX, realZ);
if (isFallingWaterfallThroat(hydrology)) {
continue;
}
@@ -134,9 +134,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
int topY = Math.min(hf, chunkHeight - 1);
HydrologyColumnSample hydrology = complex.getHydrologyRuntime() == null
? null
: complex.getHydrologyRuntime().sample(realX, realZ).orElse(null);
HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(realX, realZ);
HydrologyColumnLayer hydrologyFluid = hydrology == null
? null
: hydrology.primarySurfaceFluidLayer().orElse(null);
@@ -252,9 +250,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
}
if (upperContext != null) {
int rawUpperSurface = upperContext.getUpperSurfaceY(realX, realZ);
int upperGap = dimension.getUpperDimensionGap();
int upperSurfaceY = Math.max(rawUpperSurface, he + upperGap);
int upperSurfaceY = upperContext.getEffectiveSurfaceY(realX, realZ);
if (upperSurfaceY < chunkHeight - 1) {
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
@@ -40,6 +41,10 @@ public interface EngineMode extends Staged {
default EngineStage burst(EngineStage... stages) {
return (x, z, blocks, biomes, multicore, ctx) -> {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine ? irisEngine : null;
IrisEngine.GenerationRuntimeBinding generationRuntime = generationEngine == null
? null
: generationEngine.captureGenerationRuntimeBinding();
BurstExecutor e = burst().burst(stages.length);
e.setMulticore(multicore);
// BurstExecutor.complete() logs-and-swallows stage failures; without re-propagation
@@ -52,7 +57,10 @@ public interface EngineMode extends Staged {
if (failure.get() != null) {
return;
}
try (IrisContext.Scope stageScope = IrisContext.open(getEngine(), ctx.getGenerationSessionId(), ctx)) {
try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null
? null
: generationEngine.openGenerationRuntimeScope(generationRuntime);
IrisContext.Scope stageScope = IrisContext.open(getEngine(), ctx.getGenerationSessionId(), ctx)) {
i.generate(x, z, blocks, biomes, multicore, ctx);
} catch (Throwable t) {
failure.compareAndSet(null, t);
@@ -98,8 +106,16 @@ public interface EngineMode extends Staged {
inline.generate(x, z, blocks, biomes, false, ctx);
return;
}
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine ? irisEngine : null;
IrisEngine.GenerationRuntimeBinding generationRuntime = generationEngine == null
? null
: generationEngine.captureGenerationRuntimeBinding();
java.util.concurrent.CompletableFuture<Void> background = burst().completeValueAsync(() -> {
fanOut.generate(x, z, blocks, biomes, true, ctx);
try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null
? null
: generationEngine.openGenerationRuntimeScope(generationRuntime)) {
fanOut.generate(x, z, blocks, biomes, true, ctx);
}
return null;
});
Throwable inlineFailure = null;
@@ -36,6 +36,7 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -108,86 +109,144 @@ public final class HintedLocator<T> implements Locator<T> {
return exact.matches(engine, chunk);
}
@Override
public boolean matchesForSearch(Engine engine, Position2 chunk) {
return exact.matchesForSearch(engine, chunk);
}
@Override
public Position2 nearestRecorded(Engine engine, Position2 origin, int maxChunkRadius) {
return exact.nearestRecorded(engine, origin, maxChunkRadius);
}
@Override
public SearchCandidate nearestRecordedCandidate(Engine engine, Position2 origin, int maxChunkRadius) {
return exact.nearestRecordedCandidate(engine, origin, maxChunkRadius);
}
@Override
public SearchCandidate candidateForMatchedChunk(Engine engine, Position2 chunk) {
return exact.candidateForMatchedChunk(engine, chunk);
}
@Override
public CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
}
SearchCandidate recorded = nearestRecordedCandidate(engine, pos, MAX_SAMPLE_RADIUS_CHUNKS);
int activeRadius = Locator.activeSearchRadius(pos, recorded, MAX_SAMPLE_RADIUS_CHUNKS);
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("hinted_locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SearchPlan plan = planner.apply(engine);
if (!plan.isPossible()) {
return null;
}
return search(engine, plan, pos, timeout, checks, stop);
SearchCandidate nearest = search(engine, plan, pos, activeRadius, recorded, timeout, checks, stop);
return nearest == null ? null : nearest.chunk();
}
});
return LocatorCanceller.requestScoped(search, stop);
}
private Position2 search(Engine engine, SearchPlan plan, Position2 pos, long timeout, Consumer<Integer> checks, AtomicBoolean stop) {
private SearchCandidate search(
Engine engine,
SearchPlan plan,
Position2 pos,
int maximumRadius,
SearchCandidate recorded,
long timeout,
Consumer<Integer> checks,
AtomicBoolean stop
) {
if (!plan.isPossible()
&& !GenerationLocatorPolicy.hasHistoricalFallbackInSquare(engine, pos, maximumRadius)) {
return recorded;
}
Locator<?> verifier = plan.getExactOverride() != null ? plan.getExactOverride() : exact;
int stride = Math.max(1, plan.getStrideChunks());
int batchTarget = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
int maxRing = Math.max(1, MAX_SAMPLE_RADIUS_CHUNKS / stride);
int batchTarget = Math.max(1, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32);
int maxRing = Math.max(0, (maximumRadius + stride - 1) / stride + 1);
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
AtomicInteger covered = new AtomicInteger();
AtomicReference<SearchCandidate> nearest = new AtomicReference<>(recorded);
KList<Position2> batch = new KList<>();
for (int ring = 0; ring <= maxRing; ring++) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
return nearest.get();
}
appendRing(batch, pos, ring, stride);
if (batch.size() < batchTarget && ring < maxRing) {
continue;
int cells = ring == 0 ? 1 : ring * 8;
for (int index = 0; index < cells; index++) {
Position2 sample = ringPosition(pos, ring, stride, index);
if (sample == null) {
continue;
}
batch.add(sample);
if (batch.size() < batchTarget) {
continue;
}
processBatch(engine, plan, verifier, batch, pos, maximumRadius, nearest,
stride, timeout, stopwatch, covered, checks, stop);
batch.clear();
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return nearest.get();
}
}
Position2 result = processBatch(engine, plan, verifier, batch, stride, timeout, stopwatch, covered, checks, stop);
batch.clear();
if (result != null) {
return result;
int radius = Locator.activeSearchRadius(pos, nearest.get(), maximumRadius);
if (ring == maxRing || nearest.get() != null && (long) ring * stride > radius) {
if (!batch.isEmpty()) {
processBatch(engine, plan, verifier, batch, pos, maximumRadius, nearest,
stride, timeout, stopwatch, covered, checks, stop);
}
return nearest.get();
}
}
return null;
return nearest.get();
}
private Position2 processBatch(Engine engine, SearchPlan plan, Locator<?> verifier, KList<Position2> batch, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicInteger covered, Consumer<Integer> checks, AtomicBoolean stop) {
private void processBatch(Engine engine, SearchPlan plan, Locator<?> verifier, KList<Position2> batch,
Position2 origin, int maximumRadius, AtomicReference<SearchCandidate> nearest,
int stride, long timeout, PrecisionStopwatch stopwatch, AtomicInteger covered,
Consumer<Integer> checks, AtomicBoolean stop) {
batch.sort(Comparator.comparingLong(sample -> chunkDistanceSquared(origin, sample)));
int size = batch.size();
boolean[] hits = new boolean[size];
boolean fine = stride <= 1;
CoarseSample coarse = plan.getCoarse();
AtomicBoolean matched = new AtomicBoolean(false);
int radius = Locator.activeSearchRadius(origin, nearest.get(), maximumRadius);
BurstExecutor executor = MultiBurst.burst.burst(size);
for (int i = 0; i < size; i++) {
int index = i;
Position2 sample = batch.get(i);
executor.queue(() -> {
if (stop.get() || engine.isClosing() || (fine && matched.get())) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout
|| !intersectsSearchRadius(origin, sample, fine ? 0 : stride, radius)) {
return;
}
int blockX = (sample.getX() << 4) + 8;
int blockZ = (sample.getZ() << 4) + 8;
boolean historicalFallback = GenerationLocatorPolicy.hasHistoricalFallbackInSquare(
engine,
sample,
fine ? 0 : stride
);
if (coarse != null && !coarse.test(blockX, blockZ)) {
if (!historicalFallback && (!plan.isPossible()
|| coarse != null && !coarse.test(blockX, blockZ))) {
return;
}
if (fine) {
if (verifier.matches(engine, sample)) {
hits[index] = true;
matched.set(true);
if (verifier.matchesForSearch(engine, sample)) {
recordCandidate(engine, sample, origin, nearest);
}
return;
@@ -198,86 +257,107 @@ public final class HintedLocator<T> implements Locator<T> {
}
executor.complete();
covered.addAndGet(size * stride * stride);
covered.updateAndGet(value -> (int) Math.min(Integer.MAX_VALUE,
(long) value + (long) size * stride * stride));
checks.accept(covered.get());
if (fine) {
return;
}
for (int i = 0; i < size; i++) {
if (!hits[i]) {
continue;
}
if (fine) {
return batch.get(i);
}
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
return;
}
Position2 refined = refine(engine, verifier, batch.get(i), stride, timeout, stopwatch, stop);
if (refined != null) {
return refined;
int searchRadius = Locator.activeSearchRadius(origin, nearest.get(), maximumRadius);
if (intersectsSearchRadius(origin, batch.get(i), stride, searchRadius)) {
refine(engine, verifier, batch.get(i), origin, searchRadius, nearest,
stride, timeout, stopwatch, stop);
}
}
return null;
}
private Position2 refine(Engine engine, Locator<?> verifier, Position2 candidate, int stride, long timeout, PrecisionStopwatch stopwatch, AtomicBoolean stop) {
private void refine(Engine engine, Locator<?> verifier, Position2 candidate, Position2 origin,
int searchRadius, AtomicReference<SearchCandidate> nearest,
int stride, long timeout, PrecisionStopwatch stopwatch, AtomicBoolean stop) {
KList<Position2> cells = new KList<>();
for (int ring = 0; ring <= stride; ring++) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout) {
return null;
return;
}
cells.clear();
appendRing(cells, candidate, ring, 1);
AtomicReference<Position2> found = new AtomicReference<>();
BurstExecutor executor = MultiBurst.burst.burst(cells.size());
for (Position2 cell : cells) {
executor.queue(() -> {
if (stop.get() || engine.isClosing() || found.get() != null) {
if (stop.get() || engine.isClosing() || stopwatch.getMilliseconds() >= timeout
|| !intersectsSearchRadius(origin, cell, 0, searchRadius)) {
return;
}
if (verifier.matches(engine, cell)) {
found.compareAndSet(null, cell);
if (verifier.matchesForSearch(engine, cell)) {
recordCandidate(engine, cell, origin, nearest);
}
});
}
executor.complete();
if (found.get() != null) {
return found.get();
}
}
}
return null;
private void recordCandidate(Engine engine, Position2 chunk, Position2 origin,
AtomicReference<SearchCandidate> nearest) {
SearchCandidate candidate = candidateForMatchedChunk(engine, chunk);
if (candidate != null) {
Locator.updateNearest(nearest, candidate, origin);
}
}
private static long chunkDistanceSquared(Position2 origin, Position2 sample) {
long deltaX = (long) origin.getX() - sample.getX();
long deltaZ = (long) origin.getZ() - sample.getZ();
return deltaX * deltaX + deltaZ * deltaZ;
}
private static boolean intersectsSearchRadius(Position2 origin, Position2 sample, int extent, int radius) {
return Math.abs((long) sample.getX() - origin.getX()) <= (long) radius + extent
&& Math.abs((long) sample.getZ() - origin.getZ()) <= (long) radius + extent;
}
static void appendRing(KList<Position2> batch, Position2 origin, int ring, int stride) {
int cells = ring == 0 ? 1 : ring * 8;
for (int index = 0; index < cells; index++) {
Position2 position = ringPosition(origin, ring, stride, index);
if (position != null) {
batch.add(position);
}
}
}
private static Position2 ringPosition(Position2 origin, int ring, int stride, int index) {
if (ring == 0) {
batch.add(origin);
return;
return origin;
}
int offset = ring * stride;
for (int dx = -ring; dx <= ring; dx++) {
int x = origin.getX() + (dx * stride);
batch.add(new Position2(x, origin.getZ() - offset));
batch.add(new Position2(x, origin.getZ() + offset));
}
for (int dz = -ring + 1; dz <= ring - 1; dz++) {
int z = origin.getZ() + (dz * stride);
batch.add(new Position2(origin.getX() - offset, z));
batch.add(new Position2(origin.getX() + offset, z));
int horizontalCells = (ring * 2 + 1) * 2;
int deltaX = index < horizontalCells ? index / 2 - ring : ((index & 1) == 0 ? -ring : ring);
int deltaZ = index < horizontalCells ? ((index & 1) == 0 ? -ring : ring)
: (index - horizontalCells) / 2 - ring + 1;
long chunkX = (long) origin.getX() + (long) deltaX * stride;
long chunkZ = (long) origin.getZ() + (long) deltaZ * stride;
if (chunkX < (Integer.MIN_VALUE >> 4) || chunkX > (Integer.MAX_VALUE >> 4)
|| chunkZ < (Integer.MIN_VALUE >> 4) || chunkZ > (Integer.MAX_VALUE >> 4)) {
return null;
}
return new Position2((int) chunkX, (int) chunkZ);
}
public static SearchPlan biomePlan(Engine engine, String biomeKey) {
@@ -0,0 +1,227 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.ChunkGenerationSemantics;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationSemanticIndex;
import art.arcane.volmlib.util.math.Position2;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
final class HistoryAwareLocator<T> implements Locator<T> {
private final GenerationSemanticIndex.SemanticKind kind;
private final String key;
private final Locator<T> active;
HistoryAwareLocator(
GenerationSemanticIndex.SemanticKind kind,
String key,
Locator<T> active
) {
this.kind = Objects.requireNonNull(kind, "kind");
String requiredKey = Objects.requireNonNull(key, "key");
if (requiredKey.isBlank()) {
throw new IllegalArgumentException("Locator key cannot be blank.");
}
this.key = requiredKey;
this.active = Objects.requireNonNull(active, "active locator");
}
@Override
public boolean matches(Engine engine, Position2 chunk) {
Optional<ChunkGenerationSemantics> recorded = GenerationLocatorPolicy.recorded(engine, chunk);
if (recorded.isPresent()) {
return contains(recorded.get());
}
if (!GenerationLocatorPolicy.allowsProceduralPrediction(engine, chunk)) {
return false;
}
return GenerationLocatorPolicy.evaluateScoped(
engine,
chunk,
() -> active.matches(engine, chunk)
);
}
@Override
public boolean matchesForSearch(Engine engine, Position2 chunk) {
if (GenerationLocatorPolicy.recorded(engine, chunk).isPresent()
|| !GenerationLocatorPolicy.allowsProceduralPrediction(engine, chunk)) {
return false;
}
return GenerationLocatorPolicy.evaluateScoped(
engine,
chunk,
() -> active.matches(engine, chunk)
);
}
@Override
public SearchCandidate nearestRecordedCandidate(Engine engine, Position2 origin, int maxChunkRadius) {
GenerationHistory history = GenerationLocatorPolicy.history(engine).orElse(null);
if (history == null) {
return null;
}
GenerationSemanticIndex.Match match = history.findRecorded(
GenerationSemanticIndex.Query.acrossActivations(
kind,
key,
new ChunkGenerationSemantics.BlockPosition(
blockCenter(origin.getX()),
0,
blockCenter(origin.getZ())
),
maxChunkRadius
)
).orElse(null);
if (match == null) {
return null;
}
ChunkGenerationSemantics.BlockPosition exactPosition = match.exactPosition().orElse(null);
if (exactPosition != null) {
return new SearchCandidate(
new Position2(match.chunk().chunkX(), match.chunk().chunkZ()),
exactPosition.x(),
exactPosition.z()
);
}
return SearchCandidate.atChunkCenter(
new Position2(match.chunk().chunkX(), match.chunk().chunkZ())
);
}
@Override
public SearchCandidate candidateForMatchedChunk(Engine engine, Position2 chunk) {
if (kind != GenerationSemanticIndex.SemanticKind.STRUCTURE) {
return SearchCandidate.atChunkCenter(chunk);
}
IrisStructureLocator.LocateResult result = GenerationLocatorPolicy.evaluateValueScoped(
engine,
chunk,
() -> IrisStructureLocator.locateInChunk(engine, key, chunk.getX(), chunk.getZ())
);
if (!result.found()) {
return null;
}
return new SearchCandidate(chunk, result.originX(), result.originZ());
}
private boolean contains(ChunkGenerationSemantics semantics) {
return switch (kind) {
case SURFACE_BIOME -> semantics.surfaceBiomeKeys().contains(key);
case CAVE_BIOME -> semantics.caveBiomeKeys().contains(key);
case REGION -> semantics.regionKeys().contains(key);
case RIVER_PROFILE -> semantics.riverProfileKeys().contains(key);
case OBJECT -> semantics.objectKeys().contains(key);
case STRUCTURE -> semantics.structures().stream()
.anyMatch(occurrence -> occurrence.key().equals(key));
};
}
private static int blockCenter(int chunkCoordinate) {
long center = (long) chunkCoordinate * 16L + 8L;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, center));
}
}
final class GenerationLocatorPolicy {
private GenerationLocatorPolicy() {
}
static Optional<GenerationHistory> history(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine)) {
return Optional.empty();
}
return irisEngine.getGenerationHistoryRuntimeRouter()
.map(GenerationHistoryRuntimeRouter::history);
}
static Optional<ChunkGenerationSemantics> recorded(Engine engine, Position2 chunk) {
GenerationHistory history = history(engine).orElse(null);
if (history == null) {
return Optional.empty();
}
return history.semantics(chunk.getX(), chunk.getZ())
.filter(ChunkGenerationSemantics::sealed);
}
static boolean allowsActivePrediction(Engine engine, Position2 chunk) {
if (!engine.getComplex().allowsNewGenerationChunk(chunk.getX(), chunk.getZ())) {
return false;
}
GenerationHistory history = history(engine).orElse(null);
if (history == null) {
return true;
}
return history.isActiveUnowned(chunk.getX(), chunk.getZ())
&& recorded(engine, chunk).isEmpty();
}
static boolean allowsHistoricalFallback(Engine engine, Position2 chunk) {
GenerationHistory history = history(engine).orElse(null);
return history != null
&& history.isHistoricallyOwned(chunk.getX(), chunk.getZ())
&& recorded(engine, chunk).isEmpty();
}
static boolean allowsProceduralPrediction(Engine engine, Position2 chunk) {
return allowsHistoricalFallback(engine, chunk) || allowsActivePrediction(engine, chunk);
}
static boolean hasHistoricalFallbackInSquare(
Engine engine,
Position2 center,
int radius
) {
if (radius < 0) {
throw new IllegalArgumentException("Historical fallback radius cannot be negative.");
}
GenerationHistory history = history(engine).orElse(null);
return history != null && history.hasHistoricalFallbackInSquare(center.getX(), center.getZ(), radius);
}
static boolean evaluateScoped(
Engine engine,
Position2 chunk,
BooleanSupplier query
) {
BooleanSupplier requiredQuery = Objects.requireNonNull(query, "query");
if (!(engine instanceof IrisEngine irisEngine)) {
return requiredQuery.getAsBoolean();
}
int blockX = Math.multiplyExact(chunk.getX(), 16);
int blockZ = Math.multiplyExact(chunk.getZ(), 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ)) {
return requiredQuery.getAsBoolean();
} catch (IOException exception) {
throw new IllegalStateException("Unable to scope an Iris locator query at chunk "
+ chunk.getX() + "," + chunk.getZ() + ".", exception);
}
}
static <T> T evaluateValueScoped(
Engine engine,
Position2 chunk,
Supplier<T> query
) {
Supplier<T> requiredQuery = Objects.requireNonNull(query, "query");
if (!(engine instanceof IrisEngine irisEngine)) {
return requiredQuery.get();
}
int blockX = Math.multiplyExact(chunk.getX(), 16);
int blockZ = Math.multiplyExact(chunk.getZ(), 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ)) {
return requiredQuery.get();
} catch (IOException exception) {
throw new IllegalStateException("Unable to scope an Iris locator query at chunk "
+ chunk.getX() + "," + chunk.getZ() + ".", exception);
}
}
}
@@ -178,6 +178,22 @@ public final class IrisStructureLocator {
return resolveInChunk(engine, key, cx, cz) != null;
}
public static LocateResult locateInChunk(Engine engine, String key, int chunkX, int chunkZ) {
if (!isPlaced(engine, key)) {
return NOT_FOUND_RESULT;
}
ResolvedStart resolved = resolveInChunk(engine, key, chunkX, chunkZ);
if (resolved == null) {
return NOT_FOUND_RESULT;
}
return new LocateResult(
LocateStatus.FOUND,
resolved.originX(),
resolved.baseY(),
resolved.originZ()
);
}
public static LocateResult locate(Engine engine, String key, int fromBlockX, int fromBlockZ, int maxRadiusChunks) {
return locate(engine, key, fromBlockX, fromBlockZ, maxRadiusChunks, (chunkX, chunkZ) -> true);
}
@@ -567,9 +583,17 @@ public final class IrisStructureLocator {
"assembled pieces exceed the configured structure or world bounds")) {
return null;
}
if (!allowsResolvedFootprint(engine, placement, pieces)) {
return null;
}
return new ResolvedPlacement(placement, selectedKey, structure, pieces, rng, originX, baseY, originZ, exactY);
}
public static boolean allowsResolvedFootprint(Engine engine, ResolvedPlacement resolved) {
Objects.requireNonNull(resolved, "resolved placement");
return allowsResolvedFootprint(engine, resolved.placement(), resolved.pieces());
}
public static boolean requirePlacementOutput(IrisStructurePlacement placement, String structureKey,
int chunkX, int chunkZ, boolean outputPresent,
String failureReason) {
@@ -876,6 +900,34 @@ public final class IrisStructureLocator {
return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
}
private static boolean allowsResolvedFootprint(
Engine engine,
IrisStructurePlacement placement,
KList<PlacedStructurePiece> pieces
) {
int[] bounds = computeBounds(pieces);
if (bounds == null) {
return false;
}
IrisStructureTerrain terrain = placement.resolvedTerrain();
IrisStructureTerrainMode mode = terrain.resolvedMode();
int padding = mode == IrisStructureTerrainMode.FORCE_CARVE
|| mode == IrisStructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding())
: 0;
return engine.getComplex().allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -padding),
saturatedOffset(bounds[2], -padding),
saturatedOffset(bounds[3], padding),
saturatedOffset(bounds[5], padding)
);
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private static int randomInclusive(RNG rng, int minimum, int maximum) {
if (minimum == maximum) {
return minimum;
@@ -18,43 +18,69 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.history.GenerationSemanticIndex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.math.BigInteger;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@FunctionalInterface
public interface Locator<T> {
record SearchCandidate(Position2 chunk, int blockX, int blockZ) {
public SearchCandidate {
if (chunk == null) {
throw new IllegalArgumentException("Locator candidate chunk cannot be null.");
}
}
public static SearchCandidate atChunkCenter(Position2 chunk) {
long blockX = (long) chunk.getX() * 16L + 8L;
long blockZ = (long) chunk.getZ() * 16L + 8L;
return new SearchCandidate(
chunk,
(int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, blockX)),
(int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, blockZ))
);
}
}
static Locator<IrisRegion> region(String loadKey) {
Locator<IrisRegion> exact = (e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisRegion> exact = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.REGION,
loadKey,
(e, c) -> e.getRegion((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey)
);
return new HintedLocator<>(exact, (engine) -> HintedLocator.regionPlan(engine, loadKey));
}
static Locator<IrisObject> object(String loadKey) {
Locator<IrisObject> exact = (e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey);
Locator<IrisObject> exact = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.OBJECT,
loadKey,
(e, c) -> e.getObjectsAt(c.getX(), c.getZ()).contains(loadKey)
);
return new HintedLocator<>(exact, (engine) -> HintedLocator.objectPlan(engine, loadKey));
}
static Locator<IrisBiome> surfaceBiome(String loadKey) {
Locator<IrisBiome> exact = (e, c) -> chunkContainsSurfaceBiome(e, c, loadKey);
Locator<IrisBiome> exact = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.SURFACE_BIOME,
loadKey,
(e, c) -> chunkContainsSurfaceBiome(e, c, loadKey)
);
return new HintedLocator<>(exact, (engine) -> HintedLocator.biomePlan(engine, loadKey));
}
@@ -71,7 +97,11 @@ public interface Locator<T> {
}
static Locator<art.arcane.iris.engine.object.IrisStructure> structure(String key) {
return (e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ());
return new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.STRUCTURE,
key,
(e, c) -> IrisStructureLocator.startsInChunk(e, key, c.getX(), c.getZ())
);
}
static Locator<BlockPos> poi(String type) {
@@ -82,12 +112,16 @@ public interface Locator<T> {
}
static Locator<IrisBiome> caveBiome(String loadKey) {
Locator<IrisBiome> exact = (e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey);
Locator<IrisBiome> exact = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.CAVE_BIOME,
loadKey,
(e, c) -> e.getCaveBiome((c.getX() << 4) + 8, (c.getZ() << 4) + 8).getLoadKey().equals(loadKey)
);
return new HintedLocator<>(exact, (engine) -> HintedLocator.caveBiomePlan(engine, loadKey));
}
static Locator<IrisBiome> caveOrMantleBiome(String loadKey) {
return (e, c) -> {
Locator<IrisBiome> active = (e, c) -> {
AtomicBoolean found = new AtomicBoolean(false);
try (GenerationSessionLease lease = e.acquireGenerationLease("locator_generate_matter")) {
ChunkContext chunkContext = new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null);
@@ -109,58 +143,114 @@ public interface Locator<T> {
return found.get();
};
return new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.CAVE_BIOME,
loadKey,
active
);
}
boolean matches(Engine engine, Position2 chunk);
default CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
if (engine.isClosed()) {
throw new WrongEngineBroException();
default boolean matchesForSearch(Engine engine, Position2 chunk) {
if (!GenerationLocatorPolicy.allowsActivePrediction(engine, chunk)) {
return false;
}
return GenerationLocatorPolicy.evaluateScoped(
engine,
chunk,
() -> matches(engine, chunk)
);
}
AtomicBoolean stop = new AtomicBoolean(false);
CompletableFuture<Position2> search = MultiBurst.burst.completeValueAsync(() -> {
try (GenerationSessionLease lease = engine.acquireGenerationLease("locator_search");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
AtomicInteger searched = new AtomicInteger();
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && !engine.isClosing() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
default Position2 nearestRecorded(Engine engine, Position2 origin, int maxChunkRadius) {
SearchCandidate candidate = nearestRecordedCandidate(engine, origin, maxChunkRadius);
return candidate == null ? null : candidate.chunk();
}
for (int i = 0; i < tc; i++) {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get() || stop.get() || engine.isClosing()) {
return;
}
if (matches(engine, p)) {
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
});
}
default SearchCandidate nearestRecordedCandidate(Engine engine, Position2 origin, int maxChunkRadius) {
return null;
}
e.complete();
checks.accept(searched.get());
}
default SearchCandidate candidateForMatchedChunk(Engine engine, Position2 chunk) {
return SearchCandidate.atChunkCenter(chunk);
}
if (found.get() && foundPos.get() != null) {
return foundPos.get();
}
default CompletableFuture<Position2> find(Engine engine, Position2 pos, long timeout, Consumer<Integer> checks) throws WrongEngineBroException {
return new HintedLocator<>(this, ignored -> HintedLocator.SearchPlan.unpruned())
.find(engine, pos, timeout, checks);
}
return null;
static int activeSearchRadius(
Position2 origin,
SearchCandidate recorded,
int maximumRadius
) {
if (recorded == null) {
return maximumRadius;
}
BigInteger distance = distanceSquared(origin, recorded).sqrt();
BigInteger chunks = distance.add(BigInteger.valueOf(15L)).divide(BigInteger.valueOf(16L));
return Math.min(maximumRadius, chunks.min(BigInteger.valueOf(Integer.MAX_VALUE - 2L)).intValue() + 2);
}
static void updateNearest(
AtomicReference<SearchCandidate> nearest,
SearchCandidate candidate,
Position2 origin
) {
SearchCandidate current = nearest.get();
while (current == null || compareCandidates(origin, candidate, current) < 0) {
if (nearest.compareAndSet(current, candidate)) {
return;
}
});
return LocatorCanceller.requestScoped(search, stop);
current = nearest.get();
}
}
static SearchCandidate nearer(
Position2 origin,
SearchCandidate first,
SearchCandidate second
) {
if (first == null) {
return second;
}
if (second == null) {
return first;
}
return compareCandidates(origin, first, second) <= 0 ? first : second;
}
private static int compareCandidates(
Position2 origin,
SearchCandidate first,
SearchCandidate second
) {
int distanceComparison = distanceSquared(origin, first).compareTo(distanceSquared(origin, second));
if (distanceComparison != 0) {
return distanceComparison;
}
int xComparison = Integer.compare(first.blockX(), second.blockX());
if (xComparison != 0) {
return xComparison;
}
int zComparison = Integer.compare(first.blockZ(), second.blockZ());
if (zComparison != 0) {
return zComparison;
}
int chunkXComparison = Integer.compare(first.chunk().getX(), second.chunk().getX());
if (chunkXComparison != 0) {
return chunkXComparison;
}
return Integer.compare(first.chunk().getZ(), second.chunk().getZ());
}
private static BigInteger distanceSquared(Position2 origin, SearchCandidate candidate) {
BigInteger originX = BigInteger.valueOf((long) origin.getX() * 16L + 8L);
BigInteger originZ = BigInteger.valueOf((long) origin.getZ() * 16L + 8L);
BigInteger deltaX = originX.subtract(BigInteger.valueOf(candidate.blockX()));
BigInteger deltaZ = originZ.subtract(BigInteger.valueOf(candidate.blockZ()));
return deltaX.multiply(deltaX).add(deltaZ.multiply(deltaZ));
}
}
@@ -1,5 +1,8 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
@@ -8,13 +11,18 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.IntConsumer;
public final class NativeStructureOwnershipStore {
private static final int MAX_CACHED_AUTHORITIES = 65_536;
@@ -175,6 +183,7 @@ public final class NativeStructureOwnershipStore {
}
closed = true;
authorities.invalidateAll();
storage.close();
} finally {
lifecycleLock.writeLock().unlock();
}
@@ -224,21 +233,36 @@ public final class NativeStructureOwnershipStore {
void remove(long target, String structureKey, int originChunkX, int originChunkZ);
boolean flush();
default void close() {
}
}
private static final class MantleStorage implements Storage {
static final class MantleStorage implements Storage {
private final WeakReference<Engine> engine;
private final Map<Long, NativeStructureOwnershipBundle> pendingBundles;
private final Map<PendingTarget, NativeStructureOwnershipBundle> pendingBundles;
private final WeakReference<IrisEngine> retirementEngine;
private final IntConsumer retirementListener;
private MantleStorage(Engine engine) {
this.engine = new WeakReference<>(Objects.requireNonNull(engine,
"Native structure ownership store requires an engine"));
MantleStorage(Engine engine) {
Engine required = Objects.requireNonNull(engine,
"Native structure ownership store requires an engine");
this.engine = new WeakReference<>(required);
this.pendingBundles = new ConcurrentHashMap<>();
if (required instanceof IrisEngine irisEngine) {
this.retirementEngine = new WeakReference<>(irisEngine);
this.retirementListener = this::flushRuntime;
irisEngine.addGenerationRuntimeRetirementListener(retirementListener);
} else {
this.retirementEngine = null;
this.retirementListener = null;
}
}
@Override
public NativeStructureOwnershipBundle read(int chunkX, int chunkZ) {
MantleChunk<Matter> chunk = mantle().getChunk(chunkX, chunkZ).use();
RuntimeMantleOwner owner = owner();
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(chunkX, chunkZ).use();
try {
synchronized (chunk) {
return chunk.get(0, 0, 0, NativeStructureOwnershipBundle.class);
@@ -250,9 +274,10 @@ public final class NativeStructureOwnershipStore {
@Override
public void write(long target, NativeStructureOwnershipRecord record) {
RuntimeMantleOwner owner = owner();
int targetChunkX = unpackX(target);
int targetChunkZ = unpackZ(target);
MantleChunk<Matter> chunk = mantle().getChunk(targetChunkX, targetChunkZ).use();
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(targetChunkX, targetChunkZ).use();
try {
synchronized (chunk) {
Matter section = chunk.getOrCreate(0);
@@ -262,7 +287,7 @@ public final class NativeStructureOwnershipStore {
NativeStructureOwnershipBundle updated = Objects.requireNonNullElseGet(
bundle, NativeStructureOwnershipBundle::empty).with(record);
slice.set(0, 0, 0, updated);
pendingBundles.put(target, updated);
pendingBundles.put(new PendingTarget(owner, target), updated);
}
} finally {
chunk.release();
@@ -272,9 +297,10 @@ public final class NativeStructureOwnershipStore {
@Override
public void remove(long target, String structureKey,
int originChunkX, int originChunkZ) {
RuntimeMantleOwner owner = owner();
int targetChunkX = unpackX(target);
int targetChunkZ = unpackZ(target);
MantleChunk<Matter> chunk = mantle().getChunk(targetChunkX, targetChunkZ).use();
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(targetChunkX, targetChunkZ).use();
try {
synchronized (chunk) {
Matter section = chunk.getOrCreate(0);
@@ -294,7 +320,7 @@ public final class NativeStructureOwnershipStore {
}
slice.set(0, 0, 0, updated.records().isEmpty() ? null : updated);
section.trimSlices();
pendingBundles.put(target, updated);
pendingBundles.put(new PendingTarget(owner, target), updated);
}
} finally {
chunk.release();
@@ -303,27 +329,67 @@ public final class NativeStructureOwnershipStore {
@Override
public boolean flush() {
Map<Long, NativeStructureOwnershipBundle> pending = new TreeMap<>(pendingBundles);
return flushMatching(null);
}
@Override
public void close() {
IrisEngine activeRetirementEngine = retirementEngine == null ? null : retirementEngine.get();
if (activeRetirementEngine != null) {
activeRetirementEngine.removeGenerationRuntimeRetirementListener(retirementListener);
}
}
private void flushRuntime(int runtimeId) {
if (!flushMatching(runtimeId)) {
throw new IllegalStateException("Cannot retire generation runtime " + runtimeId
+ " while native structure ownership regions are in use.");
}
}
private boolean flushMatching(Integer runtimeId) {
List<Map.Entry<PendingTarget, NativeStructureOwnershipBundle>> pending = new ArrayList<>();
for (Map.Entry<PendingTarget, NativeStructureOwnershipBundle> entry : pendingBundles.entrySet()) {
if (runtimeId == null || entry.getKey().owner().runtimeId() == runtimeId) {
pending.add(Map.entry(entry.getKey(), entry.getValue()));
}
}
if (pending.isEmpty()) {
return true;
}
pending.sort(Comparator
.comparingLong((Map.Entry<PendingTarget, NativeStructureOwnershipBundle> entry) ->
entry.getKey().owner().activationId())
.thenComparingInt(entry -> entry.getKey().owner().runtimeId())
.thenComparingLong(entry -> entry.getKey().target()));
Mantle<Matter> mantle = mantle();
Set<Long> regions = new TreeSet<>();
for (Map.Entry<Long, NativeStructureOwnershipBundle> entry : pending.entrySet()) {
long target = entry.getKey();
restorePendingBundle(mantle, target, entry.getValue());
regions.add(Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5));
Map<RuntimeMantleOwner, Set<Long>> regionsByOwner = new LinkedHashMap<>();
for (Map.Entry<PendingTarget, NativeStructureOwnershipBundle> entry : pending) {
PendingTarget pendingTarget = entry.getKey();
RuntimeMantleOwner owner = pendingTarget.owner();
restorePendingBundle(owner.mantle().getMantle(), pendingTarget.target(), entry.getValue());
regionsByOwner.computeIfAbsent(owner, ignored -> new TreeSet<>()).add(
regionKey(pendingTarget.target()));
}
Set<Long> deferredRegions = mantle.saveIdleTectonicPlates(regions);
for (Map.Entry<Long, NativeStructureOwnershipBundle> entry : pending.entrySet()) {
long target = entry.getKey();
long region = Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5);
if (!deferredRegions.contains(region)) {
pendingBundles.remove(target, entry.getValue());
boolean complete = true;
for (Map.Entry<RuntimeMantleOwner, Set<Long>> ownerRegions : regionsByOwner.entrySet()) {
RuntimeMantleOwner owner = ownerRegions.getKey();
Set<Long> deferredRegions = owner.mantle().getMantle().saveIdleTectonicPlates(
ownerRegions.getValue());
if (!deferredRegions.isEmpty()) {
complete = false;
}
for (Map.Entry<PendingTarget, NativeStructureOwnershipBundle> entry : pending) {
PendingTarget pendingTarget = entry.getKey();
if (!pendingTarget.owner().equals(owner)
|| deferredRegions.contains(regionKey(pendingTarget.target()))) {
continue;
}
pendingBundles.remove(pendingTarget, entry.getValue());
}
}
return deferredRegions.isEmpty();
return complete;
}
private void restorePendingBundle(Mantle<Matter> mantle, long target,
@@ -342,12 +408,55 @@ public final class NativeStructureOwnershipStore {
}
}
private Mantle<Matter> mantle() {
private RuntimeMantleOwner owner() {
Engine active = engine.get();
if (active == null) {
throw new IllegalStateException("Native structure ownership engine is unavailable");
}
return active.getMantle().getMantle();
EngineMantle mantle = active.getMantle();
int runtimeId = active.getCacheID();
if (!(active instanceof IrisEngine irisEngine)) {
return new RuntimeMantleOwner(0L, runtimeId, null, mantle);
}
Optional<GenerationHistoryRuntimeRouter> attached = irisEngine.getGenerationHistoryRuntimeRouter();
if (attached.isEmpty()) {
return new RuntimeMantleOwner(0L, runtimeId, null, mantle);
}
GenerationHistoryRuntimeRouter.RuntimeOwnership routed = attached.get()
.currentRuntimeOwnership()
.orElseThrow(() -> new IllegalStateException(
"Native structure ownership requires an active generation-history runtime route."));
if (routed.binding().runtimeId() != runtimeId) {
throw new IllegalStateException("Native structure ownership runtime route changed during capture.");
}
return new RuntimeMantleOwner(routed.activationId(), runtimeId, routed.binding(), mantle);
}
private static long regionKey(long target) {
return Mantle.key(unpackX(target) >> 5, unpackZ(target) >> 5);
}
}
private record RuntimeMantleOwner(
long activationId,
int runtimeId,
IrisEngine.GenerationRuntimeBinding binding,
EngineMantle mantle
) {
private RuntimeMantleOwner {
if (activationId < 0L) {
throw new IllegalArgumentException("Generation activation ID cannot be negative.");
}
Objects.requireNonNull(mantle, "native structure ownership mantle");
if (activationId > 0L && binding == null) {
throw new IllegalArgumentException("Generation-history ownership requires a runtime binding.");
}
}
}
private record PendingTarget(RuntimeMantleOwner owner, long target) {
private PendingTarget {
Objects.requireNonNull(owner, "native structure ownership runtime");
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisStructureAnchorMode;
@@ -20,9 +21,13 @@ public final class NativeStructurePlacementPlanner {
}
public static KList<NativeStructureStartPlan> plansAt(Engine engine, int chunkX, int chunkZ) {
Engine activeEngine = Objects.requireNonNull(engine, "Native structure planner requires an engine");
if (!allowsStart(activeEngine, chunkX, chunkZ)) {
return new KList<>();
}
Map<String, NativeStructureStartPlan> plansByStructure = new LinkedHashMap<>();
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(engine, chunkX, chunkZ)) {
NativeStructureStartPlan plan = planAt(engine, placement, chunkX, chunkZ);
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(activeEngine, chunkX, chunkZ)) {
NativeStructureStartPlan plan = planAt(activeEngine, placement, chunkX, chunkZ);
if (plan != null) {
String structureKey = normalize(plan.source().getStructure());
NativeStructureStartPlan current = plansByStructure.get(structureKey);
@@ -41,6 +46,9 @@ public final class NativeStructurePlacementPlanner {
return null;
}
Engine activeEngine = Objects.requireNonNull(engine, "Native structure planner requires an engine");
if (!allowsStart(activeEngine, chunkX, chunkZ)) {
return null;
}
if (activeEngine.getSeedManager() == null) {
throw new IllegalStateException("Native structure planner requires a bound seed manager");
}
@@ -163,6 +171,11 @@ public final class NativeStructurePlacementPlanner {
return Engine.hostHeight(engine, blockX, blockZ, true) < localFluidHeight;
}
private static boolean allowsStart(Engine engine, int chunkX, int chunkZ) {
IrisComplex complex = engine.getComplex();
return complex == null || complex.allowsNewGenerationChunk(chunkX, chunkZ);
}
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
long leftIdentity = StructurePlacementGrid.placementIdentity(left);
long rightIdentity = StructurePlacementGrid.placementIdentity(right);
@@ -21,6 +21,9 @@ package art.arcane.iris.engine.framework;
import art.arcane.volmlib.util.collection.KList;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.HashMap;
import java.util.Map;
/**
* Chunk keyed memo in front of the platform volume hook. One chunk's object pass fires the same rect query dozens of
* times, so the hook is asked once per chunk column and every later object reuses that answer. Long keyed so the
@@ -29,8 +32,8 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
public final class NativeStructureVolumeMemo {
private static final int MAX_CACHED_CHUNKS = 2_048;
private final Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>> chunks =
new Long2ObjectLinkedOpenHashMap<>();
private final Map<Integer, Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>>> runtimes =
new HashMap<>();
public KList<NativeStructureVolume> volumes(Engine engine, EnginePlatformHooks hooks,
int minX, int minZ, int maxX, int maxZ) {
@@ -62,15 +65,27 @@ public final class NativeStructureVolumeMemo {
}
public void clear() {
synchronized (chunks) {
chunks.clear();
synchronized (runtimes) {
runtimes.clear();
}
}
public void evictRuntime(int runtimeId) {
synchronized (runtimes) {
runtimes.remove(runtimeId);
}
}
private KList<NativeStructureVolume> chunkVolumes(Engine engine, EnginePlatformHooks hooks,
int chunkX, int chunkZ) {
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
synchronized (chunks) {
int runtimeId = engine.getCacheID();
synchronized (runtimes) {
Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>> chunks = runtimes.get(runtimeId);
if (chunks == null) {
chunks = new Long2ObjectLinkedOpenHashMap<>();
runtimes.put(runtimeId, chunks);
}
KList<NativeStructureVolume> hit = chunks.getAndMoveToFirst(key);
if (hit != null) {
return hit;
@@ -83,7 +98,10 @@ public final class NativeStructureVolumeMemo {
if (resolved == null) {
resolved = NativeStructureVolume.NONE;
}
synchronized (chunks) {
synchronized (runtimes) {
Long2ObjectLinkedOpenHashMap<KList<NativeStructureVolume>> chunks = runtimes.computeIfAbsent(
runtimeId,
ignored -> new Long2ObjectLinkedOpenHashMap<>());
chunks.putAndMoveToFirst(key, resolved);
while (chunks.size() > MAX_CACHED_CHUNKS) {
chunks.removeLast();
@@ -0,0 +1,467 @@
package art.arcane.iris.engine.history;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public final class ChunkGenerationOwnership {
private static final int MAXIMUM_CACHED_REGIONS = 64;
private final Path directory;
private final LinkedHashMap<Long, RegionGenerationOwnership> regions;
private final Long2IntOpenHashMap regionAssignmentCounts;
private final ReentrantReadWriteLock lock;
private int explicitChunkCount;
private ChunkGenerationOwnership(Path directory) {
this.directory = directory.toAbsolutePath().normalize();
regions = new LinkedHashMap<>(MAXIMUM_CACHED_REGIONS, 0.75F, true);
regionAssignmentCounts = new Long2IntOpenHashMap();
regionAssignmentCounts.defaultReturnValue(-1);
lock = new ReentrantReadWriteLock();
}
public static ChunkGenerationOwnership load(Path directory) throws IOException {
ChunkGenerationOwnership ownership = new ChunkGenerationOwnership(directory);
ownership.loadRegionCatalog();
return ownership;
}
public long resolve(int chunkX, int chunkZ, long currentActivation) {
requireActivation(currentActivation);
lock.writeLock().lock();
try {
RegionGenerationOwnership region = loadExistingRegion(chunkX >> 5, chunkZ >> 5);
return region == null ? currentActivation : region.resolve(chunkX, chunkZ, currentActivation);
} catch (IOException error) {
throw lookupFailure(chunkX, chunkZ, error);
} finally {
lock.writeLock().unlock();
}
}
public boolean isExplicitlyAssigned(int chunkX, int chunkZ) {
lock.writeLock().lock();
try {
RegionGenerationOwnership region = loadExistingRegion(chunkX >> 5, chunkZ >> 5);
return region != null && region.hasAssignment(chunkX, chunkZ);
} catch (IOException error) {
throw lookupFailure(chunkX, chunkZ, error);
} finally {
lock.writeLock().unlock();
}
}
public boolean anyMatchingInSquare(int centerX, int centerZ, int radius, AssignmentPredicate predicate) {
if (radius < 0) {
throw new IllegalArgumentException("Ownership search radius cannot be negative.");
}
Objects.requireNonNull(predicate, "predicate");
int minimumX = (int) Math.max(Integer.MIN_VALUE, (long) centerX - radius);
int maximumX = (int) Math.min(Integer.MAX_VALUE, (long) centerX + radius);
int minimumZ = (int) Math.max(Integer.MIN_VALUE, (long) centerZ - radius);
int maximumZ = (int) Math.min(Integer.MAX_VALUE, (long) centerZ + radius);
int minimumRegionX = minimumX >> 5;
int maximumRegionX = maximumX >> 5;
int minimumRegionZ = minimumZ >> 5;
int maximumRegionZ = maximumZ >> 5;
long regionCount = ((long) maximumRegionX - minimumRegionX + 1L)
* ((long) maximumRegionZ - minimumRegionZ + 1L);
lock.writeLock().lock();
try {
if (regionCount > regionAssignmentCounts.size()) {
for (long key : regionAssignmentCounts.keySet()) {
int x = regionX(key);
int z = regionZ(key);
if (x >= minimumRegionX && x <= maximumRegionX
&& z >= minimumRegionZ && z <= maximumRegionZ
&& matchesRegion(x, z, minimumX, minimumZ, maximumX, maximumZ, predicate)) {
return true;
}
}
return false;
}
for (int x = minimumRegionX; x <= maximumRegionX; x++) {
for (int z = minimumRegionZ; z <= maximumRegionZ; z++) {
if (matchesRegion(x, z, minimumX, minimumZ, maximumX, maximumZ, predicate)) {
return true;
}
}
}
return false;
} catch (IOException error) {
throw new IllegalStateException("Unable to search generation ownership near chunk "
+ centerX + "," + centerZ + ".", error);
} finally {
lock.writeLock().unlock();
}
}
public long explicitActivation(int chunkX, int chunkZ) {
lock.writeLock().lock();
try {
RegionGenerationOwnership region = loadExistingRegion(chunkX >> 5, chunkZ >> 5);
if (region == null) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " has no explicit generation activation"
);
}
return region.explicitActivation(chunkX, chunkZ);
} catch (IOException error) {
throw lookupFailure(chunkX, chunkZ, error);
} finally {
lock.writeLock().unlock();
}
}
public boolean assign(int chunkX, int chunkZ, long activation) throws IOException {
requireActivation(activation);
lock.writeLock().lock();
try {
RegionGenerationOwnership region = regionForAssignment(chunkX, chunkZ);
boolean assigned = region.assign(chunkX, chunkZ, activation);
if (assigned) {
recordAssignment(region);
}
return assigned;
} finally {
lock.writeLock().unlock();
}
}
public int assignAll(WorldChunkInventory inventory, long activation) throws IOException {
requireActivation(activation);
lock.writeLock().lock();
try {
validateAssignments(inventory, activation);
int[] assigned = new int[1];
inventory.forEach((chunkX, chunkZ) -> {
RegionGenerationOwnership region = regionForAssignment(chunkX, chunkZ);
if (region.assign(chunkX, chunkZ, activation)) {
recordAssignment(region);
assigned[0]++;
}
});
return assigned[0];
} finally {
lock.writeLock().unlock();
}
}
public int assignUnassigned(WorldChunkInventory inventory, long activation) throws IOException {
requireActivation(activation);
lock.writeLock().lock();
try {
int[] assigned = new int[1];
inventory.forEach((chunkX, chunkZ) -> {
RegionGenerationOwnership existing = loadExistingRegion(chunkX >> 5, chunkZ >> 5);
if (existing != null && existing.hasAssignment(chunkX, chunkZ)) {
return;
}
RegionGenerationOwnership region = regionForAssignment(chunkX, chunkZ);
if (region.assign(chunkX, chunkZ, activation)) {
recordAssignment(region);
assigned[0]++;
}
});
return assigned[0];
} finally {
lock.writeLock().unlock();
}
}
public int persist() throws IOException {
lock.writeLock().lock();
try {
requireSafeParent();
int persisted = 0;
for (RegionGenerationOwnership region : regions.values()) {
if (region.persist(directory)) {
persisted++;
}
}
return persisted;
} finally {
lock.writeLock().unlock();
}
}
public int explicitChunkCount() {
lock.readLock().lock();
try {
return explicitChunkCount;
} finally {
lock.readLock().unlock();
}
}
public void forEachAssignment(AssignmentConsumer consumer) throws IOException {
lock.writeLock().lock();
try {
for (long regionKey : orderedRegionKeys()) {
RegionGenerationOwnership region = loadExistingRegion(regionX(regionKey), regionZ(regionKey));
if (region != null) {
region.forEachAssignment(consumer::accept);
}
}
} finally {
lock.writeLock().unlock();
}
}
public long[] snapshotExplicitChunkKeys() {
lock.writeLock().lock();
try {
long[] chunks = new long[explicitChunkCount];
int[] cursor = new int[1];
try {
forEachAssignment((chunkX, chunkZ, activationId) -> chunks[cursor[0]++] = packChunk(chunkX, chunkZ));
} catch (IOException error) {
throw new IllegalStateException("Unable to materialize generation ownership", error);
}
if (cursor[0] != chunks.length) {
throw new IllegalStateException(
"Generation ownership changed while creating a snapshot: expected "
+ chunks.length + " chunks but found " + cursor[0]
);
}
Arrays.sort(chunks);
return chunks;
} finally {
lock.writeLock().unlock();
}
}
int cachedRegionCount() {
lock.readLock().lock();
try {
return regions.size();
} finally {
lock.readLock().unlock();
}
}
int regionCount() {
lock.readLock().lock();
try {
return regionAssignmentCounts.size();
} finally {
lock.readLock().unlock();
}
}
public static long packChunk(int chunkX, int chunkZ) {
return ((long) chunkX << Integer.SIZE) | (chunkZ & 0xFFFFFFFFL);
}
public static int chunkX(long packedChunk) {
return (int) (packedChunk >> Integer.SIZE);
}
public static int chunkZ(long packedChunk) {
return (int) packedChunk;
}
private void loadRegionCatalog() throws IOException {
if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)
&& !Files.isSymbolicLink(directory)) {
Path parent = directory.getParent();
if (parent != null
&& !Files.exists(parent, LinkOption.NOFOLLOW_LINKS)
&& !Files.isSymbolicLink(parent)) {
return;
}
}
requireSafeParent();
if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
return;
}
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership path is not a directory: " + directory);
}
List<Path> files = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(
directory,
"*" + RegionGenerationOwnership.FILE_SUFFIX
)) {
for (Path file : stream) {
files.add(file);
}
}
files.sort(Comparator.comparing(path -> path.getFileName().toString()));
for (Path file : files) {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership shard is not a regular file: " + file);
}
RegionGenerationOwnership.Metadata metadata = RegionGenerationOwnership.readMetadata(file);
String expectedName = RegionGenerationOwnership.fileName(metadata.regionX(), metadata.regionZ());
if (!file.getFileName().toString().equals(expectedName)) {
throw new IOException("Generation ownership shard name does not match its coordinates: " + file);
}
long regionKey = packRegion(metadata.regionX(), metadata.regionZ());
if (regionAssignmentCounts.putIfAbsent(regionKey, metadata.assignmentCount()) != -1) {
throw new IOException("Duplicate generation ownership shard for region "
+ metadata.regionX() + "," + metadata.regionZ());
}
try {
explicitChunkCount = Math.addExact(explicitChunkCount, metadata.assignmentCount());
} catch (ArithmeticException error) {
throw new IOException("Generation ownership contains too many assigned chunks", error);
}
}
}
private void requireSafeParent() throws IOException {
Path parent = directory.getParent();
if (parent == null || !Files.exists(parent, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership parent directory is missing: " + parent);
}
BasicFileAttributes attributes = Files.readAttributes(
parent,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException("Generation ownership parent path is not a safe directory: " + parent);
}
}
private void validateAssignments(WorldChunkInventory inventory, long activation) throws IOException {
inventory.forEach((chunkX, chunkZ) -> {
RegionGenerationOwnership region = loadExistingRegion(chunkX >> 5, chunkZ >> 5);
if (region == null || !region.hasAssignment(chunkX, chunkZ)) {
return;
}
long existing = region.explicitActivation(chunkX, chunkZ);
if (existing != activation) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " already belongs to activation " + existing
+ " and cannot be reassigned to " + activation
);
}
});
}
private RegionGenerationOwnership regionForAssignment(int chunkX, int chunkZ) throws IOException {
int regionX = chunkX >> 5;
int regionZ = chunkZ >> 5;
long regionKey = packRegion(regionX, regionZ);
RegionGenerationOwnership region = regions.get(regionKey);
if (region != null) {
return region;
}
region = regionAssignmentCounts.containsKey(regionKey)
? readRegion(regionX, regionZ)
: new RegionGenerationOwnership(regionX, regionZ);
cacheRegion(regionKey, region);
return region;
}
private RegionGenerationOwnership loadExistingRegion(int regionX, int regionZ) throws IOException {
long regionKey = packRegion(regionX, regionZ);
RegionGenerationOwnership region = regions.get(regionKey);
if (region != null) {
return region;
}
if (!regionAssignmentCounts.containsKey(regionKey)) {
return null;
}
region = readRegion(regionX, regionZ);
cacheRegion(regionKey, region);
return region;
}
private RegionGenerationOwnership readRegion(int regionX, int regionZ) throws IOException {
Path file = directory.resolve(RegionGenerationOwnership.fileName(regionX, regionZ));
RegionGenerationOwnership region = RegionGenerationOwnership.read(file);
if (region.regionX() != regionX || region.regionZ() != regionZ) {
throw new IOException("Generation ownership shard name does not match its coordinates: " + file);
}
return region;
}
private boolean matchesRegion(int regionX, int regionZ, int minimumX, int minimumZ,
int maximumX, int maximumZ, AssignmentPredicate predicate) throws IOException {
RegionGenerationOwnership region = loadExistingRegion(regionX, regionZ);
return region != null && region.anyMatchingInBounds(minimumX, minimumZ, maximumX, maximumZ, predicate);
}
private void cacheRegion(long regionKey, RegionGenerationOwnership region) throws IOException {
regions.put(regionKey, region);
while (regions.size() > MAXIMUM_CACHED_REGIONS) {
Iterator<Map.Entry<Long, RegionGenerationOwnership>> entries = regions.entrySet().iterator();
Map.Entry<Long, RegionGenerationOwnership> eldest = entries.next();
RegionGenerationOwnership evicted = eldest.getValue();
if (evicted.persist(directory)) {
regionAssignmentCounts.put(eldest.getKey().longValue(), evicted.assignmentCount());
}
entries.remove();
}
}
private void recordAssignment(RegionGenerationOwnership region) {
explicitChunkCount++;
regionAssignmentCounts.put(packRegion(region.regionX(), region.regionZ()), region.assignmentCount());
}
private List<Long> orderedRegionKeys() {
ArrayList<Long> ordered = new ArrayList<>(regionAssignmentCounts.keySet());
ordered.sort(ChunkGenerationOwnership::compareRegionKeys);
return ordered;
}
private static IllegalStateException lookupFailure(int chunkX, int chunkZ, IOException error) {
return new IllegalStateException(
"Unable to load generation ownership for chunk " + chunkX + "," + chunkZ,
error
);
}
private static long packRegion(int regionX, int regionZ) {
return ((long) regionX << Integer.SIZE) | (regionZ & 0xFFFFFFFFL);
}
private static int regionX(long regionKey) {
return (int) (regionKey >> Integer.SIZE);
}
private static int regionZ(long regionKey) {
return (int) regionKey;
}
private static int compareRegionKeys(long first, long second) {
int xComparison = Integer.compare(regionX(first), regionX(second));
return xComparison != 0 ? xComparison : Integer.compare(regionZ(first), regionZ(second));
}
private static void requireActivation(long activation) {
if (activation <= 0L) {
throw new IllegalArgumentException("Generation activation IDs must be positive: " + activation);
}
}
@FunctionalInterface
public interface AssignmentPredicate {
boolean test(int chunkX, int chunkZ, long activationId);
}
@FunctionalInterface
public interface AssignmentConsumer {
void accept(int chunkX, int chunkZ, long activationId) throws IOException;
}
}
@@ -0,0 +1,479 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
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;
static final int MAX_KEYS_PER_KIND = 2_048;
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<RiverFeatureOccurrence> RIVER_FEATURE_COMPARATOR = Comparator
.comparing(RiverFeatureOccurrence::profileKey)
.thenComparing(RiverFeatureOccurrence::type)
.thenComparingInt(occurrence -> occurrence.position().x())
.thenComparingInt(occurrence -> occurrence.position().z())
.thenComparingInt(occurrence -> occurrence.position().y())
.thenComparingLong(RiverFeatureOccurrence::featureId);
private final int chunkX;
private final int chunkZ;
private final long activationId;
private final boolean sealed;
private final Set<String> surfaceBiomeKeys;
private final Set<String> caveBiomeKeys;
private final Set<String> regionKeys;
private final Set<String> riverProfileKeys;
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private ChunkGenerationSemantics(Builder builder) {
chunkX = builder.chunkX;
chunkZ = builder.chunkZ;
activationId = builder.activationId;
sealed = builder.sealed;
surfaceBiomeKeys = immutableKeys(builder.surfaceBiomeKeys, "surface biome");
caveBiomeKeys = immutableKeys(builder.caveBiomeKeys, "cave biome");
regionKeys = immutableKeys(builder.regionKeys, "region");
riverProfileKeys = immutableKeys(builder.riverProfileKeys, "river profile");
objectKeys = immutableKeys(builder.objectKeys, "object");
riverFeatures = immutableRiverFeatures(builder.riverFeatures);
structures = immutableStructures(builder.structures);
}
public static Builder builder(int chunkX, int chunkZ, long activationId) {
return new Builder(chunkX, chunkZ, activationId);
}
public int chunkX() {
return chunkX;
}
public int chunkZ() {
return chunkZ;
}
public long activationId() {
return activationId;
}
public boolean sealed() {
return sealed;
}
public Set<String> surfaceBiomeKeys() {
return surfaceBiomeKeys;
}
public Set<String> caveBiomeKeys() {
return caveBiomeKeys;
}
public Set<String> regionKeys() {
return regionKeys;
}
public Set<String> riverProfileKeys() {
return riverProfileKeys;
}
public Set<String> objectKeys() {
return objectKeys;
}
public Set<RiverFeatureOccurrence> riverFeatures() {
return riverFeatures;
}
public Set<StructureOccurrence> structures() {
return structures;
}
public ChunkGenerationSemantics merge(ChunkGenerationSemantics update) {
ChunkGenerationSemantics requiredUpdate = Objects.requireNonNull(update, "update");
if (chunkX != requiredUpdate.chunkX || chunkZ != requiredUpdate.chunkZ) {
throw new IllegalArgumentException(
"Generation semantic updates must address the same chunk: "
+ chunkX + "," + chunkZ + " and "
+ requiredUpdate.chunkX + "," + requiredUpdate.chunkZ
);
}
if (activationId != requiredUpdate.activationId) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ
+ " belongs to generation activation " + activationId
+ ", not " + requiredUpdate.activationId
);
}
boolean addsFacts = !surfaceBiomeKeys.containsAll(requiredUpdate.surfaceBiomeKeys)
|| !caveBiomeKeys.containsAll(requiredUpdate.caveBiomeKeys)
|| !regionKeys.containsAll(requiredUpdate.regionKeys)
|| !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys)
|| !objectKeys.containsAll(requiredUpdate.objectKeys)
|| !riverFeatures.containsAll(requiredUpdate.riverFeatures)
|| !structures.containsAll(requiredUpdate.structures);
if (sealed && addsFacts) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics"
);
}
if (!addsFacts && (sealed || !requiredUpdate.sealed)) {
return this;
}
Builder merged = builder(chunkX, chunkZ, activationId)
.addSurfaceBiomes(surfaceBiomeKeys)
.addSurfaceBiomes(requiredUpdate.surfaceBiomeKeys)
.addCaveBiomes(caveBiomeKeys)
.addCaveBiomes(requiredUpdate.caveBiomeKeys)
.addRegions(regionKeys)
.addRegions(requiredUpdate.regionKeys)
.addRiverProfiles(riverProfileKeys)
.addRiverProfiles(requiredUpdate.riverProfileKeys)
.addObjects(objectKeys)
.addObjects(requiredUpdate.objectKeys)
.addRiverFeatures(riverFeatures)
.addRiverFeatures(requiredUpdate.riverFeatures)
.addStructures(structures)
.addStructures(requiredUpdate.structures);
if (sealed || requiredUpdate.sealed) {
merged.seal();
}
return merged.build();
}
@Override
public boolean equals(Object candidate) {
if (this == candidate) {
return true;
}
if (!(candidate instanceof ChunkGenerationSemantics semantics)) {
return false;
}
return chunkX == semantics.chunkX
&& chunkZ == semantics.chunkZ
&& activationId == semantics.activationId
&& sealed == semantics.sealed
&& surfaceBiomeKeys.equals(semantics.surfaceBiomeKeys)
&& caveBiomeKeys.equals(semantics.caveBiomeKeys)
&& regionKeys.equals(semantics.regionKeys)
&& riverProfileKeys.equals(semantics.riverProfileKeys)
&& objectKeys.equals(semantics.objectKeys)
&& riverFeatures.equals(semantics.riverFeatures)
&& structures.equals(semantics.structures);
}
@Override
public int hashCode() {
return Objects.hash(
chunkX,
chunkZ,
activationId,
sealed,
surfaceBiomeKeys,
caveBiomeKeys,
regionKeys,
riverProfileKeys,
objectKeys,
riverFeatures,
structures
);
}
@Override
public String toString() {
return "ChunkGenerationSemantics[chunkX=" + chunkX
+ ", chunkZ=" + chunkZ
+ ", activationId=" + activationId
+ ", sealed=" + sealed
+ ", surfaceBiomeKeys=" + surfaceBiomeKeys
+ ", caveBiomeKeys=" + caveBiomeKeys
+ ", regionKeys=" + regionKeys
+ ", riverProfileKeys=" + riverProfileKeys
+ ", objectKeys=" + objectKeys
+ ", riverFeatures=" + riverFeatures
+ ", structures=" + structures
+ "]";
}
static Comparator<StructureOccurrence> structureComparator() {
return STRUCTURE_COMPARATOR;
}
static Comparator<RiverFeatureOccurrence> riverFeatureComparator() {
return RIVER_FEATURE_COMPARATOR;
}
static String requireResourceKey(String key) {
String requiredKey = Objects.requireNonNull(key, "key");
if (requiredKey.isBlank()) {
throw new IllegalArgumentException("Generation semantic resource keys must not be blank");
}
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);
}
if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) {
throw new IllegalArgumentException("Generation semantic resource key is too long");
}
return requiredKey;
}
private static Set<String> immutableKeys(Collection<String> keys, String kind) {
if (keys.size() > MAX_KEYS_PER_KIND) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_KEYS_PER_KIND + " " + kind + " keys"
);
}
TreeSet<String> validated = new TreeSet<>();
for (String key : keys) {
validated.add(requireResourceKey(key));
}
return Collections.unmodifiableSet(validated);
}
private static Set<StructureOccurrence> immutableStructures(Collection<StructureOccurrence> structures) {
if (structures.size() > MAX_STRUCTURES) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_STRUCTURES + " structure occurrences"
);
}
TreeSet<StructureOccurrence> validated = new TreeSet<>(STRUCTURE_COMPARATOR);
for (StructureOccurrence occurrence : structures) {
validated.add(Objects.requireNonNull(occurrence, "structure occurrence"));
}
return Collections.unmodifiableSet(validated);
}
private static Set<RiverFeatureOccurrence> immutableRiverFeatures(
Collection<RiverFeatureOccurrence> occurrences
) {
if (occurrences.size() > MAX_RIVER_FEATURES) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_RIVER_FEATURES + " river feature occurrences"
);
}
TreeSet<RiverFeatureOccurrence> validated = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
for (RiverFeatureOccurrence occurrence : occurrences) {
validated.add(Objects.requireNonNull(occurrence, "river feature occurrence"));
}
return Collections.unmodifiableSet(validated);
}
public record BlockPosition(int x, int y, int z) {
}
public record StructureOccurrence(String key, BlockPosition position) {
public StructureOccurrence {
key = requireResourceKey(key);
position = Objects.requireNonNull(position, "position");
}
}
public record RiverFeatureOccurrence(
String profileKey,
HydrologyFeatureType type,
long featureId,
BlockPosition position
) {
public RiverFeatureOccurrence {
profileKey = requireResourceKey(profileKey);
type = Objects.requireNonNull(type, "type");
position = Objects.requireNonNull(position, "position");
}
}
public static final class Builder {
private final int chunkX;
private final int chunkZ;
private final long activationId;
private final Set<String> surfaceBiomeKeys;
private final Set<String> caveBiomeKeys;
private final Set<String> regionKeys;
private final Set<String> riverProfileKeys;
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private boolean sealed;
private Builder(int chunkX, int chunkZ, long activationId) {
if (activationId <= 0L) {
throw new IllegalArgumentException("Generation activation IDs must be positive: " + activationId);
}
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.activationId = activationId;
surfaceBiomeKeys = new TreeSet<>();
caveBiomeKeys = new TreeSet<>();
regionKeys = new TreeSet<>();
riverProfileKeys = new TreeSet<>();
objectKeys = new TreeSet<>();
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
}
public Builder addSurfaceBiome(String key) {
addKey(surfaceBiomeKeys, key, "surface biome");
return this;
}
public Builder addSurfaceBiomes(Collection<String> keys) {
addKeys(surfaceBiomeKeys, keys, "surface biome");
return this;
}
public Builder addCaveBiome(String key) {
addKey(caveBiomeKeys, key, "cave biome");
return this;
}
public Builder addCaveBiomes(Collection<String> keys) {
addKeys(caveBiomeKeys, keys, "cave biome");
return this;
}
public Builder addRegion(String key) {
addKey(regionKeys, key, "region");
return this;
}
public Builder addRegions(Collection<String> keys) {
addKeys(regionKeys, keys, "region");
return this;
}
public Builder addRiverProfile(String key) {
addKey(riverProfileKeys, key, "river profile");
return this;
}
public Builder addRiverProfiles(Collection<String> keys) {
addKeys(riverProfileKeys, keys, "river profile");
return this;
}
public Builder addRiverFeature(
String profileKey,
HydrologyFeatureType type,
long featureId,
int blockX,
int blockY,
int blockZ
) {
return addRiverFeature(new RiverFeatureOccurrence(
profileKey,
type,
featureId,
new BlockPosition(blockX, blockY, blockZ)
));
}
public Builder addRiverFeature(RiverFeatureOccurrence occurrence) {
RiverFeatureOccurrence required = Objects.requireNonNull(
occurrence,
"river feature occurrence"
);
if (riverFeatures.size() >= MAX_RIVER_FEATURES && !riverFeatures.contains(required)) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_RIVER_FEATURES
+ " river feature occurrences"
);
}
riverFeatures.add(required);
addRiverProfile(required.profileKey());
return this;
}
public Builder addRiverFeatures(Collection<RiverFeatureOccurrence> occurrences) {
Collection<RiverFeatureOccurrence> requiredOccurrences = Objects.requireNonNull(
occurrences,
"river feature occurrences"
);
for (RiverFeatureOccurrence occurrence : requiredOccurrences) {
addRiverFeature(occurrence);
}
return this;
}
public Builder addObject(String key) {
addKey(objectKeys, key, "object");
return this;
}
public Builder addObjects(Collection<String> keys) {
addKeys(objectKeys, keys, "object");
return this;
}
public Builder addStructure(String key, int blockX, int blockY, int blockZ) {
return addStructure(new StructureOccurrence(key, new BlockPosition(blockX, blockY, blockZ)));
}
public Builder addStructure(StructureOccurrence occurrence) {
if (structures.size() >= MAX_STRUCTURES && !structures.contains(occurrence)) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_STRUCTURES + " structure occurrences"
);
}
structures.add(Objects.requireNonNull(occurrence, "structure occurrence"));
return this;
}
public Builder addStructures(Collection<StructureOccurrence> occurrences) {
Collection<StructureOccurrence> requiredOccurrences = Objects.requireNonNull(occurrences, "occurrences");
for (StructureOccurrence occurrence : requiredOccurrences) {
addStructure(occurrence);
}
return this;
}
public Builder seal() {
sealed = true;
return this;
}
public ChunkGenerationSemantics build() {
return new ChunkGenerationSemantics(this);
}
private static void addKeys(Set<String> target, Collection<String> keys, String kind) {
Collection<String> requiredKeys = Objects.requireNonNull(keys, "keys");
for (String key : requiredKeys) {
addKey(target, key, kind);
}
}
private static void addKey(Set<String> target, String key, String kind) {
String validated = requireResourceKey(key);
if (target.size() >= MAX_KEYS_PER_KIND && !target.contains(validated)) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_KEYS_PER_KIND + " " + kind + " keys"
);
}
target.add(validated);
}
}
}
@@ -0,0 +1,207 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonObject;
import com.google.gson.JsonNull;
import java.util.Objects;
import java.util.regex.Pattern;
public final class GenerationActivation {
private static final Pattern EPOCH_ID_PATTERN = Pattern.compile("[0-9a-f]{64}");
private final long activationId;
private final String epochId;
private final Long parentActivationId;
private final long createdAtEpochMillis;
private final GenerationTransition transition;
private GenerationActivation(State state) {
State requiredState = Objects.requireNonNull(state, "state");
this.activationId = requiredState.activationId();
this.epochId = requiredState.epochId();
this.parentActivationId = requiredState.parentActivationId();
this.createdAtEpochMillis = requiredState.createdAtEpochMillis();
this.transition = requiredState.transition();
}
static GenerationActivation initial(String epochId, long createdAtEpochMillis) {
return new GenerationActivation(new State(1L, epochId, null, createdAtEpochMillis, null));
}
static GenerationActivation next(
long activationId,
String epochId,
long parentActivationId,
long createdAtEpochMillis,
int transitionWidthBlocks
) {
return new GenerationActivation(new State(
activationId,
epochId,
parentActivationId,
createdAtEpochMillis,
GenerationTransition.pending(transitionWidthBlocks)
));
}
static GenerationActivation restore(State state) {
return new GenerationActivation(state);
}
public long activationId() {
return activationId;
}
public String epochId() {
return epochId;
}
public Long parentActivationId() {
return parentActivationId;
}
public long createdAtEpochMillis() {
return createdAtEpochMillis;
}
public GenerationTransition transition() {
return transition;
}
public boolean isInitial() {
return parentActivationId == null;
}
GenerationActivation completeTransition(String boundaryIdentity, String terrainSignatureIdentity) {
if (transition == null) {
throw new IllegalStateException("The initial activation has no generation transition.");
}
return new GenerationActivation(new State(
activationId,
epochId,
parentActivationId,
createdAtEpochMillis,
transition.complete(boundaryIdentity, terrainSignatureIdentity)
));
}
State state() {
return new State(activationId, epochId, parentActivationId, createdAtEpochMillis, transition);
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("activationId", activationId);
json.addProperty("epochId", epochId);
if (parentActivationId == null) {
json.add("parentActivationId", JsonNull.INSTANCE);
} else {
json.addProperty("parentActivationId", parentActivationId);
}
json.addProperty("createdAtEpochMillis", createdAtEpochMillis);
if (transition == null) {
json.add("transition", JsonNull.INSTANCE);
} else {
json.add("transition", transition.toJson());
}
return json;
}
static GenerationActivation fromJson(JsonObject json) {
GenerationManifest.JsonSchema.requireFields(
json,
"activation",
"activationId",
"epochId",
"parentActivationId",
"createdAtEpochMillis",
"transition"
);
long activationId = GenerationManifest.JsonSchema.requireLong(json, "activationId", "activation");
String epochId = GenerationManifest.JsonSchema.requireString(json, "epochId", "activation");
Long parentActivationId = GenerationManifest.JsonSchema.requireNullableLong(
json,
"parentActivationId",
"activation"
);
long createdAtEpochMillis = GenerationManifest.JsonSchema.requireLong(
json,
"createdAtEpochMillis",
"activation"
);
JsonObject transitionJson = GenerationManifest.JsonSchema.requireNullableObject(
json,
"transition",
"activation"
);
GenerationTransition transition = transitionJson == null
? null
: GenerationTransition.fromJson(transitionJson);
return restore(new State(
activationId,
epochId,
parentActivationId,
createdAtEpochMillis,
transition
));
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GenerationActivation activation)) {
return false;
}
return activationId == activation.activationId
&& createdAtEpochMillis == activation.createdAtEpochMillis
&& epochId.equals(activation.epochId)
&& Objects.equals(parentActivationId, activation.parentActivationId)
&& Objects.equals(transition, activation.transition);
}
@Override
public int hashCode() {
return Objects.hash(activationId, epochId, parentActivationId, createdAtEpochMillis, transition);
}
@Override
public String toString() {
return "GenerationActivation[activationId=" + activationId
+ ", epochId=" + epochId
+ ", parentActivationId=" + parentActivationId
+ ", createdAtEpochMillis=" + createdAtEpochMillis
+ ", transition=" + transition + "]";
}
record State(
long activationId,
String epochId,
Long parentActivationId,
long createdAtEpochMillis,
GenerationTransition transition
) {
State {
if (activationId < 1L) {
throw new IllegalArgumentException("Activation ID must be positive.");
}
epochId = Objects.requireNonNull(epochId, "epochId");
if (!EPOCH_ID_PATTERN.matcher(epochId).matches()) {
throw new IllegalArgumentException("Activation epoch ID must be a lowercase SHA-256 value.");
}
if (parentActivationId != null
&& (parentActivationId < 1L || parentActivationId >= activationId)) {
throw new IllegalArgumentException("Parent activation ID must identify an earlier activation.");
}
if (createdAtEpochMillis < 0L) {
throw new IllegalArgumentException("Activation creation time cannot be negative.");
}
if ((parentActivationId == null) != (transition == null)) {
throw new IllegalArgumentException(
"Only the initial activation may omit a generation transition."
);
}
}
}
}
@@ -0,0 +1,147 @@
package art.arcane.iris.engine.history;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public final class GenerationAdmission {
private static final ConcurrentHashMap<Path, State> STATES = new ConcurrentHashMap<>();
private final State state;
public GenerationAdmission(Path dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toAbsolutePath()
.normalize();
this.state = STATES.computeIfAbsent(root, ignored -> new State());
}
public StageLease enterStage() {
state.enterStage();
return new StageLease(state);
}
public CutoverLease beginCutover() {
state.beginCutover(false);
return new CutoverLease(state);
}
CutoverLease beginStartupCutover() {
state.beginCutover(true);
return new CutoverLease(state);
}
public static final class StageLease implements AutoCloseable {
private final State state;
private final AtomicBoolean closed;
private StageLease(State state) {
this.state = state;
this.closed = new AtomicBoolean();
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
state.leaveStage();
}
}
}
public static final class CutoverLease implements AutoCloseable {
private final State state;
private final AtomicBoolean closed;
private CutoverLease(State state) {
this.state = state;
this.closed = new AtomicBoolean();
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
state.endCutover();
}
}
}
private static final class State {
private final ReentrantLock lock;
private final Condition changed;
private int activeStages;
private int waitingCutovers;
private boolean cutoverActive;
private boolean generationAdmissionOpened;
private State() {
this.lock = new ReentrantLock(true);
this.changed = lock.newCondition();
}
private void enterStage() {
lock.lock();
try {
generationAdmissionOpened = true;
while (cutoverActive || waitingCutovers > 0) {
changed.awaitUninterruptibly();
}
activeStages++;
} finally {
lock.unlock();
}
}
private void leaveStage() {
lock.lock();
try {
if (activeStages < 1) {
throw new IllegalStateException("No generation stage is active.");
}
activeStages--;
if (activeStages == 0) {
changed.signalAll();
}
} finally {
lock.unlock();
}
}
private void beginCutover(boolean startupOnly) {
lock.lock();
try {
if (startupOnly && generationAdmissionOpened) {
throw new IllegalStateException(
"Generation activation promotion is only allowed before generation admission opens."
);
}
waitingCutovers++;
try {
while (cutoverActive || activeStages > 0) {
changed.awaitUninterruptibly();
}
cutoverActive = true;
} finally {
waitingCutovers--;
}
} finally {
lock.unlock();
}
}
private void endCutover() {
lock.lock();
try {
if (!cutoverActive) {
throw new IllegalStateException("No generation cutover is active.");
}
cutoverActive = false;
changed.signalAll();
} finally {
lock.unlock();
}
}
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.engine.history;
public final class GenerationBlend {
private GenerationBlend() {
}
public static double newEpochWeight(double distanceToHistoricalBlocks, int widthBlocks) {
if (!Double.isFinite(distanceToHistoricalBlocks) || distanceToHistoricalBlocks < 0D) {
throw new IllegalArgumentException("Distance must be finite and non-negative");
}
if (widthBlocks <= 0) {
throw new IllegalArgumentException("Blend width must be positive");
}
if (distanceToHistoricalBlocks <= 0D) {
return 0D;
}
if (distanceToHistoricalBlocks >= widthBlocks) {
return 1D;
}
return smootherstep(distanceToHistoricalBlocks / widthBlocks);
}
public static double smootherstep(double progress) {
if (!Double.isFinite(progress)) {
throw new IllegalArgumentException("Progress must be finite");
}
if (progress <= 0D) {
return 0D;
}
if (progress >= 1D) {
return 1D;
}
return progress * progress * progress * (progress * (progress * 6D - 15D) + 10D);
}
public static double interpolate(double historicalValue, double newValue, double newEpochWeight) {
if (!Double.isFinite(historicalValue) || !Double.isFinite(newValue)) {
throw new IllegalArgumentException("Blend values must be finite");
}
validateWeight(newEpochWeight);
if (newEpochWeight == 0D) {
return historicalValue;
}
if (newEpochWeight == 1D) {
return newValue;
}
return historicalValue + (newValue - historicalValue) * newEpochWeight;
}
public static int interpolateHeight(int historicalHeight, int newHeight, double newEpochWeight) {
validateWeight(newEpochWeight);
if (newEpochWeight == 0D) {
return historicalHeight;
}
if (newEpochWeight == 1D) {
return newHeight;
}
return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight);
}
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");
}
}
}
@@ -0,0 +1,435 @@
package art.arcane.iris.engine.history;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
public final class GenerationBoundary {
public static final int CHUNK_SIZE = 16;
private final String identity;
private final int historicalChunkCount;
private final HistoricalChunkSource historicalChunks;
private GenerationBoundary(
String identity,
int historicalChunkCount,
HistoricalChunkSource historicalChunks
) {
this.identity = requireIdentity(identity);
if (historicalChunkCount < 0) {
throw new IllegalArgumentException("Historical chunk count cannot be negative");
}
this.historicalChunkCount = historicalChunkCount;
this.historicalChunks = Objects.requireNonNull(historicalChunks, "Historical chunks");
}
public static GenerationBoundary freeze(String identity, Collection<ChunkCoordinate> historicalChunks) {
Collection<ChunkCoordinate> requiredChunks = Objects.requireNonNull(
historicalChunks,
"Historical chunks"
);
long[] packedChunks = new long[requiredChunks.size()];
int index = 0;
for (ChunkCoordinate chunk : requiredChunks) {
ChunkCoordinate coordinate = Objects.requireNonNull(chunk, "Historical chunk");
packedChunks[index++] = packChunk(coordinate.chunkX(), coordinate.chunkZ());
}
return freezePacked(identity, packedChunks);
}
public static GenerationBoundary freezePacked(String identity, long[] historicalChunks) {
long[] normalized = normalize(Objects.requireNonNull(historicalChunks, "Historical chunks"));
return new GenerationBoundary(identity, normalized.length, new ArraySource(normalized));
}
static GenerationBoundary backed(
String identity,
int historicalChunkCount,
HistoricalChunkSource historicalChunks
) {
return new GenerationBoundary(identity, historicalChunkCount, historicalChunks);
}
public String identity() {
return identity;
}
public int historicalChunkCount() {
return historicalChunkCount;
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
try {
return historicalChunks.contains(chunkX, chunkZ);
} catch (IOException error) {
throw new IllegalStateException("Unable to read the frozen generation boundary", error);
}
}
public boolean isHistoricalBlock(int blockX, int blockZ) {
return isHistoricalChunk(blockToChunk(blockX), blockToChunk(blockZ));
}
public boolean intersectsHistoricalBlocks(
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
if (minimumX > maximumX || minimumZ > maximumZ) {
throw new IllegalArgumentException("Generation footprint bounds are inverted");
}
if (historicalChunkCount == 0) {
return false;
}
int minimumChunkX = blockToChunk(minimumX);
int maximumChunkX = blockToChunk(maximumX);
int minimumChunkZ = blockToChunk(minimumZ);
int maximumChunkZ = blockToChunk(maximumZ);
long candidateColumns = (long) maximumChunkX - minimumChunkX + 1L;
long candidateRows = (long) maximumChunkZ - minimumChunkZ + 1L;
long candidateCount = multiplySaturated(candidateColumns, candidateRows);
if (candidateCount <= historicalChunkCount) {
for (long chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
for (long chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
if (isHistoricalChunk((int) chunkX, (int) chunkZ)) {
return true;
}
}
}
return false;
}
boolean[] found = new boolean[1];
try {
historicalChunks.forEach((chunkX, chunkZ) -> {
if (!found[0]
&& chunkX >= minimumChunkX && chunkX <= maximumChunkX
&& chunkZ >= minimumChunkZ && chunkZ <= maximumChunkZ) {
found[0] = true;
}
});
} catch (IOException error) {
throw new IllegalStateException("Unable to scan the frozen generation boundary", error);
}
return found[0];
}
public double distanceToHistoricalChunks(int blockX, int blockZ, int maximumDistanceBlocks) {
if (maximumDistanceBlocks <= 0) {
throw new IllegalArgumentException("Maximum distance must be positive");
}
if (historicalChunkCount == 0) {
return maximumDistanceBlocks;
}
if (isHistoricalBlock(blockX, blockZ)) {
return 0D;
}
long minimumChunkX = blockToChunk((long) blockX - maximumDistanceBlocks);
long maximumChunkX = blockToChunk((long) blockX + maximumDistanceBlocks);
long minimumChunkZ = blockToChunk((long) blockZ - maximumDistanceBlocks);
long maximumChunkZ = blockToChunk((long) blockZ + maximumDistanceBlocks);
long candidateColumns = maximumChunkX - minimumChunkX + 1L;
long candidateRows = maximumChunkZ - minimumChunkZ + 1L;
long candidateCount = multiplySaturated(candidateColumns, candidateRows);
double distanceSquared = candidateCount <= historicalChunkCount
? nearestByNeighborhood(
blockX,
blockZ,
minimumChunkX,
maximumChunkX,
minimumChunkZ,
maximumChunkZ,
maximumDistanceBlocks
)
: nearestBySource(blockX, blockZ, maximumDistanceBlocks);
return Math.sqrt(distanceSquared);
}
public long[] packedHistoricalChunks() {
long[] chunks = new long[historicalChunkCount];
int[] cursor = new int[1];
try {
historicalChunks.forEach((chunkX, chunkZ) -> {
if (cursor[0] >= chunks.length) {
throw new IOException("Frozen boundary contains more chunks than its catalog declares");
}
chunks[cursor[0]++] = packChunk(chunkX, chunkZ);
});
} catch (IOException error) {
throw new IllegalStateException("Unable to materialize the frozen generation boundary", error);
}
if (cursor[0] != chunks.length) {
throw new IllegalStateException("Frozen boundary contains fewer chunks than its catalog declares");
}
Arrays.sort(chunks);
return chunks;
}
public void forEachHistoricalChunk(ChunkConsumer consumer) throws IOException {
historicalChunks.forEach(Objects.requireNonNull(consumer, "Historical chunk consumer"));
}
int cachedRegionCount() {
return historicalChunks.cachedRegionCount();
}
public void requireCompleteTerrainSignatures(
Collection<TerrainBoundarySignature> signatures
) {
Collection<TerrainBoundarySignature> requiredSignatures = Objects.requireNonNull(
signatures,
"Terrain signatures"
);
LongOpenHashSet actual = new LongOpenHashSet(requiredSignatures.size());
for (TerrainBoundarySignature signature : requiredSignatures) {
TerrainBoundarySignature requiredSignature = Objects.requireNonNull(
signature,
"Terrain signature"
);
if (!isExposedBlockColumn(requiredSignature.blockX(), requiredSignature.blockZ())) {
throw new IllegalArgumentException("Terrain signature is not on the frozen generation boundary: "
+ requiredSignature.blockX() + "," + requiredSignature.blockZ());
}
long column = packBlock(requiredSignature.blockX(), requiredSignature.blockZ());
if (!actual.add(column)) {
throw new IllegalArgumentException("Duplicate terrain signature column: "
+ requiredSignature.blockX() + "," + requiredSignature.blockZ());
}
}
long expectedCount = exposedBlockColumnCount();
if (actual.size() != expectedCount) {
throw new IllegalArgumentException("Terrain signatures contain " + actual.size()
+ " columns but the frozen boundary requires " + expectedCount);
}
}
public List<BlockColumn> exposedBlockColumns() {
ArrayList<BlockColumn> columns = new ArrayList<>();
try {
forEachExposedBlockColumn((blockX, blockZ) -> columns.add(new BlockColumn(blockX, blockZ)));
} catch (IOException error) {
throw new IllegalStateException("Unable to enumerate frozen boundary columns", error);
}
columns.sort(Comparator
.comparingInt(BlockColumn::blockX)
.thenComparingInt(BlockColumn::blockZ));
return List.copyOf(columns);
}
public long exposedBlockColumnCount() {
long[] count = new long[1];
try {
forEachExposedBlockColumn((blockX, blockZ) -> count[0]++);
} catch (IOException error) {
throw new IllegalStateException("Unable to count frozen boundary columns", error);
}
return count[0];
}
public void forEachExposedBlockColumn(BlockColumnConsumer consumer) throws IOException {
BlockColumnConsumer requiredConsumer = Objects.requireNonNull(consumer, "Boundary column consumer");
historicalChunks.forEach((chunkX, chunkZ) -> emitExposedColumns(chunkX, chunkZ, requiredConsumer));
}
public static long packChunk(int chunkX, int chunkZ) {
return ((long) chunkX << Integer.SIZE) ^ (chunkZ & 0xffffffffL);
}
public static ChunkCoordinate unpackChunk(long packedChunk) {
return new ChunkCoordinate((int) (packedChunk >> Integer.SIZE), (int) packedChunk);
}
private void emitExposedColumns(
int chunkX,
int chunkZ,
BlockColumnConsumer consumer
) throws IOException {
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
boolean north = chunkZ == Integer.MIN_VALUE || !historicalChunks.contains(chunkX, chunkZ - 1);
boolean south = chunkZ == Integer.MAX_VALUE || !historicalChunks.contains(chunkX, chunkZ + 1);
boolean west = chunkX == Integer.MIN_VALUE || !historicalChunks.contains(chunkX - 1, chunkZ);
boolean east = chunkX == Integer.MAX_VALUE || !historicalChunks.contains(chunkX + 1, chunkZ);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
if (localZ == 0 && north
|| localZ == CHUNK_SIZE - 1 && south
|| localX == 0 && west
|| localX == CHUNK_SIZE - 1 && east) {
consumer.accept(minimumX + localX, minimumZ + localZ);
}
}
}
}
private boolean isExposedBlockColumn(int blockX, int blockZ) {
int chunkX = blockToChunk(blockX);
int chunkZ = blockToChunk(blockZ);
if (!isHistoricalChunk(chunkX, chunkZ)) {
return false;
}
int localX = Math.floorMod(blockX, CHUNK_SIZE);
int localZ = Math.floorMod(blockZ, CHUNK_SIZE);
return localZ == 0 && (chunkZ == Integer.MIN_VALUE || !isHistoricalChunk(chunkX, chunkZ - 1))
|| localZ == CHUNK_SIZE - 1
&& (chunkZ == Integer.MAX_VALUE || !isHistoricalChunk(chunkX, chunkZ + 1))
|| localX == 0 && (chunkX == Integer.MIN_VALUE || !isHistoricalChunk(chunkX - 1, chunkZ))
|| localX == CHUNK_SIZE - 1
&& (chunkX == Integer.MAX_VALUE || !isHistoricalChunk(chunkX + 1, chunkZ));
}
private double nearestByNeighborhood(
int blockX,
int blockZ,
long minimumChunkX,
long maximumChunkX,
long minimumChunkZ,
long maximumChunkZ,
int maximumDistanceBlocks
) {
double nearestSquared = square(maximumDistanceBlocks);
for (long chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
for (long chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
if (!isHistoricalChunk((int) chunkX, (int) chunkZ)) {
continue;
}
nearestSquared = Math.min(
nearestSquared,
distanceSquaredToChunk(blockX, blockZ, (int) chunkX, (int) chunkZ)
);
}
}
return nearestSquared;
}
private double nearestBySource(int blockX, int blockZ, int maximumDistanceBlocks) {
double[] nearestSquared = {square(maximumDistanceBlocks)};
try {
historicalChunks.forEach((chunkX, chunkZ) -> nearestSquared[0] = Math.min(
nearestSquared[0],
distanceSquaredToChunk(blockX, blockZ, chunkX, chunkZ)
));
} catch (IOException error) {
throw new IllegalStateException("Unable to scan the frozen generation boundary", error);
}
return nearestSquared[0];
}
private static double distanceSquaredToChunk(int blockX, int blockZ, int chunkX, int chunkZ) {
long minimumX = (long) chunkX * CHUNK_SIZE;
long maximumX = minimumX + CHUNK_SIZE - 1L;
long minimumZ = (long) chunkZ * CHUNK_SIZE;
long maximumZ = minimumZ + CHUNK_SIZE - 1L;
long distanceX = distanceToRange(blockX, minimumX, maximumX);
long distanceZ = distanceToRange(blockZ, minimumZ, maximumZ);
return square(distanceX) + square(distanceZ);
}
private static long distanceToRange(long value, long minimum, long maximum) {
if (value < minimum) {
return minimum - value;
}
return value > maximum ? value - maximum : 0L;
}
private static double square(long value) {
return (double) value * value;
}
private static long multiplySaturated(long left, long right) {
if (left == 0L || right == 0L) {
return 0L;
}
return left > Long.MAX_VALUE / right ? Long.MAX_VALUE : left * right;
}
private static int blockToChunk(int blockCoordinate) {
return Math.floorDiv(blockCoordinate, CHUNK_SIZE);
}
private static long blockToChunk(long blockCoordinate) {
return Math.floorDiv(blockCoordinate, CHUNK_SIZE);
}
private static long packBlock(int blockX, int blockZ) {
return ((long) blockX << Integer.SIZE) ^ (blockZ & 0xffffffffL);
}
private static String requireIdentity(String identity) {
String requiredIdentity = Objects.requireNonNull(identity, "Boundary identity");
if (requiredIdentity.isBlank()) {
throw new IllegalArgumentException("Boundary identity cannot be blank");
}
return requiredIdentity;
}
private static long[] normalize(long[] historicalChunks) {
long[] normalized = historicalChunks.clone();
Arrays.sort(normalized);
if (normalized.length < 2) {
return normalized;
}
int uniqueCount = 1;
for (int index = 1; index < normalized.length; index++) {
if (normalized[index] != normalized[uniqueCount - 1]) {
normalized[uniqueCount++] = normalized[index];
}
}
return Arrays.copyOf(normalized, uniqueCount);
}
public record ChunkCoordinate(int chunkX, int chunkZ) {
}
public record BlockColumn(int blockX, int blockZ) {
}
@FunctionalInterface
public interface ChunkConsumer {
void accept(int chunkX, int chunkZ) throws IOException;
}
@FunctionalInterface
public interface BlockColumnConsumer {
void accept(int blockX, int blockZ) throws IOException;
}
interface HistoricalChunkSource {
boolean contains(int chunkX, int chunkZ) throws IOException;
void forEach(ChunkConsumer consumer) throws IOException;
default int cachedRegionCount() {
return 0;
}
}
private static final class ArraySource implements HistoricalChunkSource {
private final long[] chunks;
private ArraySource(long[] chunks) {
this.chunks = chunks;
}
@Override
public boolean contains(int chunkX, int chunkZ) {
return Arrays.binarySearch(chunks, packChunk(chunkX, chunkZ)) >= 0;
}
@Override
public void forEach(ChunkConsumer consumer) throws IOException {
for (long chunk : chunks) {
consumer.accept((int) (chunk >> Integer.SIZE), (int) chunk);
}
}
}
}
@@ -0,0 +1,10 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisEngine;
import java.io.IOException;
@FunctionalInterface
public interface GenerationBoundarySignatureSampler {
TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException;
}
@@ -0,0 +1,733 @@
package art.arcane.iris.engine.history;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrays;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.DigestOutputStream;
import java.util.Collection;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.zip.CRC32;
public final class GenerationBoundaryStore {
public static final String SNAPSHOT_FILE_SUFFIX = ".irgb";
private static final String MASK_FILE_SUFFIX = ".irbm";
private static final int MAGIC = 0x49524742;
private static final int SCHEMA_VERSION = 2;
private static final int MASK_MAGIC = 0x4952424D;
private static final int MASK_SCHEMA_VERSION = 1;
private static final int IDENTITY_BYTES = 32;
private static final int CATALOG_FIXED_BODY_BYTES = 56;
private static final int CATALOG_ENTRY_BYTES = 40;
private static final int CHECKSUM_BYTES = Integer.BYTES;
private static final int REGION_SIDE = 32;
private static final int CHUNKS_PER_REGION = REGION_SIDE * REGION_SIDE;
private static final int MASK_BYTES = CHUNKS_PER_REGION / Byte.SIZE;
private static final int MASK_BODY_BYTES = 8 + MASK_BYTES;
private static final int MAXIMUM_CACHED_MASKS = 64;
private static final int MAX_REGION_COUNT = 8_388_608;
private static final long MAX_CATALOG_BYTES = CATALOG_FIXED_BODY_BYTES
+ (long) MAX_REGION_COUNT * CATALOG_ENTRY_BYTES
+ CHECKSUM_BYTES;
private static final int TRANSFER_BYTES = 65_536;
private static final byte[] IDENTITY_DOMAIN = {
0x49, 0x72, 0x69, 0x73, 0x42, 0x6F, 0x75, 0x6E,
0x64, 0x61, 0x72, 0x79, 0x02
};
private final Path dimensionRoot;
private final Path directory;
public GenerationBoundaryStore(Path dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "Dimension root").toAbsolutePath().normalize();
this.dimensionRoot = root;
this.directory = root.resolve("iris").resolve("generation").resolve("boundaries").normalize();
if (!directory.startsWith(root)) {
throw new IllegalArgumentException("Generation boundary path escapes the dimension root");
}
}
public Path directory() {
return directory;
}
public Path snapshotPath(long activationId) {
requireActivation(activationId);
return directory.resolve(fileName(activationId));
}
public synchronized GenerationBoundary publish(
long activationId,
Collection<GenerationBoundary.ChunkCoordinate> historicalChunks
) throws IOException {
Collection<GenerationBoundary.ChunkCoordinate> requiredChunks = Objects.requireNonNull(
historicalChunks,
"Historical chunks"
);
MaskCatalogBuilder builder = new MaskCatalogBuilder();
for (GenerationBoundary.ChunkCoordinate chunk : requiredChunks) {
GenerationBoundary.ChunkCoordinate requiredChunk = Objects.requireNonNull(
chunk,
"Historical chunk"
);
builder.add(requiredChunk.chunkX(), requiredChunk.chunkZ());
}
return publishCatalog(activationId, builder);
}
public synchronized GenerationBoundary publishPacked(long activationId, long[] historicalChunks)
throws IOException {
long[] requiredChunks = Objects.requireNonNull(historicalChunks, "Historical chunks");
MaskCatalogBuilder builder = new MaskCatalogBuilder();
for (long chunk : requiredChunks) {
builder.add((int) (chunk >> Integer.SIZE), (int) chunk);
}
return publishCatalog(activationId, builder);
}
synchronized GenerationBoundary publishOwnership(
long activationId,
ChunkGenerationOwnership ownership
) throws IOException {
ChunkGenerationOwnership requiredOwnership = Objects.requireNonNull(ownership, "Generation ownership");
MaskCatalogBuilder builder = new MaskCatalogBuilder();
requiredOwnership.forEachAssignment((chunkX, chunkZ, owner) -> builder.add(chunkX, chunkZ));
return publishCatalog(activationId, builder);
}
public GenerationBoundary load(long activationId) throws IOException {
requireActivation(activationId);
validateStorageAncestors();
Path snapshot = snapshotPath(activationId);
requireRegularFile(snapshot, "Generation boundary snapshot");
long fileSize = Files.size(snapshot);
if (fileSize < CATALOG_FIXED_BODY_BYTES + CHECKSUM_BYTES) {
throw invalid(snapshot, "truncated data");
}
if (fileSize > MAX_CATALOG_BYTES) {
throw invalid(snapshot, "catalog exceeds the maximum size");
}
verifyChecksum(snapshot, fileSize, "generation boundary snapshot");
long bodyBytes = fileSize - CHECKSUM_BYTES;
try (InputStream fileInput = Files.newInputStream(snapshot);
LimitedInputStream limitedInput = new LimitedInputStream(fileInput, bodyBytes);
DataInputStream input = new DataInputStream(limitedInput)) {
int magic = input.readInt();
if (magic != MAGIC) {
throw invalid(snapshot, "invalid magic");
}
int version = input.readUnsignedShort();
if (version != SCHEMA_VERSION) {
throw invalid(snapshot, "unsupported schema version " + version);
}
int flags = input.readUnsignedShort();
if (flags != 0) {
throw invalid(snapshot, "unsupported format flags " + flags);
}
long storedActivation = input.readLong();
if (storedActivation != activationId) {
throw invalid(snapshot, "activation ID does not match the snapshot path");
}
int chunkCount = input.readInt();
if (chunkCount < 0) {
throw invalid(snapshot, "invalid chunk count " + chunkCount);
}
byte[] storedIdentity = input.readNBytes(IDENTITY_BYTES);
if (storedIdentity.length != IDENTITY_BYTES) {
throw invalid(snapshot, "truncated snapshot identity");
}
int regionCount = input.readInt();
if (regionCount < 0 || regionCount > MAX_REGION_COUNT) {
throw invalid(snapshot, "invalid region count " + regionCount);
}
long expectedSize = CATALOG_FIXED_BODY_BYTES
+ (long) regionCount * CATALOG_ENTRY_BYTES
+ CHECKSUM_BYTES;
if (fileSize != expectedSize) {
throw invalid(snapshot, "catalog length does not match the region count");
}
long[] regionKeys = new long[regionCount];
Long2ObjectOpenHashMap<String> shardHashes = new Long2ObjectOpenHashMap<>(regionCount);
int countedChunks = 0;
long previousRegionKey = 0L;
for (int index = 0; index < regionCount; index++) {
int regionX = input.readInt();
int regionZ = input.readInt();
long regionKey = packRegion(regionX, regionZ);
if (index > 0 && compareRegions(previousRegionKey, regionKey) >= 0) {
throw invalid(snapshot, "region entries are not in canonical order");
}
byte[] hashBytes = input.readNBytes(IDENTITY_BYTES);
if (hashBytes.length != IDENTITY_BYTES) {
throw invalid(snapshot, "truncated mask hash");
}
String hash = HexFormat.of().formatHex(hashBytes);
byte[] mask = readMask(hash);
countedChunks = Math.addExact(countedChunks, countBits(mask));
regionKeys[index] = regionKey;
shardHashes.put(regionKey, hash);
previousRegionKey = regionKey;
}
if (limitedInput.remaining() != 0L) {
throw invalid(snapshot, "unexpected trailing catalog data");
}
if (countedChunks != chunkCount) {
throw invalid(snapshot, "chunk count does not match the referenced masks");
}
byte[] computedIdentity = computeIdentity(activationId, chunkCount, regionKeys, shardHashes);
if (!MessageDigest.isEqual(storedIdentity, computedIdentity)) {
throw invalid(snapshot, "boundary identity mismatch");
}
MaskSource source = new MaskSource(directory, regionKeys, shardHashes);
return GenerationBoundary.backed(toHex(computedIdentity), chunkCount, source);
} catch (ArithmeticException error) {
throw invalid(snapshot, "chunk count overflows the supported range", error);
} catch (EOFException error) {
throw invalid(snapshot, "truncated data", error);
}
}
private GenerationBoundary publishCatalog(long activationId, MaskCatalogBuilder builder) throws IOException {
requireActivation(activationId);
ensureDirectory();
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(
directory,
".activation-" + activationId + SNAPSHOT_FILE_SUFFIX + ".lock"
)) {
ensureDirectory();
PublishedCatalog catalog = builder.publish(directory, activationId);
Path target = snapshotPath(activationId);
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
GenerationBoundary existing = load(activationId);
if (!existing.identity().equals(catalog.identity())) {
throw new FileAlreadyExistsException(
"Generation boundary activation " + activationId
+ " already has a different snapshot: " + target
);
}
return existing;
}
publishAtomic(target, catalog.encoded(), "generation boundary catalog");
return load(activationId);
}
}
private byte[] readMask(String hash) throws IOException {
Path file = directory.resolve(maskFileName(hash));
requireRegularFile(file, "Generation boundary mask");
return MaskShard.read(file, hash);
}
private void ensureDirectory() throws IOException {
requireSafeDirectory(dimensionRoot);
Path irisDirectory = ensureChildDirectory(dimensionRoot, "iris");
Path generationDirectory = ensureChildDirectory(irisDirectory, "generation");
Path boundaryDirectory = ensureChildDirectory(generationDirectory, "boundaries");
if (!boundaryDirectory.equals(directory)) {
throw new IOException("Generation boundary resolved outside its storage path: " + boundaryDirectory);
}
}
private void validateStorageAncestors() throws IOException {
requireSafeDirectory(dimensionRoot);
requireSafeDirectory(dimensionRoot.resolve("iris"));
requireSafeDirectory(dimensionRoot.resolve("iris").resolve("generation"));
requireSafeDirectory(directory);
}
private static Path ensureChildDirectory(Path parent, String name) throws IOException {
Path child = parent.resolve(name);
if (Files.exists(child, LinkOption.NOFOLLOW_LINKS)) {
requireSafeDirectory(child);
return child;
}
try {
Files.createDirectory(child);
} catch (FileAlreadyExistsException race) {
requireSafeDirectory(child);
return child;
}
forceDirectory(parent);
return child;
}
private static void requireSafeDirectory(Path path) throws IOException {
if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
throw new NoSuchFileException(path.toString());
}
BasicFileAttributes attributes = Files.readAttributes(
path,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException("Generation boundary path is not a safe directory: " + path);
}
}
private static void requireRegularFile(Path path, String label) throws IOException {
if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
throw new NoSuchFileException(path.toString());
}
BasicFileAttributes attributes = Files.readAttributes(
path,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isRegularFile() || attributes.isSymbolicLink()) {
throw new IOException(label + " is not a safe regular file: " + path);
}
}
private static byte[] computeIdentity(
long activationId,
int chunkCount,
long[] regionKeys,
Long2ObjectOpenHashMap<String> shardHashes
) throws IOException {
MessageDigest digest = sha256();
digest.update(IDENTITY_DOMAIN);
try (DigestOutputStream digestOutput = new DigestOutputStream(OutputStream.nullOutputStream(), digest);
DataOutputStream output = new DataOutputStream(digestOutput)) {
output.writeLong(activationId);
output.writeInt(chunkCount);
output.writeInt(regionKeys.length);
for (long regionKey : regionKeys) {
output.writeInt(regionX(regionKey));
output.writeInt(regionZ(regionKey));
output.write(HexFormat.of().parseHex(shardHashes.get(regionKey)));
}
}
return digest.digest();
}
private static byte[] encodeCatalog(
long activationId,
int chunkCount,
long[] regionKeys,
Long2ObjectOpenHashMap<String> shardHashes,
byte[] identity
) throws IOException {
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream(
CATALOG_FIXED_BODY_BYTES + regionKeys.length * CATALOG_ENTRY_BYTES
);
try (DataOutputStream output = new DataOutputStream(bodyBytes)) {
output.writeInt(MAGIC);
output.writeShort(SCHEMA_VERSION);
output.writeShort(0);
output.writeLong(activationId);
output.writeInt(chunkCount);
output.write(identity);
output.writeInt(regionKeys.length);
for (long regionKey : regionKeys) {
output.writeInt(regionX(regionKey));
output.writeInt(regionZ(regionKey));
output.write(HexFormat.of().parseHex(shardHashes.get(regionKey)));
}
}
return withChecksum(bodyBytes.toByteArray());
}
private static void publishAtomic(Path target, byte[] encoded, String kind) throws IOException {
Path directory = target.getParent();
Path temporary = Files.createTempFile(directory, ".boundary-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
writeFully(channel, ByteBuffer.wrap(encoded));
channel.force(true);
}
try {
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException error) {
throw new IOException("Generation boundary " + kind + " publication requires an atomic move", error);
}
forceDirectory(directory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static byte[] withChecksum(byte[] body) throws IOException {
CRC32 checksum = new CRC32();
checksum.update(body);
ByteArrayOutputStream encoded = new ByteArrayOutputStream(body.length + CHECKSUM_BYTES);
encoded.write(body);
try (DataOutputStream output = new DataOutputStream(encoded)) {
output.writeInt((int) checksum.getValue());
}
return encoded.toByteArray();
}
private static void verifyChecksum(Path file, long fileSize, String kind) throws IOException {
long bodyBytes = fileSize - CHECKSUM_BYTES;
CRC32 checksum = new CRC32();
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(TRANSFER_BYTES);
long remaining = bodyBytes;
while (remaining > 0L) {
buffer.clear();
buffer.limit((int) Math.min(buffer.capacity(), remaining));
readFully(channel, buffer);
buffer.flip();
checksum.update(buffer);
remaining -= buffer.limit();
}
ByteBuffer stored = ByteBuffer.allocate(CHECKSUM_BYTES);
readFully(channel, stored);
stored.flip();
if (stored.getInt() != (int) checksum.getValue()) {
throw new IOException("Invalid " + kind + " " + file + ": checksum mismatch");
}
}
}
private static int countBits(byte[] mask) {
int count = 0;
for (byte value : mask) {
count += Integer.bitCount(value & 0xff);
}
return count;
}
private static long packRegion(int regionX, int regionZ) {
return ((long) regionX << Integer.SIZE) | (regionZ & 0xffffffffL);
}
private static int regionX(long regionKey) {
return (int) (regionKey >> Integer.SIZE);
}
private static int regionZ(long regionKey) {
return (int) regionKey;
}
private static int compareRegions(long first, long second) {
int xComparison = Integer.compare(regionX(first), regionX(second));
return xComparison != 0 ? xComparison : Integer.compare(regionZ(first), regionZ(second));
}
private static String fileName(long activationId) {
return "activation-" + activationId + SNAPSHOT_FILE_SUFFIX;
}
private static String maskFileName(String hash) {
return "chunk-mask-" + hash + MASK_FILE_SUFFIX;
}
private static void requireActivation(long activationId) {
if (activationId <= 0L) {
throw new IllegalArgumentException("Generation activation IDs must be positive: " + activationId);
}
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException error) {
throw new IllegalStateException("SHA-256 is unavailable", error);
}
}
private static String sha256(byte[] encoded) {
return HexFormat.of().formatHex(sha256().digest(encoded));
}
private static String toHex(byte[] bytes) {
return HexFormat.of().formatHex(bytes);
}
private static void writeFully(FileChannel channel, ByteBuffer buffer) throws IOException {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
private static void readFully(FileChannel channel, ByteBuffer buffer) throws IOException {
while (buffer.hasRemaining()) {
if (channel.read(buffer) < 0) {
throw new EOFException("Unexpected end of generation boundary data");
}
}
}
private static void forceDirectory(Path path) throws IOException {
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true);
} catch (UnsupportedOperationException error) {
throw new IOException("Generation boundary directory cannot be durability-synced: " + path, error);
}
}
private static IOException invalid(Path path, String reason) {
return new IOException("Invalid generation boundary snapshot " + path + ": " + reason);
}
private static IOException invalid(Path path, String reason, Throwable cause) {
return new IOException("Invalid generation boundary snapshot " + path + ": " + reason, cause);
}
private static final class MaskCatalogBuilder {
private final Long2ObjectOpenHashMap<byte[]> masks = new Long2ObjectOpenHashMap<>();
private int chunkCount;
private void add(int chunkX, int chunkZ) {
int regionX = chunkX >> 5;
int regionZ = chunkZ >> 5;
long regionKey = packRegion(regionX, regionZ);
byte[] mask = masks.computeIfAbsent(regionKey, ignored -> new byte[MASK_BYTES]);
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
| Math.floorMod(chunkX, REGION_SIDE);
int byteIndex = localIndex >>> 3;
int bit = 1 << (localIndex & 7);
if ((mask[byteIndex] & bit) == 0) {
mask[byteIndex] = (byte) (mask[byteIndex] | bit);
chunkCount = Math.addExact(chunkCount, 1);
}
}
private PublishedCatalog publish(Path directory, long activationId) throws IOException {
if (masks.size() > MAX_REGION_COUNT) {
throw new IOException("Generation boundary exceeds the maximum region count");
}
long[] regionKeys = masks.keySet().toLongArray();
sortRegionKeys(regionKeys);
Long2ObjectOpenHashMap<String> shardHashes = new Long2ObjectOpenHashMap<>(regionKeys.length);
for (long regionKey : regionKeys) {
String hash = MaskShard.publish(directory, masks.get(regionKey));
shardHashes.put(regionKey, hash);
}
byte[] identity = computeIdentity(activationId, chunkCount, regionKeys, shardHashes);
byte[] encoded = encodeCatalog(activationId, chunkCount, regionKeys, shardHashes, identity);
return new PublishedCatalog(toHex(identity), encoded);
}
private static void sortRegionKeys(long[] regionKeys) {
LongArrays.quickSort(regionKeys, GenerationBoundaryStore::compareRegions);
}
}
private record PublishedCatalog(String identity, byte[] encoded) {
}
private static final class MaskShard {
private static String publish(Path directory, byte[] mask) throws IOException {
byte[] encoded = encode(mask);
String hash = sha256(encoded);
Path target = directory.resolve(maskFileName(hash));
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
requireRegularFile(target, "Generation boundary mask");
read(target, hash);
return hash;
}
try {
publishAtomic(target, encoded, "mask shard");
} catch (FileAlreadyExistsException race) {
requireRegularFile(target, "Generation boundary mask");
read(target, hash);
}
return hash;
}
private static byte[] encode(byte[] mask) throws IOException {
if (mask.length != MASK_BYTES || countBits(mask) == 0) {
throw new IOException("Generation boundary mask is empty or malformed");
}
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream(MASK_BODY_BYTES);
try (DataOutputStream output = new DataOutputStream(bodyBytes)) {
output.writeInt(MASK_MAGIC);
output.writeShort(MASK_SCHEMA_VERSION);
output.writeShort(0);
output.write(mask);
}
return withChecksum(bodyBytes.toByteArray());
}
private static byte[] read(Path file, String expectedHash) throws IOException {
byte[] encoded = Files.readAllBytes(file);
if (encoded.length != MASK_BODY_BYTES + CHECKSUM_BYTES) {
throw new IOException("Invalid generation boundary mask " + file + ": invalid length");
}
if (!sha256(encoded).equals(expectedHash)) {
throw new IOException("Invalid generation boundary mask " + file + ": content hash mismatch");
}
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, MASK_BODY_BYTES);
if (ByteBuffer.wrap(encoded, MASK_BODY_BYTES, CHECKSUM_BYTES).getInt()
!= (int) checksum.getValue()) {
throw new IOException("Invalid generation boundary mask " + file + ": checksum mismatch");
}
try (DataInputStream input = new DataInputStream(
new ByteArrayInputStream(encoded, 0, MASK_BODY_BYTES)
)) {
if (input.readInt() != MASK_MAGIC) {
throw new IOException("Invalid generation boundary mask " + file + ": invalid magic");
}
int version = input.readUnsignedShort();
if (version != MASK_SCHEMA_VERSION) {
throw new IOException("Invalid generation boundary mask " + file
+ ": unsupported schema version " + version);
}
if (input.readUnsignedShort() != 0) {
throw new IOException("Invalid generation boundary mask " + file + ": unsupported flags");
}
byte[] mask = input.readNBytes(MASK_BYTES);
if (mask.length != MASK_BYTES || countBits(mask) == 0 || input.available() != 0) {
throw new IOException("Invalid generation boundary mask " + file + ": malformed mask");
}
return mask;
}
}
}
private static final class MaskSource implements GenerationBoundary.HistoricalChunkSource {
private final Path directory;
private final long[] regionKeys;
private final Long2ObjectOpenHashMap<String> shardHashes;
private final LinkedHashMap<Long, byte[]> masks;
private MaskSource(
Path directory,
long[] regionKeys,
Long2ObjectOpenHashMap<String> shardHashes
) {
this.directory = directory;
this.regionKeys = regionKeys;
this.shardHashes = shardHashes;
masks = new LinkedHashMap<>(MAXIMUM_CACHED_MASKS, 0.75F, true);
}
@Override
public synchronized 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);
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
| Math.floorMod(chunkX, REGION_SIDE);
return (mask[localIndex >>> 3] & (1 << (localIndex & 7))) != 0;
}
@Override
public synchronized void forEach(GenerationBoundary.ChunkConsumer consumer) throws IOException {
for (long regionKey : regionKeys) {
byte[] mask = load(regionKey, shardHashes.get(regionKey));
int minimumChunkX = regionX(regionKey) << 5;
int minimumChunkZ = regionZ(regionKey) << 5;
for (int localIndex = 0; localIndex < CHUNKS_PER_REGION; localIndex++) {
if ((mask[localIndex >>> 3] & (1 << (localIndex & 7))) == 0) {
continue;
}
consumer.accept(
minimumChunkX + (localIndex & 31),
minimumChunkZ + (localIndex >>> 5)
);
}
}
}
@Override
public synchronized int cachedRegionCount() {
return masks.size();
}
private byte[] load(long regionKey, String hash) throws IOException {
byte[] cached = masks.get(regionKey);
if (cached != null) {
return cached;
}
Path file = directory.resolve(maskFileName(hash));
requireRegularFile(file, "Generation boundary mask");
byte[] loaded = MaskShard.read(file, hash);
masks.put(regionKey, loaded);
while (masks.size() > MAXIMUM_CACHED_MASKS) {
Iterator<Map.Entry<Long, byte[]>> entries = masks.entrySet().iterator();
entries.next();
entries.remove();
}
return loaded;
}
}
private static final class LimitedInputStream extends InputStream {
private final InputStream input;
private long remaining;
private LimitedInputStream(InputStream input, long remaining) {
this.input = input;
this.remaining = remaining;
}
@Override
public int read() throws IOException {
if (remaining == 0L) {
return -1;
}
int value = input.read();
if (value < 0) {
throw new EOFException("Unexpected end of bounded generation boundary input");
}
remaining--;
return value;
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
Objects.checkFromIndexSize(offset, length, buffer.length);
if (remaining == 0L) {
return -1;
}
int read = input.read(buffer, offset, (int) Math.min(length, remaining));
if (read < 0) {
throw new EOFException("Unexpected end of bounded generation boundary input");
}
remaining -= read;
return read;
}
@Override
public void close() throws IOException {
input.close();
}
private long remaining() {
return remaining;
}
}
}
@@ -0,0 +1,449 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonObject;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Objects;
import java.util.regex.Pattern;
public final class GenerationEpoch {
public static final int SEED_DERIVATION_VERSION_ONE = 1;
public static final int CURRENT_SEED_DERIVATION_VERSION = SEED_DERIVATION_VERSION_ONE;
private static final int IDENTITY_SCHEMA = 1;
private static final Pattern SHA_256_PATTERN = Pattern.compile("[0-9a-f]{64}");
private static final Pattern DIMENSION_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*");
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
private static final Pattern ENVIRONMENT_PATTERN = Pattern.compile("[A-Za-z0-9_.:-]+");
private final String epochId;
private final Spec spec;
private GenerationEpoch(String epochId, Spec spec) {
this.spec = Objects.requireNonNull(spec, "spec");
this.epochId = requireSha256(epochId, "Epoch ID");
String expectedEpochId = identity(spec);
if (!this.epochId.equals(expectedEpochId)) {
throw new IllegalArgumentException("Epoch ID does not match its generation inputs.");
}
}
public static GenerationEpoch create(Spec spec) {
Spec requiredSpec = Objects.requireNonNull(spec, "spec");
return new GenerationEpoch(identity(requiredSpec), requiredSpec);
}
static GenerationEpoch restore(String epochId, Spec spec) {
return new GenerationEpoch(epochId, spec);
}
public String epochId() {
return epochId;
}
public String packFingerprint() {
return spec.packFingerprint();
}
public int generatorAbi() {
return spec.generatorAbi();
}
public int packFingerprintVersion() {
return spec.packFingerprintVersion();
}
public int rngVersion() {
return spec.rngVersion();
}
public String kernelImplementationFingerprint() {
return spec.kernelImplementationFingerprint();
}
public long worldSeed() {
return spec.worldSeed();
}
public int seedDerivationVersion() {
return spec.seedDerivationVersion();
}
public GenerationKernelRegistry.Version kernelVersion() {
return new GenerationKernelRegistry.Version(
generatorAbi(),
rngVersion(),
seedDerivationVersion()
);
}
public DimensionContract dimensionContract() {
return spec.dimensionContract();
}
public GenerationRegistryContract registryContract() {
return spec.registryContract();
}
public Spec spec() {
return spec;
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("epochId", epochId);
json.addProperty("packFingerprint", packFingerprint());
json.addProperty("packFingerprintVersion", packFingerprintVersion());
json.addProperty("worldSeed", worldSeed());
json.addProperty("seedDerivationVersion", seedDerivationVersion());
json.addProperty("generatorAbi", generatorAbi());
json.addProperty("rngVersion", rngVersion());
json.addProperty("kernelImplementationFingerprint", kernelImplementationFingerprint());
json.add("dimensionContract", dimensionContract().toJson());
json.add("registryContract", registryContract().toJson());
return json;
}
static GenerationEpoch fromJson(JsonObject json) {
GenerationManifest.JsonSchema.requireFields(
json,
"epoch",
"epochId",
"packFingerprint",
"packFingerprintVersion",
"worldSeed",
"seedDerivationVersion",
"generatorAbi",
"rngVersion",
"kernelImplementationFingerprint",
"dimensionContract",
"registryContract"
);
String epochId = GenerationManifest.JsonSchema.requireString(json, "epochId", "epoch");
String packFingerprint = GenerationManifest.JsonSchema.requireString(json, "packFingerprint", "epoch");
int packFingerprintVersion = GenerationManifest.JsonSchema.requireInt(
json,
"packFingerprintVersion",
"epoch"
);
long worldSeed = GenerationManifest.JsonSchema.requireLong(json, "worldSeed", "epoch");
int seedDerivationVersion = GenerationManifest.JsonSchema.requireInt(
json,
"seedDerivationVersion",
"epoch"
);
int generatorAbi = GenerationManifest.JsonSchema.requireInt(json, "generatorAbi", "epoch");
int rngVersion = GenerationManifest.JsonSchema.requireInt(json, "rngVersion", "epoch");
String kernelImplementationFingerprint = GenerationManifest.JsonSchema.requireString(
json,
"kernelImplementationFingerprint",
"epoch"
);
JsonObject contractJson = GenerationManifest.JsonSchema.requireObject(json, "dimensionContract", "epoch");
DimensionContract contract = DimensionContract.fromJson(contractJson);
JsonObject registryContractJson = GenerationManifest.JsonSchema.requireObject(
json,
"registryContract",
"epoch"
);
GenerationRegistryContract registryContract = GenerationRegistryContract.fromJson(registryContractJson);
return restore(epochId, new Spec(
packFingerprint,
packFingerprintVersion,
worldSeed,
seedDerivationVersion,
generatorAbi,
rngVersion,
kernelImplementationFingerprint,
contract,
registryContract
));
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GenerationEpoch epoch)) {
return false;
}
return epochId.equals(epoch.epochId) && spec.equals(epoch.spec);
}
@Override
public int hashCode() {
return Objects.hash(epochId, spec);
}
@Override
public String toString() {
return "GenerationEpoch[epochId=" + epochId + ", spec=" + spec + "]";
}
private static String identity(Spec spec) {
MessageDigest digest = sha256();
updateString(digest, "iris-generation-epoch");
updateInt(digest, IDENTITY_SCHEMA);
updateString(digest, spec.packFingerprint());
updateInt(digest, spec.packFingerprintVersion());
updateLong(digest, spec.worldSeed());
updateInt(digest, spec.seedDerivationVersion());
updateInt(digest, spec.generatorAbi());
updateInt(digest, spec.rngVersion());
updateString(digest, spec.kernelImplementationFingerprint());
DimensionContract contract = spec.dimensionContract();
updateString(digest, contract.dimensionKey());
updateString(digest, contract.dimensionTypeKey());
updateString(digest, contract.environment());
updateString(digest, contract.generationMode());
updateInt(digest, contract.internalFluidHeight());
updateInt(digest, contract.minHeight());
updateInt(digest, contract.height());
updateInt(digest, contract.logicalHeight());
updateLong(digest, Double.doubleToLongBits(contract.coordinateScale()));
digest.update((byte) (contract.upperTerrainEnabled() ? 1 : 0));
updateString(digest, contract.upperDimensionKey());
updateInt(digest, contract.upperDimensionGap());
updateString(digest, contract.upperTerrainPackFingerprint());
updateInt(digest, contract.dimensionTypeFingerprintSchema());
updateString(digest, contract.dimensionTypeFingerprint());
GenerationRegistryContract registryContract = spec.registryContract();
updateInt(digest, registryContract.fingerprintSchema());
updateString(digest, registryContract.fingerprint());
return HexFormat.of().formatHex(digest.digest());
}
private static String requireSha256(String value, String label) {
String requiredValue = Objects.requireNonNull(value, label);
if (!SHA_256_PATTERN.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " must be a lowercase SHA-256 value.");
}
return requiredValue;
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable.", exception);
}
}
private static void updateString(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
updateInt(digest, bytes.length);
digest.update(bytes);
}
private static void updateInt(MessageDigest digest, int value) {
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(value).array());
}
private static void updateLong(MessageDigest digest, long value) {
digest.update(ByteBuffer.allocate(Long.BYTES).putLong(value).array());
}
public record Spec(
String packFingerprint,
int packFingerprintVersion,
long worldSeed,
int seedDerivationVersion,
int generatorAbi,
int rngVersion,
String kernelImplementationFingerprint,
DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) {
public Spec {
packFingerprint = requireSha256(packFingerprint, "Pack fingerprint");
if (packFingerprintVersion < 1) {
throw new IllegalArgumentException("Pack fingerprint version must be positive.");
}
if (seedDerivationVersion < 1) {
throw new IllegalArgumentException("Seed derivation version must be positive.");
}
if (generatorAbi < 1) {
throw new IllegalArgumentException("Generator ABI must be positive.");
}
if (rngVersion < 1) {
throw new IllegalArgumentException("RNG version must be positive.");
}
kernelImplementationFingerprint = requireSha256(
kernelImplementationFingerprint,
"Generation kernel implementation fingerprint"
);
dimensionContract = Objects.requireNonNull(dimensionContract, "dimensionContract");
registryContract = Objects.requireNonNull(registryContract, "registryContract");
}
}
public record DimensionContract(
String dimensionKey,
String dimensionTypeKey,
String environment,
String generationMode,
int internalFluidHeight,
int minHeight,
int height,
int logicalHeight,
double coordinateScale,
boolean upperTerrainEnabled,
String upperDimensionKey,
int upperDimensionGap,
String upperTerrainPackFingerprint,
int dimensionTypeFingerprintSchema,
String dimensionTypeFingerprint
) {
public DimensionContract {
dimensionKey = requirePattern(dimensionKey, "Dimension key", DIMENSION_KEY_PATTERN);
dimensionTypeKey = requirePattern(dimensionTypeKey, "Dimension type key", RESOURCE_KEY_PATTERN);
environment = requirePattern(environment, "Dimension environment", ENVIRONMENT_PATTERN);
generationMode = requirePattern(generationMode, "Generation mode", ENVIRONMENT_PATTERN);
if (height < 1) {
throw new IllegalArgumentException("Dimension height must be positive.");
}
Math.addExact(minHeight, height);
if (logicalHeight < 0 || logicalHeight > height) {
throw new IllegalArgumentException("Logical height must be inside the dimension height.");
}
if (!Double.isFinite(coordinateScale) || coordinateScale <= 0D || coordinateScale > 30000000D) {
throw new IllegalArgumentException("Coordinate scale must be finite and inside (0, 30000000].");
}
upperDimensionKey = requirePattern(
upperDimensionKey,
"Upper dimension key",
DIMENSION_KEY_PATTERN
);
if (upperDimensionGap < 0) {
throw new IllegalArgumentException("Upper dimension gap must not be negative.");
}
upperTerrainPackFingerprint = requireSha256(
upperTerrainPackFingerprint,
"Upper terrain pack fingerprint"
);
if (upperTerrainEnabled == upperDimensionKey.equals("none")) {
throw new IllegalArgumentException("Upper terrain enablement and dimension key disagree.");
}
if (upperTerrainEnabled == upperTerrainPackFingerprint.equals("0".repeat(64))) {
throw new IllegalArgumentException("Upper terrain enablement and pack fingerprint disagree.");
}
if (!upperTerrainEnabled && upperDimensionGap != 0) {
throw new IllegalArgumentException("Disabled upper terrain must have a zero gap.");
}
GenerationEpochContractFactory.requireSupportedDimensionTypeFingerprintSchema(
dimensionTypeFingerprintSchema
);
dimensionTypeFingerprint = requireSha256(dimensionTypeFingerprint, "Dimension type fingerprint");
}
public boolean hasSameLayout(DimensionContract other) {
Objects.requireNonNull(other, "Dimension contract");
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);
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("dimensionKey", dimensionKey);
json.addProperty("dimensionTypeKey", dimensionTypeKey);
json.addProperty("environment", environment);
json.addProperty("generationMode", generationMode);
json.addProperty("internalFluidHeight", internalFluidHeight);
json.addProperty("minHeight", minHeight);
json.addProperty("height", height);
json.addProperty("logicalHeight", logicalHeight);
json.addProperty("coordinateScale", coordinateScale);
json.addProperty("upperTerrainEnabled", upperTerrainEnabled);
json.addProperty("upperDimensionKey", upperDimensionKey);
json.addProperty("upperDimensionGap", upperDimensionGap);
json.addProperty("upperTerrainPackFingerprint", upperTerrainPackFingerprint);
json.addProperty("dimensionTypeFingerprintSchema", dimensionTypeFingerprintSchema);
json.addProperty("dimensionTypeFingerprint", dimensionTypeFingerprint);
return json;
}
static DimensionContract fromJson(JsonObject json) {
GenerationManifest.JsonSchema.requireFields(
json,
"dimension contract",
"dimensionKey",
"dimensionTypeKey",
"environment",
"generationMode",
"internalFluidHeight",
"minHeight",
"height",
"logicalHeight",
"coordinateScale",
"upperTerrainEnabled",
"upperDimensionKey",
"upperDimensionGap",
"upperTerrainPackFingerprint",
"dimensionTypeFingerprintSchema",
"dimensionTypeFingerprint"
);
return new DimensionContract(
GenerationManifest.JsonSchema.requireString(json, "dimensionKey", "dimension contract"),
GenerationManifest.JsonSchema.requireString(json, "dimensionTypeKey", "dimension contract"),
GenerationManifest.JsonSchema.requireString(json, "environment", "dimension contract"),
GenerationManifest.JsonSchema.requireString(json, "generationMode", "dimension contract"),
GenerationManifest.JsonSchema.requireInt(json, "internalFluidHeight", "dimension contract"),
GenerationManifest.JsonSchema.requireInt(json, "minHeight", "dimension contract"),
GenerationManifest.JsonSchema.requireInt(json, "height", "dimension contract"),
GenerationManifest.JsonSchema.requireInt(json, "logicalHeight", "dimension contract"),
GenerationManifest.JsonSchema.requireDouble(json, "coordinateScale", "dimension contract"),
GenerationManifest.JsonSchema.requireBoolean(
json,
"upperTerrainEnabled",
"dimension contract"
),
GenerationManifest.JsonSchema.requireString(json, "upperDimensionKey", "dimension contract"),
GenerationManifest.JsonSchema.requireInt(json, "upperDimensionGap", "dimension contract"),
GenerationManifest.JsonSchema.requireString(
json,
"upperTerrainPackFingerprint",
"dimension contract"
),
GenerationManifest.JsonSchema.requireInt(
json,
"dimensionTypeFingerprintSchema",
"dimension contract"
),
GenerationManifest.JsonSchema.requireString(
json,
"dimensionTypeFingerprint",
"dimension contract"
)
);
}
public int maxHeight() {
return Math.addExact(minHeight, height);
}
private static String requirePattern(String value, String label, Pattern pattern) {
String requiredValue = Objects.requireNonNull(value, label);
if (!pattern.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " is invalid: " + requiredValue);
}
return requiredValue;
}
}
}
@@ -0,0 +1,211 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionMode;
import art.arcane.iris.engine.object.IrisDimensionModeType;
import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
import art.arcane.iris.engine.object.IrisEnvironment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Objects;
public final class GenerationEpochContractFactory {
public static final int DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE = 1;
public static final int CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA =
DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE;
private static final String FINGERPRINT_DOMAIN = "iris-dimension-type-authored-semantics";
private static final String NO_UPPER_TERRAIN_FINGERPRINT = "0".repeat(64);
private static final Gson SEMANTIC_JSON = new GsonBuilder().serializeNulls().create();
private GenerationEpochContractFactory() {
}
public static GenerationEpoch.DimensionContract create(
IrisDimension dimension,
String dimensionKey,
String dimensionTypeKey
) {
return create(
dimension,
dimensionKey,
dimensionTypeKey,
CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA
);
}
public static GenerationEpoch.DimensionContract create(
IrisDimension dimension,
String dimensionKey,
String dimensionTypeKey,
int dimensionTypeFingerprintSchema
) {
requireSupportedDimensionTypeFingerprintSchema(dimensionTypeFingerprintSchema);
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisEnvironment environment = Objects.requireNonNull(
requiredDimension.getEnvironment(),
"Dimension environment"
);
IrisDimensionType dimensionType = requiredDimension.getDimensionType();
requireDimensionAgreement(requiredDimension, dimensionType);
IrisDimensionMode configuredMode = requiredDimension.getMode();
IrisDimensionModeType mode = configuredMode == null || configuredMode.getType() == null
? IrisDimensionModeType.OVERWORLD
: configuredMode.getType();
boolean upperTerrainEnabled = requiredDimension.hasUpperDimension();
String upperDimensionKey = upperTerrainEnabled
? requireUpperDimensionKey(requiredDimension)
: "none";
return new GenerationEpoch.DimensionContract(
dimensionKey,
dimensionTypeKey,
environment.name(),
mode.name(),
requiredDimension.getFluidHeight(),
dimensionType.minY(),
dimensionType.height(),
dimensionType.logicalHeight(),
effectiveCoordinateScale(dimensionType),
upperTerrainEnabled,
upperDimensionKey,
upperTerrainEnabled ? requiredDimension.getUpperDimensionGap() : 0,
upperTerrainEnabled
? requireUpperTerrainPackFingerprint(requiredDimension, upperDimensionKey)
: NO_UPPER_TERRAIN_FINGERPRINT,
dimensionTypeFingerprintSchema,
fingerprintDimensionType(dimensionType, dimensionTypeFingerprintSchema)
);
}
public static String fingerprintDimensionType(IrisDimensionType dimensionType) {
return fingerprintDimensionType(
dimensionType,
CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA
);
}
public static String dimensionTypeSemanticJson(IrisDimensionType dimensionType) {
IrisDimensionType requiredType = Objects.requireNonNull(dimensionType, "dimensionType");
JsonObject semantic = new JsonObject();
semantic.addProperty("base", requiredType.base().name());
semantic.addProperty("logicalHeight", requiredType.logicalHeight());
semantic.addProperty("height", requiredType.height());
semantic.addProperty("minY", requiredType.minY());
semantic.add("options", SEMANTIC_JSON.toJsonTree(requiredType.options()));
return semantic.toString();
}
public static String fingerprintDimensionType(
IrisDimensionType dimensionType,
int fingerprintSchema
) {
requireSupportedDimensionTypeFingerprintSchema(fingerprintSchema);
IrisDimensionType requiredType = Objects.requireNonNull(dimensionType, "dimensionType");
MessageDigest digest = sha256();
updateString(digest, FINGERPRINT_DOMAIN);
updateInt(digest, fingerprintSchema);
updateString(digest, dimensionTypeSemanticJson(requiredType));
return HexFormat.of().formatHex(digest.digest());
}
public static void requireSupportedDimensionTypeFingerprintSchema(int fingerprintSchema) {
if (fingerprintSchema != DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE) {
throw new IllegalArgumentException(
"Unsupported dimension-type fingerprint schema " + fingerprintSchema + "."
);
}
}
private static double effectiveCoordinateScale(IrisDimensionType dimensionType) {
IrisDimensionTypeOptions options = dimensionType.options();
double configured = options.coordinateScale();
if (configured != -1D) {
return configured;
}
return dimensionType.base() == IDataFixer.Dimension.NETHER ? 8D : 1D;
}
private static String requireUpperDimensionKey(IrisDimension dimension) {
String key = Objects.requireNonNull(
dimension.getUpperDimension(),
"Upper dimension key"
).trim().toLowerCase(Locale.ROOT);
if (key.isEmpty() || key.equals("none")) {
throw new IllegalStateException("Enabled upper terrain requires a dimension key.");
}
return key;
}
private static String requireUpperTerrainPackFingerprint(
IrisDimension dimension,
String upperDimensionKey
) {
IrisData data = Objects.requireNonNull(
dimension.getLoader(),
"Upper terrain dimension loader"
);
if (!upperDimensionKey.equals(dimension.getLoadKey())) {
IrisDimension upperDimension = data.getDimensionLoader().load(upperDimensionKey, false);
if (upperDimension == null || upperDimension.getLoader() != data) {
throw new IllegalStateException("Upper dimension '" + upperDimensionKey
+ "' must exist in the same immutable Iris pack.");
}
}
try {
return GenerationPackFingerprint.compute(
data.getDataFolder().toPath(),
GenerationPackFingerprint.CURRENT_VERSION
);
} catch (IOException failure) {
throw new IllegalStateException("Unable to fingerprint the immutable upper terrain pack.", failure);
}
}
private static void requireDimensionAgreement(
IrisDimension dimension,
IrisDimensionType dimensionType
) {
int declaredMinHeight = dimension.getMinHeight();
int declaredHeight = Math.subtractExact(dimension.getMaxHeight(), declaredMinHeight);
int declaredLogicalHeight = dimension.getLogicalHeight();
if (dimensionType.minY() == declaredMinHeight
&& dimensionType.height() == declaredHeight
&& dimensionType.logicalHeight() == declaredLogicalHeight) {
return;
}
throw new IllegalStateException(
"Iris dimension-type semantics disagree with the dimension contract."
);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable.", exception);
}
}
private static void updateString(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
updateInt(digest, bytes.length);
digest.update(bytes);
}
private static void updateInt(MessageDigest digest, int value) {
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(value).array());
}
}
@@ -0,0 +1,221 @@
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;
public final class GenerationFindCatalog {
private static final Map<GenerationHistory, RetainedCatalog> RETAINED = new WeakHashMap<>();
private static final RetainedCatalog EMPTY = new RetainedCatalog(0L);
private GenerationFindCatalog() {
}
public static KList<IrisBiome> biomes(Engine engine) {
return merge(engine.getAllBiomes(), retained(engine).biomes);
}
public static IrisBiome biome(Engine engine, String key) {
return find(biomes(engine), key);
}
public static KList<IrisRegion> regions(Engine engine) {
return merge(engine.getDimension().getAllRegions(engine), retained(engine).regions);
}
public static IrisRegion region(Engine engine, String key) {
return find(regions(engine), key);
}
public static KList<String> objectKeys(Engine engine) {
Set<String> keys = new LinkedHashSet<>();
for (String key : engine.getData().getObjectLoader().getPossibleKeys()) {
keys.add(key);
}
keys.addAll(retained(engine).objects);
return new KList<>(keys);
}
public static boolean hasObjectPlacement(Engine engine, String key) {
return engine.hasObjectPlacement(key) || retained(engine).objects.contains(normalizeObjectKey(key));
}
public static KList<String> retainedStructureKeys(Engine engine) {
return new KList<>(retained(engine).structures);
}
public static boolean hasRetainedStructurePlacement(Engine engine, String key) {
return retained(engine).structures.contains(normalizeKey(key));
}
private static RetainedCatalog retained(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine)) {
return EMPTY;
}
return irisEngine.getGenerationHistoryRuntimeRouter()
.map(router -> retained(router.history()))
.orElse(EMPTY);
}
private static RetainedCatalog retained(GenerationHistory history) {
GenerationManifest manifest = history.manifest();
synchronized (RETAINED) {
RetainedCatalog catalog = RETAINED.get(history);
if (catalog != null && catalog.activeActivationId == manifest.activeActivation().activationId()) {
return catalog;
}
catalog = loadRetained(history, manifest);
RETAINED.put(history, catalog);
return catalog;
}
}
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();
}
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);
}
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();
}
}
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 <T extends IrisRegistrant> KList<T> merge(Iterable<T> active, Map<String, T> historical) {
Map<String, T> merged = new LinkedHashMap<>();
for (T registrant : active) {
merged.putIfAbsent(normalizeKey(registrant.getLoadKey()), registrant);
}
historical.forEach(merged::putIfAbsent);
return new KList<>(merged.values());
}
private static <T extends IrisRegistrant> T find(Iterable<T> entries, String key) {
String normalized = normalizeKey(key);
for (T entry : entries) {
if (normalizeKey(entry.getLoadKey()).equals(normalized)) {
return entry;
}
}
return null;
}
private static String normalizeKey(String key) {
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
}
private static String normalizeObjectKey(String key) {
String normalized = normalizeKey(key).replace('\\', '/');
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
while (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized.endsWith(".iob") ? normalized.substring(0, normalized.length() - 4) : normalized;
}
private static final class RetainedCatalog {
private final long activeActivationId;
private final Map<String, IrisBiome> biomes = new LinkedHashMap<>();
private final Map<String, IrisRegion> regions = new LinkedHashMap<>();
private final Set<String> objects = new LinkedHashSet<>();
private final Set<String> structures = new LinkedHashSet<>();
private RetainedCatalog(long activeActivationId) {
this.activeActivationId = activeActivationId;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,111 @@
package art.arcane.iris.engine.history;
import java.nio.file.Path;
import java.util.Objects;
import java.util.regex.Pattern;
public final class GenerationHistoryPaths {
public static final String IRIS_DIRECTORY_NAME = "iris";
public static final String GENERATION_DIRECTORY_NAME = "generation";
public static final String EPOCHS_DIRECTORY_NAME = "epochs";
public static final String ACTIVATIONS_DIRECTORY_NAME = "activations";
public static final String OWNERSHIP_DIRECTORY_NAME = "ownership";
public static final String LEGACY_PACK_DIRECTORY_NAME = "pack";
public static final String LEGACY_MANTLE_DIRECTORY_NAME = "mantle-hydrology";
public static final String REGION_DIRECTORY_NAME = "region";
private static final Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}");
private final Path dimensionRoot;
private final Path irisRoot;
private final Path generationRoot;
private final Path epochsRoot;
private final Path activationsRoot;
private final Path ownershipRoot;
private final Path manifest;
private final Path legacyPackRoot;
private final Path legacyMantleRoot;
private final Path regionRoot;
private GenerationHistoryPaths(Path dimensionRoot) {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toAbsolutePath()
.normalize();
irisRoot = this.dimensionRoot.resolve(IRIS_DIRECTORY_NAME);
generationRoot = irisRoot.resolve(GENERATION_DIRECTORY_NAME);
epochsRoot = generationRoot.resolve(EPOCHS_DIRECTORY_NAME);
activationsRoot = generationRoot.resolve(ACTIVATIONS_DIRECTORY_NAME);
ownershipRoot = generationRoot.resolve(OWNERSHIP_DIRECTORY_NAME);
manifest = generationRoot.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME);
legacyPackRoot = irisRoot.resolve(LEGACY_PACK_DIRECTORY_NAME);
legacyMantleRoot = this.dimensionRoot.resolve(LEGACY_MANTLE_DIRECTORY_NAME);
regionRoot = this.dimensionRoot.resolve(REGION_DIRECTORY_NAME);
}
public static GenerationHistoryPaths forDimension(Path dimensionRoot) {
return new GenerationHistoryPaths(dimensionRoot);
}
public Path dimensionRoot() {
return dimensionRoot;
}
public Path irisRoot() {
return irisRoot;
}
public Path generationRoot() {
return generationRoot;
}
public Path epochsRoot() {
return epochsRoot;
}
public Path epochRoot(String epochId) {
String requiredEpochId = Objects.requireNonNull(epochId, "epochId");
if (!SHA_256.matcher(requiredEpochId).matches()) {
throw new IllegalArgumentException("epochId must be a lowercase SHA-256 digest");
}
return epochsRoot.resolve(requiredEpochId);
}
public Path packRoot(String epochId) {
return epochRoot(epochId).resolve(LEGACY_PACK_DIRECTORY_NAME);
}
public Path activationsRoot() {
return activationsRoot;
}
public Path activationRoot(long activationId) {
if (activationId <= 0L) {
throw new IllegalArgumentException("activationId must be positive");
}
return activationsRoot.resolve(Long.toString(activationId));
}
public Path activationMantleRoot(long activationId) {
return activationRoot(activationId).resolve(LEGACY_MANTLE_DIRECTORY_NAME);
}
public Path ownershipRoot() {
return ownershipRoot;
}
public Path manifest() {
return manifest;
}
public Path legacyPackRoot() {
return legacyPackRoot;
}
public Path legacyMantleRoot() {
return legacyMantleRoot;
}
public Path regionRoot() {
return regionRoot;
}
}
@@ -0,0 +1,383 @@
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.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.Strictness;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Clock;
import java.util.Objects;
import java.util.Optional;
public final class GenerationHistoryStore {
public static final String MANIFEST_FILE_NAME = "manifest.json";
private static final int MAX_MANIFEST_BYTES = 16 * 1_024 * 1_024;
private static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.serializeNulls()
.setPrettyPrinting()
.create();
private final Path generationDirectory;
private final Path manifestPath;
private final Clock clock;
private final DirectorySync directorySync;
private GenerationManifest manifest;
private IOException failure;
private GenerationHistoryStore(
Path generationDirectory,
GenerationManifest manifest,
Clock clock,
DirectorySync directorySync
) {
this.generationDirectory = generationDirectory;
this.manifestPath = generationDirectory.resolve(MANIFEST_FILE_NAME);
this.manifest = Objects.requireNonNull(manifest, "manifest");
this.clock = Objects.requireNonNull(clock, "clock");
this.directorySync = Objects.requireNonNull(directorySync, "directorySync");
}
public static GenerationHistoryStore initialize(
Path generationDirectory,
GenerationEpoch initialEpoch
) throws IOException {
return initialize(generationDirectory, initialEpoch, Clock.systemUTC());
}
static GenerationHistoryStore initialize(
Path generationDirectory,
GenerationEpoch initialEpoch,
Clock clock
) throws IOException {
return initialize(generationDirectory, initialEpoch, clock, GenerationHistoryStore::forceDirectory);
}
static GenerationHistoryStore initialize(
Path generationDirectory,
GenerationEpoch initialEpoch,
Clock clock,
DirectorySync directorySync
) throws IOException {
Path directory = normalizeDirectory(generationDirectory);
requireSafeParent(directory);
Path parent = Objects.requireNonNull(directory.getParent(), "Generation history parent");
String lockName = "." + Objects.requireNonNull(directory.getFileName(), "Generation history directory name")
+ ".manifest.lock";
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(parent, lockName)) {
ensureDirectory(directory);
Path manifestPath = directory.resolve(MANIFEST_FILE_NAME);
if (Files.exists(manifestPath, LinkOption.NOFOLLOW_LINKS)) {
throw new FileAlreadyExistsException(manifestPath.toString());
}
Clock requiredClock = Objects.requireNonNull(clock, "clock");
GenerationManifest manifest = GenerationManifest.initial(
Objects.requireNonNull(initialEpoch, "initialEpoch"),
requiredClock.millis()
);
DirectorySync requiredDirectorySync = Objects.requireNonNull(directorySync, "directorySync");
writeManifestAtomically(directory, manifestPath, manifest, false, requiredDirectorySync);
return new GenerationHistoryStore(directory, manifest, requiredClock, requiredDirectorySync);
}
}
public static GenerationHistoryStore open(Path generationDirectory) throws IOException {
return open(generationDirectory, Clock.systemUTC());
}
static GenerationHistoryStore open(Path generationDirectory, Clock clock) throws IOException {
Path directory = normalizeDirectory(generationDirectory);
requireDirectory(directory);
Path manifestPath = directory.resolve(MANIFEST_FILE_NAME);
requireRegularFile(manifestPath);
GenerationManifest manifest = readManifest(manifestPath);
return new GenerationHistoryStore(
directory,
manifest,
clock,
GenerationHistoryStore::forceDirectory
);
}
public Path generationDirectory() {
return generationDirectory;
}
public synchronized GenerationManifest manifest() {
requireUsable();
return manifest;
}
public synchronized GenerationActivation activeActivation() {
requireUsable();
return manifest.activeActivation();
}
public synchronized GenerationEpoch activeEpoch() {
requireUsable();
return manifest.activeEpoch();
}
public synchronized Optional<GenerationActivation> pendingActivation() {
requireUsable();
return manifest.pendingActivation();
}
public synchronized Optional<GenerationEpoch> pendingEpoch() {
requireUsable();
return manifest.pendingEpoch();
}
public synchronized Optional<GenerationEpoch> epoch(String epochId) {
requireUsable();
return manifest.epoch(epochId);
}
public synchronized Optional<GenerationActivation> activation(long activationId) {
requireUsable();
return manifest.activation(activationId);
}
public synchronized GenerationActivation preparePendingActivation(
GenerationEpoch epoch,
int transitionWidthBlocks
) throws IOException {
requireUsable();
GenerationManifest updated = manifest.preparePending(
epoch,
clock.millis(),
transitionWidthBlocks
);
if (updated == manifest) {
return manifest.pendingActivation().orElseThrow();
}
writeUpdatedManifest(updated);
manifest = updated;
return updated.pendingActivation().orElseThrow();
}
public synchronized GenerationActivation completePendingTransition(
long expectedActivationId,
String boundaryIdentity,
String terrainSignatureIdentity
) throws IOException {
requireUsable();
GenerationManifest updated = manifest.completePendingTransition(
expectedActivationId,
boundaryIdentity,
terrainSignatureIdentity
);
if (updated != manifest) {
writeUpdatedManifest(updated);
manifest = updated;
}
return manifest.pendingActivation().orElseThrow();
}
public synchronized GenerationActivation activatePending(
long expectedActivationId
) throws IOException {
requireUsable();
GenerationManifest updated = manifest.activatePending(expectedActivationId);
writeUpdatedManifest(updated);
manifest = updated;
return updated.activeActivation();
}
private void writeUpdatedManifest(GenerationManifest updated) throws IOException {
try {
writeManifestAtomically(generationDirectory, manifestPath, updated, true, directorySync);
} catch (IOException error) {
reconcileAfterWriteFailure(error);
throw error;
}
}
private static GenerationManifest readManifest(Path manifestPath) throws IOException {
long size = Files.size(manifestPath);
if (size < 2L || size > MAX_MANIFEST_BYTES) {
throw new IOException("Generation manifest size is invalid: " + manifestPath);
}
byte[] encoded = Files.readAllBytes(manifestPath);
if (encoded.length < 2 || encoded.length > MAX_MANIFEST_BYTES) {
throw new IOException("Generation manifest size changed while reading: " + manifestPath);
}
try (Reader input = new StringReader(new String(encoded, StandardCharsets.UTF_8));
JsonReader reader = new JsonReader(input)) {
reader.setStrictness(Strictness.STRICT);
JsonElement parsed = JsonParser.parseReader(reader);
if (parsed == null || !parsed.isJsonObject()) {
throw new IllegalArgumentException("Generation manifest must be a JSON object.");
}
if (reader.peek() != JsonToken.END_DOCUMENT) {
throw new IllegalArgumentException("Generation manifest contains trailing content.");
}
return GenerationManifest.fromJson(parsed.getAsJsonObject());
} catch (JsonParseException | IllegalArgumentException | ArithmeticException exception) {
throw new IOException("Invalid generation manifest: " + manifestPath, exception);
}
}
private static void writeManifestAtomically(
Path generationDirectory,
Path manifestPath,
GenerationManifest manifest,
boolean replaceExisting,
DirectorySync directorySync
) throws IOException {
requireSafeParent(generationDirectory);
requireDirectory(generationDirectory);
byte[] content = GSON.toJson(manifest.toJson()).getBytes(StandardCharsets.UTF_8);
Path temporary = Files.createTempFile(generationDirectory, ".manifest-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(
temporary,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
if (replaceExisting) {
Files.move(
temporary,
manifestPath,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING
);
} else {
Files.move(temporary, manifestPath, StandardCopyOption.ATOMIC_MOVE);
}
} catch (AtomicMoveNotSupportedException exception) {
throw new IOException("Generation history requires atomic manifest publication.", exception);
}
directorySync.force(generationDirectory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static Path normalizeDirectory(Path directory) {
return Objects.requireNonNull(directory, "generationDirectory").toAbsolutePath().normalize();
}
private static void ensureDirectory(Path directory) throws IOException {
requireSafeParent(directory);
if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
requireDirectory(directory);
forceDirectory(Objects.requireNonNull(directory.getParent(), "Generation history parent"));
return;
}
try {
Files.createDirectory(directory);
} catch (FileAlreadyExistsException race) {
requireDirectory(directory);
forceDirectory(Objects.requireNonNull(directory.getParent(), "Generation history parent"));
return;
}
forceDirectory(Objects.requireNonNull(directory.getParent(), "Generation history parent"));
requireDirectory(directory);
}
private static void requireDirectory(Path directory) throws IOException {
requireSafeParent(directory);
if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new NoSuchFileException(directory.toString());
}
BasicFileAttributes attributes = Files.readAttributes(
directory,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException("Generation history path is not a safe directory: " + directory);
}
}
private static void requireSafeParent(Path directory) throws IOException {
Path parent = directory.getParent();
if (parent == null || !Files.exists(parent, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation history parent directory is missing: " + parent);
}
BasicFileAttributes attributes = Files.readAttributes(
parent,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException("Generation history parent path is not a safe directory: " + parent);
}
}
private static void requireRegularFile(Path file) throws IOException {
if (!Files.exists(file, LinkOption.NOFOLLOW_LINKS)) {
throw new NoSuchFileException(file.toString());
}
BasicFileAttributes attributes = Files.readAttributes(
file,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isRegularFile() || attributes.isSymbolicLink()) {
throw new IOException("Generation manifest is not a safe regular file: " + file);
}
}
private static void forceDirectory(Path directory) throws IOException {
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
} catch (UnsupportedOperationException exception) {
throw new IOException("Generation history directory cannot be durability-synced: " + directory, exception);
}
}
private void reconcileAfterWriteFailure(IOException writeFailure) {
try {
requireDirectory(generationDirectory);
requireRegularFile(manifestPath);
manifest = readManifest(manifestPath);
} catch (IOException reconciliationFailure) {
writeFailure.addSuppressed(reconciliationFailure);
failure = writeFailure;
}
}
private void requireUsable() {
if (failure != null) {
throw new IllegalStateException("Generation history store is unusable after a failed manifest write", failure);
}
}
@FunctionalInterface
interface DirectorySync {
void force(Path directory) throws IOException;
}
}
@@ -0,0 +1,91 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
final class GenerationKernelCatalog {
private static final int MAXIMUM_BYTES = 16 * 1024;
private GenerationKernelCatalog() {
}
static GenerationKernelRegistry load() {
try (InputStream input = GenerationKernelCatalog.class.getResourceAsStream(
"/META-INF/iris/generation-kernels/catalog.tsv")) {
if (input == null) {
throw new IOException("Missing Iris generation kernel catalog.");
}
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);
Class<? extends GenerationKernelRegistry.RuntimeFactory> factoryType = Class.forName(
seal.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader())
.asSubclass(GenerationKernelRegistry.RuntimeFactory.class);
Map<GenerationKernelRegistry.AlgorithmVersion, GenerationKernelRegistry.RuntimeFactory> factories = new LinkedHashMap<>();
for (GenerationKernelRegistry.AlgorithmVersion algorithm : seal.algorithms()) {
factories.put(algorithm, factoryType.getDeclaredConstructor().newInstance());
}
kernels.add(new GenerationKernelRegistry.Kernel(abi, seal.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);
}
}
static Catalog parse(byte[] bytes) throws IOException {
if (bytes.length == 0 || bytes.length > MAXIMUM_BYTES) {
throw new IOException("Invalid generation kernel catalog size.");
}
String text = new String(bytes, StandardCharsets.UTF_8);
String[] lines = text.split("\n", -1);
if (!text.endsWith("\n") || text.indexOf('\r') >= 0 || lines.length < 4 || lines.length > 128
|| !lines[0].equals("iris-generation-kernel-catalog-v1")) {
throw new IOException("Invalid generation kernel catalog format.");
}
try {
String[] current = lines[1].split("\t", -1);
if (current.length != 4 || !current[0].equals("current")) {
throw new IllegalArgumentException("Missing current generation version.");
}
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(
positive(current[1]), positive(current[2]), positive(current[3]));
List<Integer> abis = new ArrayList<>();
int previous = 0;
for (int index = 2; index < lines.length - 1; index++) {
String[] kernel = lines[index].split("\t", -1);
if (kernel.length != 2 || !kernel[0].equals("kernel")) {
throw new IllegalArgumentException("Invalid generation kernel registration.");
}
int abi = positive(kernel[1]);
if (abi <= previous) {
throw new IllegalArgumentException("Generation kernel registrations must be unique and sorted.");
}
previous = abi;
abis.add(abi);
}
if (!abis.contains(version.generatorAbi())) {
throw new IllegalArgumentException("Current generation ABI is not registered.");
}
return new Catalog(version, List.copyOf(abis));
} catch (IllegalArgumentException failure) {
throw new IOException("Invalid generation kernel catalog.", failure);
}
}
private static int positive(String value) {
if (!value.matches("[1-9][0-9]{0,8}")) {
throw new IllegalArgumentException("Invalid generation version.");
}
return Integer.parseInt(value);
}
record Catalog(GenerationKernelRegistry.Version current, List<Integer> abis) {
}
}
@@ -0,0 +1,278 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.mantle.EngineMantle;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Pattern;
public final class GenerationKernelRegistry {
private static final Pattern SHA_256_PATTERN = Pattern.compile("[0-9a-f]{64}");
private static final GenerationKernelRegistry STANDARD = GenerationKernelCatalog.load();
private final Version current;
private final Map<Integer, Kernel> kernels;
public GenerationKernelRegistry(Version current, Collection<Kernel> kernels) {
this.current = Objects.requireNonNull(current, "current");
Collection<Kernel> requiredKernels = Objects.requireNonNull(kernels, "kernels");
TreeMap<Integer, Kernel> indexed = new TreeMap<>();
for (Kernel kernel : requiredKernels) {
Kernel requiredKernel = Objects.requireNonNull(kernel, "kernel");
if (indexed.putIfAbsent(requiredKernel.generatorAbi(), requiredKernel) != null) {
throw new IllegalArgumentException("Duplicate generation ABI "
+ requiredKernel.generatorAbi() + ".");
}
}
this.kernels = Map.copyOf(indexed);
if (!supports(current.generatorAbi(), current.rngVersion(), current.seedDerivationVersion())) {
throw new IllegalArgumentException("Current generation version is not registered.");
}
}
public static GenerationKernelRegistry standard() {
return STANDARD;
}
public Version current() {
return current;
}
public boolean supports(int generatorAbi, int rngVersion, int seedDerivationVersion) {
Kernel kernel = kernels.get(generatorAbi);
return kernel != null && kernel.runtimeFactories().containsKey(
new AlgorithmVersion(rngVersion, seedDerivationVersion)
);
}
public Kernel requireSupported(
int generatorAbi,
int rngVersion,
int seedDerivationVersion
) throws IOException {
Kernel kernel = kernels.get(generatorAbi);
if (kernel == null) {
throw new IOException("Unsupported Iris generation ABI " + generatorAbi + ".");
}
AlgorithmVersion version = new AlgorithmVersion(rngVersion, seedDerivationVersion);
if (!kernel.runtimeFactories().containsKey(version)) {
throw new IOException("Unsupported Iris RNG/seed derivation version "
+ rngVersion + "/" + seedDerivationVersion
+ " for generation ABI " + generatorAbi + ".");
}
return kernel;
}
public Kernel requireSupported(Version version) throws IOException {
Version requiredVersion = Objects.requireNonNull(version, "version");
return requireSupported(
requiredVersion.generatorAbi(),
requiredVersion.rngVersion(),
requiredVersion.seedDerivationVersion()
);
}
public Kernel requireSupported(Version version, String implementationFingerprint) throws IOException {
Version requiredVersion = Objects.requireNonNull(version, "version");
Kernel kernel = requireSupported(requiredVersion);
String requiredFingerprint = requireFingerprint(
implementationFingerprint,
"Generation kernel implementation fingerprint"
);
if (!kernel.implementationFingerprint().equals(requiredFingerprint)) {
throw new IOException("Iris generation kernel " + requiredVersion
+ " has implementation fingerprint " + kernel.implementationFingerprint()
+ " instead of the epoch's required fingerprint " + requiredFingerprint + ".");
}
return kernel;
}
public RuntimeKernel select(Version version) throws IOException {
Version requiredVersion = Objects.requireNonNull(version, "version");
Kernel kernel = requireSupported(requiredVersion);
AlgorithmVersion algorithmVersion = new AlgorithmVersion(
requiredVersion.rngVersion(),
requiredVersion.seedDerivationVersion()
);
RuntimeFactory factory = kernel.runtimeFactories().get(algorithmVersion);
if (factory == null) {
throw new IOException("No executable Iris generation kernel is registered for "
+ requiredVersion + ".");
}
return new RuntimeKernel(requiredVersion, kernel.implementationFingerprint(), factory);
}
public record Version(int generatorAbi, int rngVersion, int seedDerivationVersion) {
public Version {
if (generatorAbi < 1) {
throw new IllegalArgumentException("Generator ABI must be positive.");
}
if (rngVersion < 1) {
throw new IllegalArgumentException("RNG version must be positive.");
}
if (seedDerivationVersion < 1) {
throw new IllegalArgumentException("Seed derivation version must be positive.");
}
}
}
private static String requireFingerprint(String value, String label) {
String requiredValue = Objects.requireNonNull(value, label);
if (!SHA_256_PATTERN.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " must be a lowercase SHA-256 value.");
}
return requiredValue;
}
public record Kernel(
int generatorAbi,
String implementationFingerprint,
Map<AlgorithmVersion, RuntimeFactory> runtimeFactories
) {
public Kernel {
if (generatorAbi < 1) {
throw new IllegalArgumentException("Generator ABI must be positive.");
}
implementationFingerprint = requireFingerprint(
implementationFingerprint,
"Generation kernel implementation fingerprint"
);
Map<AlgorithmVersion, RuntimeFactory> requiredFactories = Map.copyOf(Objects.requireNonNull(
runtimeFactories,
"runtimeFactories"
));
if (requiredFactories.isEmpty()) {
throw new IllegalArgumentException("A generation kernel must provide an executable runtime factory.");
}
for (Map.Entry<AlgorithmVersion, RuntimeFactory> entry : requiredFactories.entrySet()) {
Objects.requireNonNull(entry.getKey(), "algorithmVersion");
Objects.requireNonNull(entry.getValue(), "runtimeFactory");
}
runtimeFactories = requiredFactories;
}
}
@FunctionalInterface
public interface RuntimeFactory {
IrisComplex create(IrisEngine engine, TransitionGenerationPlan transitionPlan);
default SeedManager createSeedManager(long worldSeed) {
throw new UnsupportedOperationException("Generation kernel does not own seed derivation.");
}
default EngineMantle createMantle(IrisEngine engine, Path storageDirectory) {
throw new UnsupportedOperationException("Generation kernel does not own mantle construction.");
}
default UpperDimensionContext createUpperContext(IrisEngine engine) {
throw new UnsupportedOperationException("Generation kernel does not own upper-dimension construction.");
}
default DimensionStackContext createDimensionStackContext(IrisEngine engine) {
throw new UnsupportedOperationException("Generation kernel does not own dimension-stack construction.");
}
default EngineMode createMode(IrisEngine engine) {
throw new UnsupportedOperationException("Generation kernel does not own generation-mode construction.");
}
default void registerStaticObjects(IrisEngine engine, EngineMode mode) {
throw new UnsupportedOperationException("Generation kernel does not own static-object stages.");
}
default void registerDimensionStack(IrisEngine engine, EngineMode mode, DimensionStackContext context) {
throw new UnsupportedOperationException("Generation kernel does not own dimension-stack stages.");
}
}
public record RuntimeKernel(
Version version,
String implementationFingerprint,
RuntimeFactory factory
) {
public RuntimeKernel {
Objects.requireNonNull(version, "version");
implementationFingerprint = requireFingerprint(
implementationFingerprint,
"Generation kernel implementation fingerprint"
);
Objects.requireNonNull(factory, "factory");
}
public IrisComplex createComplex(IrisEngine engine, TransitionGenerationPlan transitionPlan) {
return Objects.requireNonNull(
factory.create(Objects.requireNonNull(engine, "engine"), transitionPlan),
"generation kernel complex"
);
}
public SeedManager createSeedManager(long worldSeed) {
return Objects.requireNonNull(factory.createSeedManager(worldSeed), "generation kernel seed manager");
}
public EngineMantle createMantle(IrisEngine engine, Path storageDirectory) {
return Objects.requireNonNull(
factory.createMantle(
Objects.requireNonNull(engine, "engine"),
Objects.requireNonNull(storageDirectory, "storageDirectory")
),
"generation kernel mantle"
);
}
public UpperDimensionContext createUpperContext(IrisEngine engine) {
return factory.createUpperContext(Objects.requireNonNull(engine, "engine"));
}
public DimensionStackContext createDimensionStackContext(IrisEngine engine) {
return factory.createDimensionStackContext(Objects.requireNonNull(engine, "engine"));
}
public EngineMode createMode(IrisEngine engine) {
return Objects.requireNonNull(
factory.createMode(Objects.requireNonNull(engine, "engine")),
"generation kernel mode"
);
}
public void registerStaticObjects(IrisEngine engine, EngineMode mode) {
factory.registerStaticObjects(
Objects.requireNonNull(engine, "engine"),
Objects.requireNonNull(mode, "mode")
);
}
public void registerDimensionStack(
IrisEngine engine,
EngineMode mode,
DimensionStackContext context
) {
factory.registerDimensionStack(
Objects.requireNonNull(engine, "engine"),
Objects.requireNonNull(mode, "mode"),
context
);
}
}
public record AlgorithmVersion(int rngVersion, int seedDerivationVersion) {
public AlgorithmVersion {
if (rngVersion < 1) {
throw new IllegalArgumentException("RNG version must be positive.");
}
if (seedDerivationVersion < 1) {
throw new IllegalArgumentException("Seed derivation version must be positive.");
}
}
}
}
@@ -0,0 +1,133 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
final class GenerationKernelSourceSeal {
private static final int MAXIMUM_BYTES = 8 * 1024 * 1024;
private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}");
private GenerationKernelSourceSeal() {
}
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() + ".");
}
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)) {
if (input == null) {
throw new IOException("Missing generation kernel source seal " + 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.");
}
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_$]*)+")) {
throw new IOException("Invalid generation kernel factory identity.");
}
String fingerprint = fingerprint(abi, factoryClass, bytes);
List<GenerationKernelRegistry.AlgorithmVersion> algorithms = new ArrayList<>();
for (String line : lines) {
if (line.startsWith("algorithm\t")) {
String[] fields = line.split("\t", -1);
algorithms.add(new GenerationKernelRegistry.AlgorithmVersion(
Integer.parseInt(fields[1]), Integer.parseInt(fields[2])));
}
}
return new Descriptor(abi, factoryClass, List.copyOf(algorithms), fingerprint);
} catch (IOException failure) {
throw new IllegalStateException("Cannot load sealed Iris generation ABI " + abi + ".", failure);
}
}
static String fingerprint(int abi, String factoryClass, byte[] bytes) throws IOException {
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.");
}
String source = new String(bytes, StandardCharsets.UTF_8);
String prefix = "iris-generation-kernel-seal-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 + ".");
}
requireCanonicalEntries(source.substring(prefix.length()));
try {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException(failure);
}
}
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.");
}
int previousType = -1;
String previousKey = "";
boolean hasScope = false;
boolean hasAlgorithm = false;
int previousRng = 0;
int previousSeed = 0;
for (int index = 0; index < entries.length - 1; index++) {
String[] fields = entries[index].split("\t", -1);
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.");
}
int rng = Integer.parseInt(fields[1]);
int seed = Integer.parseInt(fields[2]);
if (rng < previousRng || (rng == previousRng && seed <= previousSeed)) {
throw new IOException("Generation kernel algorithm versions are not canonical.");
}
previousRng = rng;
previousSeed = seed;
hasAlgorithm = true;
continue;
}
int type = switch (fields[0]) {
case "scope" -> 0;
case "exclude" -> 1;
case "source" -> 2;
case "dependency" -> 3;
default -> -1;
};
int expectedFields = type < 2 ? 2 : 3;
if (type < previousType || type < 0 || fields.length != expectedFields || fields[1].isBlank()
|| 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.");
}
previousType = type;
previousKey = fields[1];
hasScope |= type == 0;
}
if (!hasScope || !hasAlgorithm) {
throw new IOException("Generation kernel seal has no source scope or algorithm version.");
}
}
record Descriptor(int abi, String factoryClass, List<GenerationKernelRegistry.AlgorithmVersion> algorithms,
String fingerprint) {
}
}
@@ -0,0 +1,134 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.modifier.IrisCustomModifier;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionMode;
import art.arcane.iris.engine.object.IrisDimensionModeType;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import java.nio.file.Path;
final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactory {
static final String IMPLEMENTATION_FINGERPRINT =
GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class);
@Override
public SeedManager createSeedManager(long worldSeed) {
return new SeedManager(worldSeed);
}
@Override
public EngineMantle createMantle(IrisEngine engine, Path storageDirectory) {
return new IrisEngineMantle(engine, storageDirectory);
}
@Override
public IrisComplex create(IrisEngine engine, TransitionGenerationPlan transitionPlan) {
return new IrisComplex(engine, transitionPlan);
}
@Override
public UpperDimensionContext createUpperContext(IrisEngine engine) {
IrisDimension dimension = engine.getDimension();
if (!dimension.hasUpperDimension()) {
return null;
}
String upperKey = dimension.getUpperDimension();
IrisDimension upperDimension = upperKey.equals(dimension.getLoadKey())
? dimension
: engine.getData().getDimensionLoader().load(upperKey, false);
if (upperDimension == null) {
throw new IllegalStateException("Upper dimension '" + upperKey
+ "' is absent from the immutable Iris epoch pack.");
}
UpperDimensionContext context = UpperDimensionContext.create(engine, upperDimension);
IrisLogging.info("Upper dimension enabled: " + upperKey
+ (context.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
return context;
}
@Override
public DimensionStackContext createDimensionStackContext(IrisEngine engine) {
IrisDimension dimension = engine.getDimension();
if (!dimension.hasDimensionStack()) {
return null;
}
if (dimension.hasUpperDimension()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
+ "' cannot enable both dimensionStack and upperDimension.");
}
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
IrisLogging.info("Dimension stack enabled: "
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
return context;
}
@Override
public EngineMode createMode(IrisEngine engine) {
Throwable configuredFailure = null;
try {
IrisDimensionMode configuredMode = engine.getDimension().getMode();
if (configuredMode == null) {
configuredMode = new IrisDimensionMode();
engine.getDimension().setMode(configuredMode);
}
EngineMode configured = configuredMode.create(engine);
if (configured == null) {
throw new IllegalStateException("Dimension mode factory returned null");
}
return configured;
} catch (Throwable failure) {
configuredFailure = failure;
IrisLogging.reportError(failure);
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
IrisLogging.warn("Failed to initialize configured dimension mode for "
+ engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
}
}
try {
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine);
if (fallback == null) {
throw new IllegalStateException("OVERWORLD mode factory returned null");
}
return fallback;
} catch (Throwable fallbackFailure) {
fallbackFailure.addSuppressed(configuredFailure);
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure);
}
}
@Override
public void registerStaticObjects(IrisEngine engine, EngineMode mode) {
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks));
}
@Override
public void registerDimensionStack(
IrisEngine engine,
EngineMode mode,
DimensionStackContext context
) {
if (context == null) {
return;
}
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
mode.registerStage((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) ->
stackCustom.modify(x, z, blocks, multicore, chunkContext));
}
}
@@ -0,0 +1,560 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
public final class GenerationManifest {
public static final int CURRENT_SCHEMA_VERSION = 1;
private final int schemaVersion;
private final NavigableMap<String, GenerationEpoch> epochs;
private final NavigableMap<Long, GenerationActivation> activations;
private final long activeActivationId;
private final Long pendingActivationId;
private GenerationManifest(State state) {
State requiredState = Objects.requireNonNull(state, "state");
this.schemaVersion = requiredState.schemaVersion();
this.epochs = immutableEpochs(requiredState.epochs());
this.activations = immutableActivations(requiredState.activations());
this.activeActivationId = requiredState.activeActivationId();
this.pendingActivationId = requiredState.pendingActivationId();
validate();
}
public static GenerationManifest initial(GenerationEpoch epoch, long createdAtEpochMillis) {
GenerationEpoch requiredEpoch = Objects.requireNonNull(epoch, "epoch");
GenerationActivation activation = GenerationActivation.initial(
requiredEpoch.epochId(),
createdAtEpochMillis
);
return new GenerationManifest(new State(
CURRENT_SCHEMA_VERSION,
Map.of(requiredEpoch.epochId(), requiredEpoch),
Map.of(activation.activationId(), activation),
activation.activationId(),
null
));
}
static GenerationManifest restore(State state) {
return new GenerationManifest(state);
}
public int schemaVersion() {
return schemaVersion;
}
public Collection<GenerationEpoch> epochs() {
return epochs.values();
}
public Collection<GenerationActivation> activations() {
return activations.values();
}
public Optional<GenerationEpoch> epoch(String epochId) {
return Optional.ofNullable(epochs.get(epochId));
}
public Optional<GenerationActivation> activation(long activationId) {
return Optional.ofNullable(activations.get(activationId));
}
public GenerationActivation activeActivation() {
return activations.get(activeActivationId);
}
public GenerationEpoch activeEpoch() {
return epochs.get(activeActivation().epochId());
}
public Optional<GenerationActivation> pendingActivation() {
if (pendingActivationId == null) {
return Optional.empty();
}
return Optional.of(activations.get(pendingActivationId));
}
public Optional<GenerationEpoch> pendingEpoch() {
return pendingActivation().map(activation -> epochs.get(activation.epochId()));
}
GenerationManifest preparePending(
GenerationEpoch epoch,
long createdAtEpochMillis,
int transitionWidthBlocks
) {
GenerationEpoch requiredEpoch = Objects.requireNonNull(epoch, "epoch");
Optional<GenerationActivation> pending = pendingActivation();
if (pending.isPresent()) {
if (pending.get().epochId().equals(requiredEpoch.epochId())) {
GenerationEpoch storedEpoch = epochs.get(requiredEpoch.epochId());
if (!requiredEpoch.equals(storedEpoch)) {
throw new IllegalStateException("Pending epoch identity is inconsistent.");
}
return this;
}
throw new IllegalStateException("A different generation activation is already pending.");
}
requireCompatibleDimension(requiredEpoch);
TreeMap<String, GenerationEpoch> nextEpochs = new TreeMap<>(epochs);
GenerationEpoch storedEpoch = nextEpochs.putIfAbsent(requiredEpoch.epochId(), requiredEpoch);
if (storedEpoch != null && !storedEpoch.equals(requiredEpoch)) {
throw new IllegalStateException("Epoch identity collision detected.");
}
long activationId = Math.addExact(activations.lastKey(), 1L);
GenerationActivation activation = GenerationActivation.next(
activationId,
requiredEpoch.epochId(),
activeActivationId,
createdAtEpochMillis,
transitionWidthBlocks
);
TreeMap<Long, GenerationActivation> nextActivations = new TreeMap<>(activations);
nextActivations.put(activationId, activation);
return new GenerationManifest(new State(
schemaVersion,
nextEpochs,
nextActivations,
activeActivationId,
activationId
));
}
GenerationManifest activatePending(long expectedActivationId) {
GenerationActivation pending = pendingActivation()
.orElseThrow(() -> new IllegalStateException("No generation activation is pending."));
if (pending.activationId() != expectedActivationId) {
throw new IllegalStateException("Pending activation changed before publication.");
}
return new GenerationManifest(new State(
schemaVersion,
epochs,
activations,
pending.activationId(),
null
));
}
GenerationManifest completePendingTransition(
long expectedActivationId,
String boundaryIdentity,
String terrainSignatureIdentity
) {
GenerationActivation pending = pendingActivation()
.orElseThrow(() -> new IllegalStateException("No generation activation is pending."));
if (pending.activationId() != expectedActivationId) {
throw new IllegalStateException("Pending activation changed before transition publication.");
}
GenerationActivation completed = pending.completeTransition(
boundaryIdentity,
terrainSignatureIdentity
);
if (completed == pending) {
return this;
}
TreeMap<Long, GenerationActivation> nextActivations = new TreeMap<>(activations);
nextActivations.put(completed.activationId(), completed);
return new GenerationManifest(new State(
schemaVersion,
epochs,
nextActivations,
activeActivationId,
pendingActivationId
));
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("schemaVersion", schemaVersion);
JsonArray epochArray = new JsonArray(epochs.size());
for (GenerationEpoch epoch : epochs.values()) {
epochArray.add(epoch.toJson());
}
json.add("epochs", epochArray);
JsonArray activationArray = new JsonArray(activations.size());
for (GenerationActivation activation : activations.values()) {
activationArray.add(activation.toJson());
}
json.add("activations", activationArray);
json.addProperty("activeActivationId", activeActivationId);
if (pendingActivationId == null) {
json.add("pendingActivationId", JsonNull.INSTANCE);
} else {
json.addProperty("pendingActivationId", pendingActivationId);
}
return json;
}
static GenerationManifest fromJson(JsonObject json) {
JsonSchema.requireFields(
json,
"generation manifest",
"schemaVersion",
"epochs",
"activations",
"activeActivationId",
"pendingActivationId"
);
int schemaVersion = JsonSchema.requireInt(json, "schemaVersion", "generation manifest");
JsonArray epochArray = JsonSchema.requireArray(json, "epochs", "generation manifest");
JsonArray activationArray = JsonSchema.requireArray(json, "activations", "generation manifest");
long activeActivationId = JsonSchema.requireLong(
json,
"activeActivationId",
"generation manifest"
);
Long pendingActivationId = JsonSchema.requireNullableLong(
json,
"pendingActivationId",
"generation manifest"
);
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);
if (epochs.putIfAbsent(epoch.epochId(), epoch) != null) {
throw new IllegalArgumentException("Generation manifest contains duplicate epoch IDs.");
}
}
TreeMap<Long, GenerationActivation> activations = new TreeMap<>();
for (int index = 0; index < activationArray.size(); index++) {
JsonObject activationJson = JsonSchema.requireObject(
activationArray.get(index),
"activations[" + index + "]"
);
GenerationActivation activation = GenerationActivation.fromJson(activationJson);
if (activations.putIfAbsent(activation.activationId(), activation) != null) {
throw new IllegalArgumentException("Generation manifest contains duplicate activation IDs.");
}
}
return restore(new State(
schemaVersion,
epochs,
activations,
activeActivationId,
pendingActivationId
));
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GenerationManifest manifest)) {
return false;
}
return schemaVersion == manifest.schemaVersion
&& activeActivationId == manifest.activeActivationId
&& epochs.equals(manifest.epochs)
&& activations.equals(manifest.activations)
&& Objects.equals(pendingActivationId, manifest.pendingActivationId);
}
@Override
public int hashCode() {
return Objects.hash(schemaVersion, epochs, activations, activeActivationId, pendingActivationId);
}
private static NavigableMap<String, GenerationEpoch> immutableEpochs(
Map<String, GenerationEpoch> source
) {
Objects.requireNonNull(source, "epochs");
TreeMap<String, GenerationEpoch> copy = new TreeMap<>();
for (Map.Entry<String, GenerationEpoch> entry : source.entrySet()) {
String epochId = Objects.requireNonNull(entry.getKey(), "epoch ID");
GenerationEpoch epoch = Objects.requireNonNull(entry.getValue(), "epoch");
if (!epochId.equals(epoch.epochId())) {
throw new IllegalArgumentException("Epoch map key does not match the epoch ID.");
}
copy.put(epochId, epoch);
}
return Collections.unmodifiableNavigableMap(copy);
}
private static NavigableMap<Long, GenerationActivation> immutableActivations(
Map<Long, GenerationActivation> source
) {
Objects.requireNonNull(source, "activations");
TreeMap<Long, GenerationActivation> copy = new TreeMap<>();
for (Map.Entry<Long, GenerationActivation> entry : source.entrySet()) {
Long activationId = Objects.requireNonNull(entry.getKey(), "activation ID");
GenerationActivation activation = Objects.requireNonNull(entry.getValue(), "activation");
if (activationId.longValue() != activation.activationId()) {
throw new IllegalArgumentException("Activation map key does not match the activation ID.");
}
copy.put(activationId, activation);
}
return Collections.unmodifiableNavigableMap(copy);
}
private void validate() {
if (schemaVersion != CURRENT_SCHEMA_VERSION) {
throw new IllegalArgumentException("Unsupported generation manifest schema: " + schemaVersion);
}
if (epochs.isEmpty()) {
throw new IllegalArgumentException("Generation manifest must contain an epoch.");
}
if (activations.isEmpty()) {
throw new IllegalArgumentException("Generation manifest must contain an activation.");
}
GenerationEpoch.DimensionContract contract = epochs.firstEntry().getValue().dimensionContract();
long worldSeed = epochs.firstEntry().getValue().worldSeed();
for (GenerationEpoch epoch : epochs.values()) {
if (!contract.hasSameLayout(epoch.dimensionContract())) {
throw new IllegalArgumentException("Generation epochs use incompatible dimension contracts.");
}
if (worldSeed != epoch.worldSeed()) {
throw new IllegalArgumentException("Generation epochs use different world seeds.");
}
}
long expectedActivationId = 1L;
Set<String> referencedEpochs = new LinkedHashSet<>();
for (GenerationActivation activation : activations.values()) {
if (activation.activationId() != expectedActivationId) {
throw new IllegalArgumentException("Generation activation IDs must be contiguous and monotonic.");
}
Long expectedParent = expectedActivationId == 1L ? null : expectedActivationId - 1L;
if (!Objects.equals(activation.parentActivationId(), expectedParent)) {
throw new IllegalArgumentException("Generation activation history is not a linear chain.");
}
if (!epochs.containsKey(activation.epochId())) {
throw new IllegalArgumentException("Generation activation references a missing epoch.");
}
referencedEpochs.add(activation.epochId());
if (activation.parentActivationId() == null) {
if (activation.transition() != null) {
throw new IllegalArgumentException("Initial activation must not define a transition.");
}
} else if (activation.transition() == null) {
throw new IllegalArgumentException("Generation activation is missing its transition policy.");
}
expectedActivationId = Math.addExact(expectedActivationId, 1L);
}
if (!referencedEpochs.equals(epochs.keySet())) {
throw new IllegalArgumentException("Generation manifest contains an unreferenced epoch.");
}
if (!activations.containsKey(activeActivationId)) {
throw new IllegalArgumentException("Active generation activation is missing.");
}
long latestActivationId = activations.lastKey();
if (pendingActivationId == null) {
if (activeActivationId != latestActivationId) {
throw new IllegalArgumentException("Active generation activation is not the latest activation.");
}
requireCompletedTransitions();
return;
}
if (!activations.containsKey(pendingActivationId)) {
throw new IllegalArgumentException("Pending generation activation is missing.");
}
if (pendingActivationId != latestActivationId) {
throw new IllegalArgumentException("Pending generation activation is not the latest activation.");
}
GenerationActivation pending = activations.get(pendingActivationId);
if (!Objects.equals(pending.parentActivationId(), activeActivationId)) {
throw new IllegalArgumentException("Pending generation activation does not follow the active activation.");
}
for (GenerationActivation activation : activations.values()) {
if (activation.activationId() != pendingActivationId
&& activation.transition() != null
&& !activation.transition().isComplete()) {
throw new IllegalArgumentException("Published generation transition is incomplete.");
}
}
}
private void requireCompletedTransitions() {
for (GenerationActivation activation : activations.values()) {
if (activation.transition() != null && !activation.transition().isComplete()) {
throw new IllegalArgumentException("Published generation transition is incomplete.");
}
}
}
private void requireCompatibleDimension(GenerationEpoch epoch) {
if (!activeEpoch().dimensionContract().hasSameLayout(epoch.dimensionContract())) {
throw new IllegalArgumentException("Generation epoch changes the immutable dimension contract.");
}
if (activeEpoch().worldSeed() != epoch.worldSeed()) {
throw new IllegalArgumentException("Generation epoch changes the immutable world seed.");
}
}
record State(
int schemaVersion,
Map<String, GenerationEpoch> epochs,
Map<Long, GenerationActivation> activations,
long activeActivationId,
Long pendingActivationId
) {
}
static final class JsonSchema {
private JsonSchema() {
}
static void requireFields(JsonObject json, String context, String... expectedNames) {
Objects.requireNonNull(json, context);
Set<String> expected = Set.of(expectedNames);
Set<String> actual = json.keySet();
if (!actual.equals(expected)) {
ArrayList<String> missing = new ArrayList<>(expected);
missing.removeAll(actual);
ArrayList<String> unknown = new ArrayList<>(actual);
unknown.removeAll(expected);
Collections.sort(missing);
Collections.sort(unknown);
throw new IllegalArgumentException(
"Invalid " + context + " fields. Missing=" + missing + ", unknown=" + unknown + "."
);
}
}
static JsonArray requireArray(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null || !value.isJsonArray()) {
throw new IllegalArgumentException(context + "." + name + " must be an array.");
}
return value.getAsJsonArray();
}
static JsonObject requireObject(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null || !value.isJsonObject()) {
throw new IllegalArgumentException(context + "." + name + " must be an object.");
}
return value.getAsJsonObject();
}
static JsonObject requireNullableObject(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null) {
throw new IllegalArgumentException(context + "." + name + " is required.");
}
if (value.isJsonNull()) {
return null;
}
if (!value.isJsonObject()) {
throw new IllegalArgumentException(context + "." + name + " must be an object or null.");
}
return value.getAsJsonObject();
}
static JsonObject requireObject(JsonElement value, String context) {
if (value == null || !value.isJsonObject()) {
throw new IllegalArgumentException(context + " must be an object.");
}
return value.getAsJsonObject();
}
static String requireString(JsonObject json, String name, String context) {
JsonPrimitive value = requirePrimitive(json, name, context);
if (!value.isString()) {
throw new IllegalArgumentException(context + "." + name + " must be a string.");
}
return value.getAsString();
}
static int requireInt(JsonObject json, String name, String context) {
try {
return requireNumber(json, name, context).intValueExact();
} catch (ArithmeticException exception) {
throw new IllegalArgumentException(context + "." + name + " must be an exact integer.", exception);
}
}
static long requireLong(JsonObject json, String name, String context) {
try {
return requireNumber(json, name, context).longValueExact();
} catch (ArithmeticException exception) {
throw new IllegalArgumentException(context + "." + name + " must be an exact long.", exception);
}
}
static Long requireNullableLong(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null) {
throw new IllegalArgumentException(context + "." + name + " is required.");
}
if (value.isJsonNull()) {
return null;
}
return requireLong(json, name, context);
}
static String requireNullableString(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null) {
throw new IllegalArgumentException(context + "." + name + " is required.");
}
if (value.isJsonNull()) {
return null;
}
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
throw new IllegalArgumentException(context + "." + name + " must be a string or null.");
}
return value.getAsString();
}
static double requireDouble(JsonObject json, String name, String context) {
return requireNumber(json, name, context).doubleValue();
}
static boolean requireBoolean(JsonObject json, String name, String context) {
JsonPrimitive value = requirePrimitive(json, name, context);
if (!value.isBoolean()) {
throw new IllegalArgumentException(context + "." + name + " must be a boolean.");
}
return value.getAsBoolean();
}
private static BigDecimal requireNumber(JsonObject json, String name, String context) {
JsonPrimitive value = requirePrimitive(json, name, context);
if (!value.isNumber()) {
throw new IllegalArgumentException(context + "." + name + " must be a number.");
}
try {
return new BigDecimal(value.getAsString());
} catch (NumberFormatException exception) {
throw new IllegalArgumentException(context + "." + name + " is not a valid number.", exception);
}
}
private static JsonPrimitive requirePrimitive(JsonObject json, String name, String context) {
JsonElement value = json.get(name);
if (value == null || value.isJsonNull() || !value.isJsonPrimitive()) {
throw new IllegalArgumentException(context + "." + name + " must be a primitive value.");
}
return value.getAsJsonPrimitive();
}
}
}
@@ -0,0 +1,142 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.Objects;
public final class GenerationPackFingerprint {
public static final int CURRENT_VERSION = 1;
private static final int BUFFER_BYTES = 64 * 1_024;
private GenerationPackFingerprint() {
}
public static String compute(Path packRoot, int version) throws IOException {
requireSupported(version);
return computeVersionOne(packRoot);
}
public static void requireSupported(int version) throws IOException {
if (version != 1) {
throw new IOException("Unsupported Iris generation pack fingerprint version " + version + ".");
}
}
private static String computeVersionOne(Path packRoot) throws IOException {
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(root)) {
throw new IOException("Generation pack fingerprint root is missing or unsafe: " + root);
}
Path realRoot = root.toRealPath(LinkOption.NOFOLLOW_LINKS);
List<FingerprintEntry> entries = new ArrayList<>();
collectTree(realRoot, entries);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
MessageDigest digest = sha256();
byte[] buffer = new byte[BUFFER_BYTES];
for (FingerprintEntry entry : entries) {
updateEntry(digest, entry.relativePath(), entry.size());
long readBytes = 0L;
try (InputStream input = Files.newInputStream(
entry.source(),
StandardOpenOption.READ,
LinkOption.NOFOLLOW_LINKS
)) {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
digest.update(buffer, 0, read);
readBytes += read;
}
}
}
if (readBytes != entry.size()) {
throw new IOException("Iris generation pack changed while fingerprinting: "
+ entry.source());
}
}
return HexFormat.of().formatHex(digest.digest());
}
private static void collectTree(Path root, List<FingerprintEntry> entries) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
Path directory,
BasicFileAttributes attributes
) throws IOException {
if (attributes.isSymbolicLink() || Files.isSymbolicLink(directory)) {
throw new IOException("Generation pack fingerprint rejected symbolic link: " + directory);
}
if (!directory.equals(root)
&& root.relativize(directory).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
Path relativePath = root.relativize(file);
String fileName = file.getFileName().toString();
if ((relativePath.getNameCount() == 1 && PackDirectoryResolver.isHiddenName(fileName))
|| fileName.endsWith(".code-workspace")) {
return FileVisitResult.CONTINUE;
}
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
throw new IOException("Generation pack fingerprint rejected symbolic link: " + file);
}
if (!attributes.isRegularFile()) {
throw new IOException("Generation pack fingerprint rejected unsupported entry: " + file);
}
entries.add(new FingerprintEntry(
file,
relativePath.toString().replace(File.separatorChar, '/'),
attributes.size()
));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException {
throw new IOException("Unable to inspect Iris generation pack entry: " + file, failure);
}
});
}
private static void updateEntry(MessageDigest digest, String relativePath, long size) {
byte[] relativeBytes = relativePath.getBytes(StandardCharsets.UTF_8);
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(relativeBytes.length).array());
digest.update(relativeBytes);
digest.update(ByteBuffer.allocate(Long.BYTES).putLong(size).array());
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable.", exception);
}
}
private record FingerprintEntry(Path source, String relativePath, long size) {
}
}
@@ -0,0 +1,275 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.regex.Pattern;
public final class GenerationPackRepository {
private static final Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}");
private final Path dimensionRoot;
private final Path generationRoot;
private final Path epochsRoot;
public GenerationPackRepository(Path dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toAbsolutePath()
.normalize();
this.dimensionRoot = root;
generationRoot = root.resolve("iris").resolve("generation");
epochsRoot = generationRoot.resolve("epochs");
}
public Path generationRoot() {
return generationRoot;
}
public Path epochRoot(String epochId) {
return epochsRoot.resolve(requireDigest(epochId, "epochId"));
}
public Path packRoot(String epochId) {
return epochRoot(epochId).resolve("pack");
}
public synchronized Path publish(
String epochId,
String packFingerprint,
int packFingerprintVersion,
Path source
) throws IOException {
String requiredEpochId = requireDigest(epochId, "epochId");
String requiredFingerprint = requireDigest(packFingerprint, "packFingerprint");
ensureEpochsRoot();
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(
epochsRoot,
".epoch-" + requiredEpochId + ".lock"
)) {
ensureEpochsRoot();
return publishLocked(requiredEpochId, requiredFingerprint, packFingerprintVersion, source);
}
}
private Path publishLocked(
String epochId,
String packFingerprint,
int packFingerprintVersion,
Path source
) throws IOException {
Path target = epochsRoot.resolve(epochId);
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;
try {
Path stagedPack = stage.resolve("pack");
copyPackTree(source, stagedPack);
requireFingerprint(stagedPack, 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;
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
} finally {
if (!published && Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
AtomicDirectoryPublisher.deleteTree(stage);
}
}
}
public Path requireExactPack(
String epochId,
String packFingerprint,
int packFingerprintVersion
) throws IOException {
String requiredEpochId = requireDigest(epochId, "epochId");
validateExistingAncestors(requiredEpochId);
Path epoch = epochRoot(requiredEpochId);
requireSafeDirectory(epoch, "Generation epoch path is not a safe directory");
Path pack = epoch.resolve("pack");
requireSafeDirectory(pack, "Generation epoch pack is missing or unsafe");
requireFingerprint(
pack,
requireDigest(packFingerprint, "packFingerprint"),
packFingerprintVersion
);
return pack;
}
private void ensureEpochsRoot() throws IOException {
requireSafeDirectory(dimensionRoot, "Dimension root is not a safe directory");
Path irisRoot = ensureChildDirectory(dimensionRoot, "iris");
Path ensuredGenerationRoot = ensureChildDirectory(irisRoot, "generation");
Path ensuredEpochsRoot = ensureChildDirectory(ensuredGenerationRoot, "epochs");
if (!ensuredEpochsRoot.equals(epochsRoot)) {
throw new IOException("Generation epoch repository resolved outside its storage path: " + ensuredEpochsRoot);
}
}
private void validateExistingAncestors(String epochId) throws IOException {
requireSafeDirectory(dimensionRoot, "Dimension root is not a safe directory");
Path irisRoot = dimensionRoot.resolve("iris");
requireSafeDirectory(irisRoot, "Generation epoch ancestor is not a safe directory");
requireSafeDirectory(generationRoot, "Generation epoch ancestor is not a safe directory");
requireSafeDirectory(epochsRoot, "Generation epoch ancestor is not a safe directory");
Path epoch = epochsRoot.resolve(epochId);
requireSafeDirectory(epoch, "Generation epoch path is not a safe directory");
}
private static Path ensureChildDirectory(Path parent, String name) throws IOException {
Path child = parent.resolve(name);
if (Files.exists(child, LinkOption.NOFOLLOW_LINKS)) {
requireSafeDirectory(child, "Generation epoch ancestor is not a safe directory");
forceDirectory(parent);
return child;
}
try {
Files.createDirectory(child);
} catch (FileAlreadyExistsException race) {
requireSafeDirectory(child, "Generation epoch ancestor is not a safe directory");
forceDirectory(parent);
return child;
}
forceDirectory(parent);
requireSafeDirectory(child, "Generation epoch ancestor is not a safe directory");
return child;
}
private static void requireSafeDirectory(Path directory, String message) throws IOException {
if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException(message + ": " + directory);
}
BasicFileAttributes attributes = Files.readAttributes(
directory,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException(message + ": " + directory);
}
}
private static void forceDirectory(Path directory) throws IOException {
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
} catch (UnsupportedOperationException exception) {
throw new IOException("Generation epoch directory cannot be durability-synced: " + directory, exception);
}
}
private 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);
}
Path normalizedSource = sourcePath.toRealPath();
if (!Files.isDirectory(normalizedSource, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Pack source is not a directory: " + normalizedSource);
}
Path normalizedTarget = Objects.requireNonNull(target, "target").toAbsolutePath().normalize();
if (Files.exists(normalizedTarget, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(normalizedTarget)) {
throw new IOException("Pack publication stage already exists: " + normalizedTarget);
}
if (normalizedTarget.startsWith(normalizedSource) || normalizedSource.startsWith(normalizedTarget)) {
throw new IOException("Pack source and publication stage overlap: "
+ normalizedSource + " and " + normalizedTarget);
}
Files.walkFileTree(normalizedSource, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
if (attributes.isSymbolicLink() || Files.isSymbolicLink(directory)) {
throw new IOException("Pack contains a symbolic link: " + directory);
}
if (!directory.equals(normalizedSource)
&& normalizedSource.relativize(directory).getNameCount() == 1
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
Files.createDirectories(copyDestination(normalizedSource, normalizedTarget, directory));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
throw new IOException("Pack contains a symbolic link: " + file);
}
if (!attributes.isRegularFile()) {
throw new IOException("Pack contains an unsupported entry: " + file);
}
Path relative = normalizedSource.relativize(file);
String fileName = file.getFileName().toString();
if ((relative.getNameCount() == 1 && PackDirectoryResolver.isHiddenName(fileName))
|| fileName.endsWith(".code-workspace")) {
return FileVisitResult.CONTINUE;
}
Path destination = copyDestination(normalizedSource, normalizedTarget, file);
Files.createDirectories(Objects.requireNonNull(destination.getParent(), "Pack entry parent"));
Files.copy(file, destination, StandardCopyOption.COPY_ATTRIBUTES);
try (FileChannel channel = FileChannel.open(destination, StandardOpenOption.WRITE)) {
channel.force(true);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException failure) throws IOException {
throw new IOException("Unable to copy pack entry: " + file, failure);
}
});
}
private static Path copyDestination(Path source, Path target, Path entry) throws IOException {
Path destination = target.resolve(source.relativize(entry)).normalize();
if (!destination.startsWith(target)) {
throw new IOException("Pack entry escapes its publication stage: " + entry);
}
return destination;
}
private static void requireFingerprint(
Path pack,
String expected,
int packFingerprintVersion
) throws IOException {
String actual = GenerationPackFingerprint.compute(pack, packFingerprintVersion);
if (!expected.equals(actual)) {
throw new IOException("Generation epoch pack fingerprint mismatch at " + pack
+ ": expected " + expected + " but found " + actual);
}
}
private static String requireDigest(String value, String field) {
String requiredValue = Objects.requireNonNull(value, field);
if (!SHA_256.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(field + " must be a lowercase SHA-256 digest");
}
return requiredValue;
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
final class GenerationPublicationLock implements AutoCloseable {
private static final ConcurrentHashMap<Path, ReentrantLock> PROCESS_LOCKS = new ConcurrentHashMap<>();
private final ReentrantLock processLock;
private final FileChannel channel;
private final FileLock fileLock;
private GenerationPublicationLock(ReentrantLock processLock, FileChannel channel, FileLock fileLock) {
this.processLock = processLock;
this.channel = channel;
this.fileLock = fileLock;
}
static GenerationPublicationLock acquire(Path directory, String fileName) throws IOException {
Path requiredDirectory = Objects.requireNonNull(directory, "directory").toAbsolutePath().normalize();
Path lockPath = requiredDirectory.resolve(Objects.requireNonNull(fileName, "fileName")).normalize();
if (!Objects.equals(lockPath.getParent(), requiredDirectory)) {
throw new IOException("Generation publication lock escapes its storage directory: " + lockPath);
}
requireSafeDirectory(requiredDirectory);
ReentrantLock processLock = PROCESS_LOCKS.computeIfAbsent(lockPath, ignored -> new ReentrantLock());
processLock.lock();
FileChannel channel = null;
FileLock fileLock = null;
try {
channel = FileChannel.open(
lockPath,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
LinkOption.NOFOLLOW_LINKS
);
requireSafeRegularFile(lockPath);
fileLock = channel.lock();
return new GenerationPublicationLock(processLock, channel, fileLock);
} catch (IOException | RuntimeException error) {
if (fileLock != null) {
fileLock.close();
}
if (channel != null) {
channel.close();
}
processLock.unlock();
throw error;
}
}
@Override
public void close() throws IOException {
IOException failure = null;
try {
fileLock.close();
} catch (IOException error) {
failure = error;
}
try {
channel.close();
} catch (IOException error) {
if (failure == null) {
failure = error;
} else {
failure.addSuppressed(error);
}
} finally {
processLock.unlock();
}
if (failure != null) {
throw failure;
}
}
private static void requireSafeDirectory(Path directory) throws IOException {
BasicFileAttributes attributes = Files.readAttributes(
directory,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isDirectory() || attributes.isSymbolicLink()) {
throw new IOException("Generation publication lock directory is unsafe: " + directory);
}
}
private static void requireSafeRegularFile(Path file) throws IOException {
BasicFileAttributes attributes = Files.readAttributes(
file,
BasicFileAttributes.class,
LinkOption.NOFOLLOW_LINKS
);
if (!attributes.isRegularFile() || attributes.isSymbolicLink()) {
throw new IOException("Generation publication lock file is unsafe: " + file);
}
}
}
@@ -0,0 +1,605 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Pattern;
public final class GenerationRegistryContract {
public static final int FINGERPRINT_SCHEMA_VERSION_ONE = 1;
public static final int CURRENT_FINGERPRINT_SCHEMA = FINGERPRINT_SCHEMA_VERSION_ONE;
private static final String FINGERPRINT_DOMAIN = "iris-generation-registry-contract";
private static final int MAX_DEFINITIONS = 65_535;
private static final int MAX_RENDERER_IDENTITY_BYTES = 512;
private static final int MAX_GENERATED_SEMANTIC_BYTES = 1024 * 1024;
private static final int MAX_GENERATED_SOURCE_BYTES = 1024 * 1024;
private static final int MAX_TOTAL_GENERATED_SOURCE_BYTES = 8 * 1024 * 1024;
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
private static final Pattern SHA_256_PATTERN = Pattern.compile("[0-9a-f]{64}");
private final int fingerprintSchema;
private final String fingerprint;
private final NavigableMap<PhysicalResourceKey, String> definitions;
private final NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources;
private GenerationRegistryContract(
int fingerprintSchema,
String fingerprint,
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
) {
requireSupportedFingerprintSchema(fingerprintSchema);
this.fingerprintSchema = fingerprintSchema;
this.definitions = immutableDefinitions(definitions);
this.generatedSources = immutableGeneratedSources(generatedSources, this.definitions);
this.fingerprint = requireSha256(fingerprint, "Registry contract fingerprint");
String expectedFingerprint = fingerprint(fingerprintSchema, this.definitions, this.generatedSources);
if (!this.fingerprint.equals(expectedFingerprint)) {
throw new IllegalArgumentException("Registry contract fingerprint does not match its definitions.");
}
}
public static GenerationRegistryContract empty() {
return fromDefinitions(CURRENT_FINGERPRINT_SCHEMA, Map.of());
}
public static GenerationRegistryContract fromDefinitions(Map<PhysicalResourceKey, String> definitions) {
return fromDefinitions(CURRENT_FINGERPRINT_SCHEMA, definitions);
}
public static GenerationRegistryContract fromDefinitions(
int fingerprintSchema,
Map<PhysicalResourceKey, String> definitions
) {
return fromDefinitionsAndGeneratedSources(fingerprintSchema, definitions, Map.of());
}
public static GenerationRegistryContract fromDefinitionsAndGeneratedSources(
Map<PhysicalResourceKey, String> definitions,
Map<PhysicalResourceKey, GeneratedSource> generatedSources
) {
return fromDefinitionsAndGeneratedSources(
CURRENT_FINGERPRINT_SCHEMA,
definitions,
generatedSources
);
}
public static GenerationRegistryContract fromDefinitionsAndGeneratedSources(
int fingerprintSchema,
Map<PhysicalResourceKey, String> definitions,
Map<PhysicalResourceKey, GeneratedSource> generatedSources
) {
requireSupportedFingerprintSchema(fingerprintSchema);
NavigableMap<PhysicalResourceKey, String> canonicalDefinitions = canonicalizeDefinitions(definitions);
NavigableMap<PhysicalResourceKey, GeneratedSource> canonicalSources = canonicalizeGeneratedSources(
generatedSources,
canonicalDefinitions
);
return new GenerationRegistryContract(
fingerprintSchema,
fingerprint(fingerprintSchema, canonicalDefinitions, canonicalSources),
canonicalDefinitions,
canonicalSources
);
}
public int fingerprintSchema() {
return fingerprintSchema;
}
public String fingerprint() {
return fingerprint;
}
public NavigableMap<PhysicalResourceKey, String> definitions() {
return definitions;
}
public NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources() {
return generatedSources;
}
public void requireDefinitionsAvailableIn(GenerationRegistryContract available) throws IOException {
GenerationRegistryContract requiredAvailable = Objects.requireNonNull(available, "available");
for (Map.Entry<PhysicalResourceKey, String> entry : definitions.entrySet()) {
String availableFingerprint = requiredAvailable.definitions.get(entry.getKey());
if (availableFingerprint == null) {
throw new IOException("Missing historical registry definition "
+ entry.getKey().registryKey() + " / " + entry.getKey().resourceKey() + ".");
}
if (!entry.getValue().equals(availableFingerprint)) {
throw new IOException("Historical registry definition changed for "
+ entry.getKey().registryKey() + " / " + entry.getKey().resourceKey() + ".");
}
}
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("fingerprintSchema", fingerprintSchema);
json.addProperty("fingerprint", fingerprint);
JsonArray definitionArray = new JsonArray();
for (Map.Entry<PhysicalResourceKey, String> entry : definitions.entrySet()) {
JsonObject definitionJson = new JsonObject();
definitionJson.addProperty("registryKey", entry.getKey().registryKey());
definitionJson.addProperty("resourceKey", entry.getKey().resourceKey());
definitionJson.addProperty("definitionSha256", entry.getValue());
definitionArray.add(definitionJson);
}
json.add("definitions", definitionArray);
JsonArray generatedSourceArray = new JsonArray();
for (Map.Entry<PhysicalResourceKey, GeneratedSource> entry : generatedSources.entrySet()) {
JsonObject sourceJson = new JsonObject();
sourceJson.addProperty("registryKey", entry.getKey().registryKey());
sourceJson.addProperty("resourceKey", entry.getKey().resourceKey());
sourceJson.addProperty("sourceSchema", entry.getValue().sourceSchema());
sourceJson.addProperty("semanticSha256", entry.getValue().semanticSha256());
sourceJson.addProperty("semanticJson", entry.getValue().semanticJson());
sourceJson.addProperty("rendererIdentity", entry.getValue().rendererIdentity());
sourceJson.addProperty(
"renderedDefinitionSha256",
entry.getValue().renderedDefinitionSha256()
);
sourceJson.addProperty("sourceJson", entry.getValue().sourceJson());
generatedSourceArray.add(sourceJson);
}
json.add("generatedSources", generatedSourceArray);
return json;
}
static GenerationRegistryContract fromJson(JsonObject json) {
GenerationManifest.JsonSchema.requireFields(
json,
"registry contract",
"fingerprintSchema",
"fingerprint",
"definitions",
"generatedSources"
);
int fingerprintSchema = GenerationManifest.JsonSchema.requireInt(
json,
"fingerprintSchema",
"registry contract"
);
String fingerprint = GenerationManifest.JsonSchema.requireString(
json,
"fingerprint",
"registry contract"
);
JsonArray definitionArray = GenerationManifest.JsonSchema.requireArray(
json,
"definitions",
"registry contract"
);
NavigableMap<PhysicalResourceKey, String> definitions = parseCanonicalDefinitions(definitionArray);
JsonArray generatedSourceArray = GenerationManifest.JsonSchema.requireArray(
json,
"generatedSources",
"registry contract"
);
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources = parseCanonicalGeneratedSources(
generatedSourceArray,
definitions
);
return new GenerationRegistryContract(
fingerprintSchema,
fingerprint,
definitions,
generatedSources
);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GenerationRegistryContract contract)) {
return false;
}
return fingerprintSchema == contract.fingerprintSchema
&& fingerprint.equals(contract.fingerprint)
&& definitions.equals(contract.definitions)
&& generatedSources.equals(contract.generatedSources);
}
@Override
public int hashCode() {
return Objects.hash(fingerprintSchema, fingerprint, definitions, generatedSources);
}
@Override
public String toString() {
return "GenerationRegistryContract[fingerprintSchema=" + fingerprintSchema
+ ", fingerprint=" + fingerprint
+ ", definitions=" + definitions
+ ", generatedSourceKeys=" + generatedSources.keySet() + "]";
}
public static void requireSupportedFingerprintSchema(int fingerprintSchema) {
if (fingerprintSchema != FINGERPRINT_SCHEMA_VERSION_ONE) {
throw new IllegalArgumentException(
"Unsupported generation registry fingerprint schema " + fingerprintSchema + "."
);
}
}
private static NavigableMap<PhysicalResourceKey, String> canonicalizeDefinitions(
Map<PhysicalResourceKey, String> definitions
) {
Map<PhysicalResourceKey, String> requiredDefinitions = Objects.requireNonNull(definitions, "definitions");
if (requiredDefinitions.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry contract contains too many definitions.");
}
TreeMap<PhysicalResourceKey, String> canonicalDefinitions = new TreeMap<>();
for (Map.Entry<PhysicalResourceKey, String> entry : requiredDefinitions.entrySet()) {
PhysicalResourceKey key = Objects.requireNonNull(entry.getKey(), "registry definition key");
String definitionSha256 = requireSha256(entry.getValue(), "Registry definition fingerprint");
canonicalDefinitions.put(key, definitionSha256);
}
return canonicalDefinitions;
}
private static NavigableMap<PhysicalResourceKey, String> parseCanonicalDefinitions(JsonArray definitionArray) {
if (definitionArray.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry contract contains too many definitions.");
}
TreeMap<PhysicalResourceKey, String> definitions = new TreeMap<>();
PhysicalResourceKey previousKey = null;
for (int index = 0; index < definitionArray.size(); index++) {
JsonElement element = definitionArray.get(index);
JsonObject definitionJson = GenerationManifest.JsonSchema.requireObject(
element,
"registry contract.definitions[" + index + "]"
);
GenerationManifest.JsonSchema.requireFields(
definitionJson,
"registry contract definition",
"registryKey",
"resourceKey",
"definitionSha256"
);
PhysicalResourceKey key = new PhysicalResourceKey(
GenerationManifest.JsonSchema.requireString(
definitionJson,
"registryKey",
"registry contract definition"
),
GenerationManifest.JsonSchema.requireString(
definitionJson,
"resourceKey",
"registry contract definition"
)
);
if (previousKey != null && previousKey.compareTo(key) >= 0) {
throw new IllegalArgumentException(
"Registry contract definitions must be strictly sorted by physical resource key."
);
}
String definitionSha256 = requireSha256(
GenerationManifest.JsonSchema.requireString(
definitionJson,
"definitionSha256",
"registry contract definition"
),
"Registry definition fingerprint"
);
definitions.put(key, definitionSha256);
previousKey = key;
}
return definitions;
}
private static NavigableMap<PhysicalResourceKey, String> immutableDefinitions(
NavigableMap<PhysicalResourceKey, String> definitions
) {
NavigableMap<PhysicalResourceKey, String> canonicalDefinitions = canonicalizeDefinitions(definitions);
return Collections.unmodifiableNavigableMap(canonicalDefinitions);
}
private static NavigableMap<PhysicalResourceKey, GeneratedSource> canonicalizeGeneratedSources(
Map<PhysicalResourceKey, GeneratedSource> generatedSources,
NavigableMap<PhysicalResourceKey, String> definitions
) {
Map<PhysicalResourceKey, GeneratedSource> requiredSources = Objects.requireNonNull(
generatedSources,
"generatedSources"
);
if (requiredSources.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry contract contains too many generated sources.");
}
TreeMap<PhysicalResourceKey, GeneratedSource> canonicalSources = new TreeMap<>();
long totalBytes = 0L;
for (Map.Entry<PhysicalResourceKey, GeneratedSource> entry : requiredSources.entrySet()) {
PhysicalResourceKey key = Objects.requireNonNull(entry.getKey(), "generated source key");
if (!definitions.containsKey(key)) {
throw new IllegalArgumentException("Generated registry source has no matching definition: "
+ key.registryKey() + " / " + key.resourceKey() + ".");
}
GeneratedSource source = Objects.requireNonNull(entry.getValue(), "generated source");
int sourceBytes = source.sourceJson().getBytes(StandardCharsets.UTF_8).length;
if (sourceBytes > MAX_GENERATED_SOURCE_BYTES) {
throw new IllegalArgumentException("Generated registry source is too large: "
+ key.registryKey() + " / " + key.resourceKey() + ".");
}
totalBytes += sourceBytes;
totalBytes += source.semanticJson().getBytes(StandardCharsets.UTF_8).length;
totalBytes += source.rendererIdentity().getBytes(StandardCharsets.UTF_8).length;
totalBytes += source.sourceSchema().getBytes(StandardCharsets.UTF_8).length;
if (totalBytes > MAX_TOTAL_GENERATED_SOURCE_BYTES) {
throw new IllegalArgumentException("Registry contract generated sources are too large.");
}
canonicalSources.put(key, source);
}
return canonicalSources;
}
private static NavigableMap<PhysicalResourceKey, GeneratedSource> parseCanonicalGeneratedSources(
JsonArray generatedSourceArray,
NavigableMap<PhysicalResourceKey, String> definitions
) {
if (generatedSourceArray.size() > MAX_DEFINITIONS) {
throw new IllegalArgumentException("Registry contract contains too many generated sources.");
}
LinkedHashMap<PhysicalResourceKey, GeneratedSource> sources = new LinkedHashMap<>();
PhysicalResourceKey previousKey = null;
for (int index = 0; index < generatedSourceArray.size(); index++) {
JsonObject sourceJson = GenerationManifest.JsonSchema.requireObject(
generatedSourceArray.get(index),
"registry contract.generatedSources[" + index + "]"
);
GenerationManifest.JsonSchema.requireFields(
sourceJson,
"registry contract generated source",
"registryKey",
"resourceKey",
"sourceSchema",
"semanticSha256",
"semanticJson",
"rendererIdentity",
"renderedDefinitionSha256",
"sourceJson"
);
PhysicalResourceKey key = new PhysicalResourceKey(
GenerationManifest.JsonSchema.requireString(
sourceJson,
"registryKey",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"resourceKey",
"registry contract generated source"
)
);
if (previousKey != null && previousKey.compareTo(key) >= 0) {
throw new IllegalArgumentException(
"Registry contract generated sources must be strictly sorted by physical resource key."
);
}
sources.put(
key,
new GeneratedSource(
GenerationManifest.JsonSchema.requireString(
sourceJson,
"sourceSchema",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"semanticSha256",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"semanticJson",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"rendererIdentity",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"renderedDefinitionSha256",
"registry contract generated source"
),
GenerationManifest.JsonSchema.requireString(
sourceJson,
"sourceJson",
"registry contract generated source"
)
)
);
previousKey = key;
}
return canonicalizeGeneratedSources(sources, definitions);
}
private static NavigableMap<PhysicalResourceKey, GeneratedSource> immutableGeneratedSources(
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources,
NavigableMap<PhysicalResourceKey, String> definitions
) {
return Collections.unmodifiableNavigableMap(
canonicalizeGeneratedSources(generatedSources, definitions)
);
}
private static String fingerprint(
int fingerprintSchema,
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
) {
return switch (fingerprintSchema) {
case FINGERPRINT_SCHEMA_VERSION_ONE -> fingerprintVersionOne(definitions, generatedSources);
default -> throw new IllegalArgumentException(
"Unsupported generation registry fingerprint schema " + fingerprintSchema + "."
);
};
}
private static String fingerprintVersionOne(
NavigableMap<PhysicalResourceKey, String> definitions,
NavigableMap<PhysicalResourceKey, GeneratedSource> generatedSources
) {
MessageDigest digest = sha256();
updateString(digest, FINGERPRINT_DOMAIN);
updateInt(digest, FINGERPRINT_SCHEMA_VERSION_ONE);
updateInt(digest, definitions.size());
for (Map.Entry<PhysicalResourceKey, String> entry : definitions.entrySet()) {
updateString(digest, entry.getKey().registryKey());
updateString(digest, entry.getKey().resourceKey());
updateString(digest, entry.getValue());
}
updateInt(digest, generatedSources.size());
for (Map.Entry<PhysicalResourceKey, GeneratedSource> entry : generatedSources.entrySet()) {
updateString(digest, entry.getKey().registryKey());
updateString(digest, entry.getKey().resourceKey());
updateString(digest, entry.getValue().sourceSchema());
updateString(digest, entry.getValue().semanticSha256());
updateString(digest, entry.getValue().semanticJson());
updateString(digest, entry.getValue().rendererIdentity());
updateString(digest, entry.getValue().renderedDefinitionSha256());
updateString(digest, entry.getValue().sourceJson());
}
return HexFormat.of().formatHex(digest.digest());
}
private static String requireGeneratedJson(String value, String label, int maximumBytes) {
String source = Objects.requireNonNull(value, label);
if (source.isBlank()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
if (source.getBytes(StandardCharsets.UTF_8).length > maximumBytes) {
throw new IllegalArgumentException(label + " is too large.");
}
JsonElement parsed;
try {
parsed = JsonParser.parseString(source);
} catch (RuntimeException exception) {
throw new IllegalArgumentException(label + " must be valid JSON.", exception);
}
if (!parsed.isJsonObject()) {
throw new IllegalArgumentException(label + " must be a JSON object.");
}
return source;
}
private static String requireGeneratedSemanticJson(String value) {
String semantic = requireGeneratedJson(
value,
"Generated registry semantic JSON",
MAX_GENERATED_SEMANTIC_BYTES
);
return JsonParser.parseString(semantic).toString();
}
private static String requireRendererIdentity(String value) {
return requireBoundedIdentity(value, "Generated registry renderer identity");
}
private static String requireBoundedIdentity(String value, String label) {
String identity = Objects.requireNonNull(value, label).trim();
if (identity.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
if (identity.getBytes(StandardCharsets.UTF_8).length > MAX_RENDERER_IDENTITY_BYTES) {
throw new IllegalArgumentException(label + " is too large.");
}
return identity;
}
private static String requireSha256(String value, String label) {
String requiredValue = Objects.requireNonNull(value, label);
if (!SHA_256_PATTERN.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " must be a lowercase SHA-256 value.");
}
return requiredValue;
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable.", exception);
}
}
private static void updateString(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
updateInt(digest, bytes.length);
digest.update(bytes);
}
private static void updateInt(MessageDigest digest, int value) {
digest.update(ByteBuffer.allocate(Integer.BYTES).putInt(value).array());
}
public record PhysicalResourceKey(String registryKey, String resourceKey)
implements Comparable<PhysicalResourceKey> {
public PhysicalResourceKey {
registryKey = requireResourceKey(registryKey, "Registry key");
resourceKey = requireResourceKey(resourceKey, "Resource key");
}
@Override
public int compareTo(PhysicalResourceKey other) {
PhysicalResourceKey requiredOther = Objects.requireNonNull(other, "other");
int registryComparison = registryKey.compareTo(requiredOther.registryKey);
if (registryComparison != 0) {
return registryComparison;
}
return resourceKey.compareTo(requiredOther.resourceKey);
}
private static String requireResourceKey(String value, String label) {
String requiredValue = Objects.requireNonNull(value, label);
if (!RESOURCE_KEY_PATTERN.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " is invalid: " + requiredValue);
}
return requiredValue;
}
}
public record GeneratedSource(
String sourceSchema,
String semanticSha256,
String semanticJson,
String rendererIdentity,
String renderedDefinitionSha256,
String sourceJson
) {
public GeneratedSource {
sourceSchema = requireBoundedIdentity(sourceSchema, "Generated registry source schema");
semanticSha256 = requireSha256(semanticSha256, "Generated registry semantic fingerprint");
semanticJson = requireGeneratedSemanticJson(semanticJson);
rendererIdentity = requireRendererIdentity(rendererIdentity);
renderedDefinitionSha256 = requireSha256(
renderedDefinitionSha256,
"Rendered registry definition fingerprint"
);
sourceJson = requireGeneratedJson(
sourceJson,
"Generated registry source JSON",
MAX_GENERATED_SOURCE_BYTES
);
}
}
}
@@ -0,0 +1,224 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.StructurePlacementMarker;
import art.arcane.iris.engine.framework.StructurePlacementScope;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
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 java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public final class GenerationSemanticCapture {
private static final int CHUNK_SIZE = 16;
private GenerationSemanticCapture() {
}
public static ChunkGenerationSemantics capture(
Engine engine,
GenerationHistory.GenerationStage stage
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
GenerationHistory.GenerationStage requiredStage = Objects.requireNonNull(stage, "stage");
return capture(
requiredEngine,
requiredStage.chunkX(),
requiredStage.chunkZ(),
requiredStage.activation().activationId()
);
}
public static ChunkGenerationSemantics capture(
Engine engine,
int chunkX,
int chunkZ,
long activationId
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
ChunkGenerationSemantics.Builder semantics = ChunkGenerationSemantics.builder(
chunkX,
chunkZ,
activationId
);
captureColumns(requiredEngine, chunkX, chunkZ, semantics);
captureMantleFacts(requiredEngine, chunkX, chunkZ, semantics);
captureStructures(requiredEngine, chunkX, chunkZ, semantics);
return semantics.seal().build();
}
private static void captureColumns(
Engine engine,
int chunkX,
int chunkZ,
ChunkGenerationSemantics.Builder semantics
) {
IrisComplex complex = engine.getComplex();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int blockX = minimumX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int blockZ = minimumZ + localZ;
if (complex.allowsNewDiscreteContentAt(blockX, blockZ)) {
IrisBiome biome = complex.getTrueBiomeStream().get(blockX, blockZ);
if (biome != null && biome.getLoadKey() != null) {
semantics.addSurfaceBiome(biome.getLoadKey());
}
IrisRegion region = complex.getRegionStream().get(blockX, blockZ);
if (region != null && region.getLoadKey() != null) {
semantics.addRegion(region.getLoadKey());
}
}
if (!hasFullHydrologyWeight(complex, blockX, blockZ)) {
continue;
}
HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(blockX, blockZ);
if (hydrology == null) {
continue;
}
for (HydrologyColumnLayer layer : hydrology.layers()) {
semantics.addRiverProfile(layer.profileKey());
semantics.addRiverFeature(
layer.profileKey(),
layer.feature().type(),
layer.feature().id(),
layer.feature().x(),
Math.addExact(
layer.feature().y(),
engine.getDimension().getMinHeight()
),
layer.feature().z()
);
}
}
}
}
private static void captureMantleFacts(
Engine engine,
int chunkX,
int chunkZ,
ChunkGenerationSemantics.Builder semantics
) {
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) -> {
captureCave(
engine,
semantics,
resolvedCaves,
minimumX + localX,
y,
minimumZ + localZ,
cavern == null ? null : cavern.getCustomBiome()
);
});
mantle.iterateChunk(chunkX, chunkZ, HydrologyCaveCell.class, (localX, y, localZ, cell) -> {
int blockX = minimumX + localX;
int blockZ = minimumZ + localZ;
if (!hasFullHydrologyWeight(engine.getComplex(), blockX, blockZ)) {
return;
}
captureCave(
engine,
semantics,
resolvedCaves,
blockX,
y,
blockZ,
cell == null ? null : cell.floodedBiomeKey()
);
if (cell != null && cell.fluidProfileKey() != null && !cell.fluidProfileKey().isBlank()) {
semantics.addRiverProfile(cell.fluidProfileKey());
}
});
mantle.iterateChunk(chunkX, chunkZ, String.class, (localX, y, localZ, marker) -> {
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(marker);
if (decoded != null) {
semantics.addObject(decoded.objectKey());
}
});
}
private static boolean hasFullHydrologyWeight(IrisComplex complex, int blockX, int blockZ) {
TransitionGenerationPlan transition = complex.getTransitionGenerationPlan();
return transition == null || transition.hydrologyWeightAt(blockX, blockZ) == 1D;
}
private static void captureCave(
Engine engine,
ChunkGenerationSemantics.Builder semantics,
Set<CavePosition> resolvedCaves,
int blockX,
int y,
int blockZ,
String explicitBiomeKey
) {
CavePosition position = new CavePosition(blockX, y, blockZ);
if (!resolvedCaves.add(position)) {
return;
}
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
semantics.addCaveBiome(explicitBiomeKey);
return;
}
IrisBiome caveBiome = engine.getCaveBiome(blockX, y, blockZ);
if (caveBiome != null && caveBiome.getLoadKey() != null) {
semantics.addCaveBiome(caveBiome.getLoadKey());
}
}
private static void captureStructures(
Engine engine,
int chunkX,
int chunkZ,
ChunkGenerationSemantics.Builder semantics
) {
IrisComplex complex = engine.getComplex();
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (!complex.allowsNewGenerationChunk(chunkX, chunkZ)
|| !mantle.hasFlag(chunkX, chunkZ, ReservedFlag.JIGSAW)) {
return;
}
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(
engine,
chunkX,
chunkZ)) {
if (placement == null || !placement.hasIrisStructures()) {
continue;
}
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
engine,
placement,
chunkX,
chunkZ
);
if (resolved == null || !IrisStructureLocator.allowsResolvedFootprint(engine, resolved)) {
continue;
}
semantics.addStructure(
resolved.structureKey(),
resolved.originX(),
resolved.baseY(),
resolved.originZ()
);
}
}
private record CavePosition(int x, int y, int z) {
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,261 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
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;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
public final class GenerationSemanticQueries {
private GenerationSemanticQueries() {
}
public static IrisStructureLocator.LocateResult nearestStructure(
Engine engine,
String key,
int blockX,
int blockZ,
int maximumChunkRadius
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
String requiredKey = ChunkGenerationSemantics.requireResourceKey(
Objects.requireNonNull(key, "key").trim().toLowerCase(Locale.ROOT));
int radius = Math.max(0, Math.min(maximumChunkRadius, 2048));
GenerationHistory history = history(requiredEngine).orElse(null);
if (history == null) {
return IrisStructureLocator.locate(requiredEngine, requiredKey, blockX, blockZ, radius);
}
IrisStructureLocator.LocateResult active = IrisStructureLocator.locate(
requiredEngine, requiredKey, blockX, blockZ, radius,
(chunkX, chunkZ) -> allowsActivePrediction(requiredEngine, chunkX, chunkZ));
if (active.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
return active;
}
GenerationSemanticIndex.Match recorded = history.findRecorded(
GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.STRUCTURE,
requiredKey,
new ChunkGenerationSemantics.BlockPosition(blockX, 0, blockZ),
radius
)
).orElse(null);
if (recorded == null) {
return active;
}
ChunkGenerationSemantics.BlockPosition position = recorded.exactPosition().orElseThrow();
if (active.found() && horizontalDistanceSquared(blockX, blockZ, active.originX(), active.originZ())
.compareTo(horizontalDistanceSquared(blockX, blockZ, position.x(), position.z())) < 0) {
return active;
}
return new IrisStructureLocator.LocateResult(
IrisStructureLocator.LocateStatus.FOUND, position.x(), position.y(), position.z());
}
public static Optional<RiverResult> nearestRiver(
Engine engine,
HydrologyFeatureQuery query,
int blockX,
int blockZ,
int maximumDistance,
IntConsumer progress
) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
HydrologyFeatureQuery requiredQuery = Objects.requireNonNull(query, "query");
IntConsumer requiredProgress = Objects.requireNonNull(progress, "progress");
if (maximumDistance < 0) {
throw new IllegalArgumentException("Maximum river search distance cannot be negative.");
}
GenerationHistory history = history(requiredEngine).orElse(null);
RiverResult recorded = recordedRiver(
history,
requiredQuery,
blockX,
blockZ,
maximumDistance
);
RiverResult active = activeRiver(
requiredEngine,
history,
requiredQuery,
blockX,
blockZ,
maximumDistance,
requiredProgress
);
if (recorded == null) {
return Optional.ofNullable(active);
}
if (active == null) {
return Optional.of(recorded);
}
int comparison = horizontalDistanceSquared(blockX, blockZ, recorded.x(), recorded.z())
.compareTo(horizontalDistanceSquared(blockX, blockZ, active.x(), active.z()));
return Optional.of(comparison <= 0 ? recorded : active);
}
public static boolean allowsActivePrediction(Engine engine, int chunkX, int chunkZ) {
Engine requiredEngine = Objects.requireNonNull(engine, "engine");
if (!requiredEngine.getComplex().allowsNewGenerationChunk(chunkX, chunkZ)) {
return false;
}
GenerationHistory history = history(requiredEngine).orElse(null);
if (history == null) {
return true;
}
return history.isActiveUnowned(chunkX, chunkZ)
&& history.semantics(chunkX, chunkZ)
.filter(ChunkGenerationSemantics::sealed)
.isEmpty();
}
private static RiverResult recordedRiver(
GenerationHistory history,
HydrologyFeatureQuery query,
int blockX,
int blockZ,
int maximumDistance
) {
if (history == null) {
return null;
}
int maxChunkRadius = Math.addExact(Math.floorDiv(maximumDistance, 16), 1);
GenerationSemanticIndex.RiverMatch match = history.findRecordedRiver(
GenerationSemanticIndex.RiverQuery.acrossActivations(
query.types(),
query.profileKey(),
new ChunkGenerationSemantics.BlockPosition(blockX, 0, blockZ),
maxChunkRadius
)
).orElse(null);
if (match == null) {
return null;
}
ChunkGenerationSemantics.RiverFeatureOccurrence occurrence = match.occurrence();
ChunkGenerationSemantics.BlockPosition position = occurrence.position();
BigInteger maximumDistanceSquared = BigInteger.valueOf(maximumDistance)
.multiply(BigInteger.valueOf(maximumDistance));
if (horizontalDistanceSquared(blockX, blockZ, position.x(), position.z())
.compareTo(maximumDistanceSquared) > 0) {
return null;
}
return new RiverResult(
RiverSource.RECORDED,
occurrence.type(),
Optional.of(occurrence.profileKey()),
occurrence.featureId(),
position.x(),
position.y(),
position.z(),
OptionalLong.of(match.chunk().activationId())
);
}
private static RiverResult activeRiver(
Engine engine,
GenerationHistory history,
HydrologyFeatureQuery query,
int blockX,
int blockZ,
int maximumDistance,
IntConsumer progress
) {
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
if (runtime == null) {
return null;
}
Predicate<HydrologyFeatureRef> eligibility = feature -> allowsActiveHydrologyPrediction(
engine,
feature.x(),
feature.z()
);
HydrologyFeatureRef feature = runtime.nearestFeature(
query.types(),
query.profileKey(),
blockX,
blockZ,
maximumDistance,
eligibility,
progress
).orElse(null);
if (feature == null) {
return null;
}
OptionalLong activationId = history == null
? OptionalLong.empty()
: OptionalLong.of(history.activeActivation().activationId());
return new RiverResult(
RiverSource.ACTIVE_PREDICTION,
feature.type(),
Optional.ofNullable(query.profileKey()),
feature.id(),
feature.x(),
Math.addExact(feature.y(), engine.getDimension().getMinHeight()),
feature.z(),
activationId
);
}
private static boolean allowsActiveHydrologyPrediction(Engine engine, int blockX, int blockZ) {
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
if (transition != null && transition.hydrologyWeightAt(blockX, blockZ) != 1D) {
return false;
}
return allowsActivePrediction(
engine,
Math.floorDiv(blockX, 16),
Math.floorDiv(blockZ, 16)
);
}
private static Optional<GenerationHistory> history(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine)) {
return Optional.empty();
}
return irisEngine.getGenerationHistoryRuntimeRouter()
.map(GenerationHistoryRuntimeRouter::history);
}
private static BigInteger horizontalDistanceSquared(
int firstX,
int firstZ,
int secondX,
int secondZ
) {
BigInteger deltaX = BigInteger.valueOf((long) firstX - secondX);
BigInteger deltaZ = BigInteger.valueOf((long) firstZ - secondZ);
return deltaX.multiply(deltaX).add(deltaZ.multiply(deltaZ));
}
public enum RiverSource {
RECORDED,
ACTIVE_PREDICTION
}
public record RiverResult(
RiverSource source,
HydrologyFeatureType type,
Optional<String> profileKey,
long featureId,
int x,
int y,
int z,
OptionalLong activationId
) {
public RiverResult {
source = Objects.requireNonNull(source, "source");
type = Objects.requireNonNull(type, "type");
profileKey = Objects.requireNonNull(profileKey, "profileKey");
activationId = Objects.requireNonNull(activationId, "activationId");
}
}
}
@@ -0,0 +1,123 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import java.util.Objects;
import java.util.regex.Pattern;
public record GenerationTransition(
int algorithmVersion,
int widthBlocks,
String boundaryIdentity,
String terrainSignatureIdentity
) {
public static final int CURRENT_ALGORITHM_VERSION = 1;
public static final int MINIMUM_WIDTH_BLOCKS = 16;
public static final int MAXIMUM_WIDTH_BLOCKS = 16_384;
private static final Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}");
public GenerationTransition {
if (algorithmVersion < 1) {
throw new IllegalArgumentException("Transition algorithm version must be positive.");
}
if (widthBlocks < MINIMUM_WIDTH_BLOCKS || widthBlocks > MAXIMUM_WIDTH_BLOCKS) {
throw new IllegalArgumentException("Transition width must be between "
+ MINIMUM_WIDTH_BLOCKS + " and " + MAXIMUM_WIDTH_BLOCKS + " blocks.");
}
if ((boundaryIdentity == null) != (terrainSignatureIdentity == null)) {
throw new IllegalArgumentException("Transition snapshot identities must be completed together.");
}
if (boundaryIdentity != null) {
boundaryIdentity = requireIdentity(boundaryIdentity, "Boundary identity");
terrainSignatureIdentity = requireIdentity(
terrainSignatureIdentity,
"Terrain signature identity"
);
}
}
public static GenerationTransition pending(int widthBlocks) {
return new GenerationTransition(CURRENT_ALGORITHM_VERSION, widthBlocks, null, null);
}
public boolean isComplete() {
return boundaryIdentity != null;
}
public GenerationTransition complete(String boundaryIdentity, String terrainSignatureIdentity) {
if (isComplete()) {
if (this.boundaryIdentity.equals(boundaryIdentity)
&& this.terrainSignatureIdentity.equals(terrainSignatureIdentity)) {
return this;
}
throw new IllegalStateException("Generation transition snapshots are already immutable.");
}
return new GenerationTransition(
algorithmVersion,
widthBlocks,
boundaryIdentity,
terrainSignatureIdentity
);
}
JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("algorithmVersion", algorithmVersion);
json.addProperty("widthBlocks", widthBlocks);
if (boundaryIdentity == null) {
json.add("boundaryIdentity", JsonNull.INSTANCE);
json.add("terrainSignatureIdentity", JsonNull.INSTANCE);
} else {
json.addProperty("boundaryIdentity", boundaryIdentity);
json.addProperty("terrainSignatureIdentity", terrainSignatureIdentity);
}
return json;
}
static GenerationTransition fromJson(JsonObject json) {
GenerationManifest.JsonSchema.requireFields(
json,
"generation transition",
"algorithmVersion",
"widthBlocks",
"boundaryIdentity",
"terrainSignatureIdentity"
);
int algorithmVersion = GenerationManifest.JsonSchema.requireInt(
json,
"algorithmVersion",
"generation transition"
);
int widthBlocks = GenerationManifest.JsonSchema.requireInt(
json,
"widthBlocks",
"generation transition"
);
String boundaryIdentity = GenerationManifest.JsonSchema.requireNullableString(
json,
"boundaryIdentity",
"generation transition"
);
String terrainSignatureIdentity = GenerationManifest.JsonSchema.requireNullableString(
json,
"terrainSignatureIdentity",
"generation transition"
);
return new GenerationTransition(
algorithmVersion,
widthBlocks,
boundaryIdentity,
terrainSignatureIdentity
);
}
private static String requireIdentity(String value, String label) {
String requiredValue = Objects.requireNonNull(value, label);
if (!SHA_256.matcher(requiredValue).matches()) {
throw new IllegalArgumentException(label + " must be a lowercase SHA-256 value.");
}
return requiredValue;
}
}
@@ -0,0 +1,170 @@
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;
public final class IrisBoundarySignatureSampler implements GenerationBoundarySignatureSampler {
public static final IrisBoundarySignatureSampler INSTANCE = new IrisBoundarySignatureSampler();
static final int VERTICAL_SAMPLE_STEP = 4;
private IrisBoundarySignatureSampler() {
}
@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.");
}
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);
}
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);
}
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,555 @@
package art.arcane.iris.engine.history;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.zip.CRC32;
final class RegionGenerationOwnership {
static final int CHUNKS_PER_SIDE = 32;
static final int CHUNK_COUNT = CHUNKS_PER_SIDE * CHUNKS_PER_SIDE;
static final String FILE_SUFFIX = ".irow";
private static final int MAGIC = 0x49524F57;
private static final int FORMAT_VERSION = 1;
private static final int FIXED_BODY_BYTES = 32;
private static final int CHECKSUM_BYTES = Integer.BYTES;
private static final int MAX_FILE_BYTES = 16_384;
private static final int MINIMUM_REGION_COORDINATE = Math.floorDiv(Integer.MIN_VALUE, CHUNKS_PER_SIDE);
private static final int MAXIMUM_REGION_COORDINATE = Math.floorDiv(Integer.MAX_VALUE, CHUNKS_PER_SIDE);
private final int regionX;
private final int regionZ;
private final long[] activations;
private int assignmentCount;
private boolean dirty;
RegionGenerationOwnership(int regionX, int regionZ) {
this.regionX = regionX;
this.regionZ = regionZ;
this.activations = new long[CHUNK_COUNT];
}
static Metadata readMetadata(Path file) throws IOException {
byte[] encoded = readEncoded(file);
validateChecksum(file, encoded);
int bodyLength = encoded.length - CHECKSUM_BYTES;
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded, 0, bodyLength))) {
int magic = input.readInt();
if (magic != MAGIC) {
throw invalid(file, "invalid magic");
}
int version = input.readUnsignedShort();
if (version != FORMAT_VERSION) {
throw invalid(file, "unsupported format version " + version);
}
int flags = input.readUnsignedShort();
if (flags != 0) {
throw invalid(file, "unsupported format flags " + flags);
}
int regionX = input.readInt();
int regionZ = input.readInt();
validateRegionCoordinate(file, regionX);
validateRegionCoordinate(file, regionZ);
int assignmentCount = input.readInt();
int paletteSize = input.readInt();
int bitsPerEntry = input.readInt();
int wordCount = input.readInt();
validateHeader(file, assignmentCount, paletteSize, bitsPerEntry, wordCount);
long expectedLength = FIXED_BODY_BYTES
+ (long) paletteSize * Long.BYTES
+ (long) wordCount * Long.BYTES
+ CHECKSUM_BYTES;
if (encoded.length != expectedLength) {
throw invalid(file, "length does not match the ownership header");
}
return new Metadata(regionX, regionZ, assignmentCount);
} catch (EOFException error) {
throw invalid(file, "truncated data", error);
}
}
static RegionGenerationOwnership read(Path file) throws IOException {
byte[] encoded = readEncoded(file);
validateChecksum(file, encoded);
int bodyLength = encoded.length - CHECKSUM_BYTES;
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded, 0, bodyLength))) {
int magic = input.readInt();
if (magic != MAGIC) {
throw invalid(file, "invalid magic");
}
int version = input.readUnsignedShort();
if (version != FORMAT_VERSION) {
throw invalid(file, "unsupported format version " + version);
}
int flags = input.readUnsignedShort();
if (flags != 0) {
throw invalid(file, "unsupported format flags " + flags);
}
int regionX = input.readInt();
int regionZ = input.readInt();
validateRegionCoordinate(file, regionX);
validateRegionCoordinate(file, regionZ);
int assignmentCount = input.readInt();
int paletteSize = input.readInt();
int bitsPerEntry = input.readInt();
int wordCount = input.readInt();
validateHeader(file, assignmentCount, paletteSize, bitsPerEntry, wordCount);
long[] palette = readPalette(file, input, paletteSize);
long[] packed = readPacked(input, wordCount);
if (input.available() != 0) {
throw invalid(file, "unexpected trailing data");
}
validateUnusedBits(file, packed, bitsPerEntry);
RegionGenerationOwnership ownership = new RegionGenerationOwnership(regionX, regionZ);
int decodedAssignments = ownership.decode(file, palette, packed, bitsPerEntry);
if (decodedAssignments != assignmentCount) {
throw invalid(file, "assignment count mismatch");
}
ownership.assignmentCount = decodedAssignments;
return ownership;
} catch (EOFException error) {
throw invalid(file, "truncated data", error);
}
}
int regionX() {
return regionX;
}
int regionZ() {
return regionZ;
}
int assignmentCount() {
synchronized (this) {
return assignmentCount;
}
}
boolean hasAssignment(int chunkX, int chunkZ) {
return activations[chunkIndex(chunkX, chunkZ)] != 0L;
}
boolean anyMatchingInBounds(int minimumX, int minimumZ, int maximumX, int maximumZ,
ChunkGenerationOwnership.AssignmentPredicate predicate) {
int baseX = regionX << 5;
int baseZ = regionZ << 5;
int minimumLocalX = Math.max(minimumX, baseX) & 31;
int maximumLocalX = Math.min(maximumX, baseX + 31) & 31;
int minimumLocalZ = Math.max(minimumZ, baseZ) & 31;
int maximumLocalZ = Math.min(maximumZ, baseZ + 31) & 31;
for (int z = minimumLocalZ; z <= maximumLocalZ; z++) {
for (int x = minimumLocalX; x <= maximumLocalX; x++) {
long activation = activations[chunkIndex(x, z)];
if (activation != 0L && predicate.test(baseX + x, baseZ + z, activation)) {
return true;
}
}
}
return false;
}
long explicitActivation(int chunkX, int chunkZ) {
long activation = activations[chunkIndex(chunkX, chunkZ)];
if (activation == 0L) {
throw new IllegalStateException("Chunk " + chunkX + "," + chunkZ + " has no explicit generation activation");
}
return activation;
}
long resolve(int chunkX, int chunkZ, long currentActivation) {
requireActivation(currentActivation);
long activation = activations[chunkIndex(chunkX, chunkZ)];
return activation == 0L ? currentActivation : activation;
}
synchronized boolean assign(int chunkX, int chunkZ, long activation) {
requireActivation(activation);
int index = chunkIndex(chunkX, chunkZ);
long existing = activations[index];
if (existing == activation) {
return false;
}
if (existing != 0L) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " already belongs to activation " + existing
+ " and cannot be reassigned to " + activation
);
}
activations[index] = activation;
assignmentCount++;
dirty = true;
return true;
}
synchronized boolean persist(Path directory) throws IOException {
if (!dirty) {
return false;
}
ensureDirectory(directory);
Path target = directory.resolve(fileName(regionX, regionZ));
Path temporary = Files.createTempFile(directory, "." + target.getFileName() + "-", ".tmp");
try {
byte[] encoded = encode();
writeForced(temporary, encoded);
try {
Files.move(
temporary,
target,
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING
);
} catch (AtomicMoveNotSupportedException error) {
throw new IOException("Generation ownership requires atomic shard publication", error);
}
forceDirectory(directory);
dirty = false;
return true;
} finally {
Files.deleteIfExists(temporary);
}
}
int fillPackedChunkKeys(long[] target, int offset) {
int baseX = regionX << 5;
int baseZ = regionZ << 5;
int cursor = offset;
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) {
target[cursor++] = ChunkGenerationOwnership.packChunk(baseX + localX, baseZ + localZ);
}
}
}
return cursor;
}
void forEachAssignment(AssignmentConsumer consumer) throws IOException {
int baseX = regionX << 5;
int baseZ = regionZ << 5;
for (int localZ = 0; localZ < CHUNKS_PER_SIDE; localZ++) {
for (int localX = 0; localX < CHUNKS_PER_SIDE; localX++) {
long activation = activations[chunkIndex(localX, localZ)];
if (activation != 0L) {
consumer.accept(baseX + localX, baseZ + localZ, activation);
}
}
}
}
static String fileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}
private synchronized byte[] encode() throws IOException {
long[] activationSnapshot = new long[CHUNK_COUNT];
Long2IntOpenHashMap paletteIndexes = new Long2IntOpenHashMap();
paletteIndexes.defaultReturnValue(-1);
long[] palette = new long[assignmentCount];
int paletteSize = 0;
for (int index = 0; index < CHUNK_COUNT; index++) {
long activation = activations[index];
activationSnapshot[index] = activation;
if (activation == 0L || paletteIndexes.get(activation) >= 0) {
continue;
}
palette[paletteSize] = activation;
paletteIndexes.put(activation, paletteSize + 1);
paletteSize++;
}
int bitsPerEntry = bitsRequired(paletteSize + 1);
long[] packed = pack(activationSnapshot, paletteIndexes, bitsPerEntry);
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream(MAX_FILE_BYTES);
try (DataOutputStream output = new DataOutputStream(bodyBytes)) {
output.writeInt(MAGIC);
output.writeShort(FORMAT_VERSION);
output.writeShort(0);
output.writeInt(regionX);
output.writeInt(regionZ);
output.writeInt(assignmentCount);
output.writeInt(paletteSize);
output.writeInt(bitsPerEntry);
output.writeInt(packed.length);
for (int index = 0; index < paletteSize; index++) {
output.writeLong(palette[index]);
}
for (long word : packed) {
output.writeLong(word);
}
}
byte[] body = bodyBytes.toByteArray();
CRC32 checksum = new CRC32();
checksum.update(body);
ByteArrayOutputStream encodedBytes = new ByteArrayOutputStream(body.length + CHECKSUM_BYTES);
encodedBytes.write(body);
try (DataOutputStream output = new DataOutputStream(encodedBytes)) {
output.writeInt((int) checksum.getValue());
}
return encodedBytes.toByteArray();
}
private int decode(Path file, long[] palette, long[] packed, int bitsPerEntry) throws IOException {
int decodedAssignments = 0;
boolean[] paletteUsed = new boolean[palette.length];
for (int index = 0; index < CHUNK_COUNT; index++) {
int paletteIndex = unpack(packed, index, bitsPerEntry);
if (paletteIndex == 0) {
continue;
}
if (paletteIndex > palette.length) {
throw invalid(file, "palette index out of range");
}
activations[index] = palette[paletteIndex - 1];
paletteUsed[paletteIndex - 1] = true;
decodedAssignments++;
}
for (boolean used : paletteUsed) {
if (!used) {
throw invalid(file, "unused activation palette entry");
}
}
return decodedAssignments;
}
private static byte[] readEncoded(Path file) throws IOException {
long size = Files.size(file);
if (size < FIXED_BODY_BYTES + CHECKSUM_BYTES) {
throw invalid(file, "truncated data");
}
if (size > MAX_FILE_BYTES) {
throw invalid(file, "file is too large");
}
byte[] encoded = Files.readAllBytes(file);
if (encoded.length > MAX_FILE_BYTES) {
throw invalid(file, "file is too large");
}
return encoded;
}
private static void validateChecksum(Path file, byte[] encoded) throws IOException {
int bodyLength = encoded.length - CHECKSUM_BYTES;
int expected = ByteBuffer.wrap(encoded, bodyLength, CHECKSUM_BYTES).getInt();
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, bodyLength);
if ((int) checksum.getValue() != expected) {
throw invalid(file, "checksum mismatch");
}
}
private static void validateHeader(
Path file,
int assignmentCount,
int paletteSize,
int bitsPerEntry,
int wordCount
) throws IOException {
if (assignmentCount < 0 || assignmentCount > CHUNK_COUNT) {
throw invalid(file, "invalid assignment count " + assignmentCount);
}
if (paletteSize < 0 || paletteSize > assignmentCount) {
throw invalid(file, "invalid palette size " + paletteSize);
}
if ((assignmentCount == 0) != (paletteSize == 0)) {
throw invalid(file, "assignment and palette counts disagree");
}
int expectedBits = bitsRequired(paletteSize + 1);
if (bitsPerEntry != expectedBits) {
throw invalid(file, "invalid bits per entry " + bitsPerEntry);
}
int expectedWords = packedWordCount(bitsPerEntry);
if (wordCount != expectedWords) {
throw invalid(file, "invalid packed word count " + wordCount);
}
}
private static long[] readPalette(Path file, DataInputStream input, int paletteSize) throws IOException {
long[] palette = new long[paletteSize];
LongOpenHashSet unique = new LongOpenHashSet(paletteSize);
for (int index = 0; index < paletteSize; index++) {
long activation = input.readLong();
if (activation <= 0L) {
throw invalid(file, "activation IDs must be positive");
}
if (!unique.add(activation)) {
throw invalid(file, "duplicate activation in palette");
}
palette[index] = activation;
}
return palette;
}
private static long[] readPacked(DataInputStream input, int wordCount) throws IOException {
long[] packed = new long[wordCount];
for (int index = 0; index < wordCount; index++) {
packed[index] = input.readLong();
}
return packed;
}
private static void validateUnusedBits(Path file, long[] packed, int bitsPerEntry) throws IOException {
if (packed.length == 0) {
return;
}
int usedBits = (CHUNK_COUNT * bitsPerEntry) & 63;
if (usedBits == 0) {
return;
}
long unusedMask = -1L << usedBits;
if ((packed[packed.length - 1] & unusedMask) != 0L) {
throw invalid(file, "nonzero unused packed bits");
}
}
private static void validateRegionCoordinate(Path file, int regionCoordinate) throws IOException {
if (regionCoordinate < MINIMUM_REGION_COORDINATE || regionCoordinate > MAXIMUM_REGION_COORDINATE) {
throw invalid(file, "region coordinate is outside the chunk coordinate range");
}
}
private static long[] pack(
long[] activations,
Long2IntOpenHashMap paletteIndexes,
int bitsPerEntry
) {
long[] packed = new long[packedWordCount(bitsPerEntry)];
if (bitsPerEntry == 0) {
return packed;
}
for (int index = 0; index < CHUNK_COUNT; index++) {
long activation = activations[index];
int paletteIndex = activation == 0L ? 0 : paletteIndexes.get(activation);
int bitIndex = index * bitsPerEntry;
int wordIndex = bitIndex >>> 6;
int bitOffset = bitIndex & 63;
packed[wordIndex] |= (long) paletteIndex << bitOffset;
if (bitOffset + bitsPerEntry > Long.SIZE) {
packed[wordIndex + 1] |= (long) paletteIndex >>> (Long.SIZE - bitOffset);
}
}
return packed;
}
private static int unpack(long[] packed, int index, int bitsPerEntry) {
if (bitsPerEntry == 0) {
return 0;
}
int bitIndex = index * bitsPerEntry;
int wordIndex = bitIndex >>> 6;
int bitOffset = bitIndex & 63;
long value = packed[wordIndex] >>> bitOffset;
if (bitOffset + bitsPerEntry > Long.SIZE) {
value |= packed[wordIndex + 1] << (Long.SIZE - bitOffset);
}
long mask = (1L << bitsPerEntry) - 1L;
return (int) (value & mask);
}
private static int bitsRequired(int values) {
if (values <= 1) {
return 0;
}
return Integer.SIZE - Integer.numberOfLeadingZeros(values - 1);
}
private static int packedWordCount(int bitsPerEntry) {
return (CHUNK_COUNT * bitsPerEntry + Long.SIZE - 1) / Long.SIZE;
}
private static int chunkIndex(int chunkX, int chunkZ) {
return (chunkX & 31) + ((chunkZ & 31) << 5);
}
private static void requireActivation(long activation) {
if (activation <= 0L) {
throw new IllegalArgumentException("Generation activation IDs must be positive: " + activation);
}
}
private static void writeForced(Path file, byte[] encoded) throws IOException {
try (FileChannel channel = FileChannel.open(
file,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
ByteBuffer buffer = ByteBuffer.wrap(encoded);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
}
private static void ensureDirectory(Path directory) throws IOException {
Path parent = directory.getParent();
if (parent == null || !Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership parent directory is missing or unsafe: " + parent);
}
if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership path is not a safe directory: " + directory);
}
return;
}
try {
Files.createDirectory(directory);
} catch (FileAlreadyExistsException race) {
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership path is not a safe directory: " + directory, race);
}
return;
}
if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Generation ownership path is not a safe directory: " + directory);
}
}
private static void forceDirectory(Path directory) throws IOException {
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
} catch (UnsupportedOperationException error) {
throw new IOException("Generation ownership directory cannot be durability-synced", error);
}
}
private static IOException invalid(Path file, String reason) {
return new IOException("Invalid generation ownership file " + file + ": " + reason);
}
private static IOException invalid(Path file, String reason, Throwable cause) {
return new IOException("Invalid generation ownership file " + file + ": " + reason, cause);
}
record Metadata(int regionX, int regionZ, int assignmentCount) {
}
@FunctionalInterface
interface AssignmentConsumer {
void accept(int chunkX, int chunkZ, long activationId) throws IOException;
}
}
@@ -0,0 +1,170 @@
package art.arcane.iris.engine.history;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
public final class TerrainBoundarySignature {
private final Column column;
private final Samples samples;
public TerrainBoundarySignature(Column column, Samples samples) {
this.column = Objects.requireNonNull(column, "Boundary column");
this.samples = Objects.requireNonNull(samples, "Boundary samples");
}
public Column column() {
return column;
}
public Samples samples() {
return samples;
}
public int blockX() {
return column.blockX();
}
public int blockZ() {
return column.blockZ();
}
public int surfaceHeight() {
return column.surfaceHeight();
}
public int oceanFloorHeight() {
return column.oceanFloorHeight();
}
public OptionalInt fluidHeight() {
return column.fluidHeight();
}
public OptionalInt upperCeilingDepth() {
return column.upperCeilingDepth();
}
public int sampleCount() {
return samples.layout().sampleCount();
}
public int sampleY(int sampleIndex) {
return samples.layout().sampleY(sampleIndex);
}
public String biomeAtSample(int sampleIndex) {
return samples.biomes().biomeAtSample(sampleIndex);
}
public record Column(
int blockX,
int blockZ,
int surfaceHeight,
int oceanFloorHeight,
OptionalInt fluidHeight,
OptionalInt upperCeilingDepth
) {
public Column {
Objects.requireNonNull(fluidHeight, "Fluid height");
Objects.requireNonNull(upperCeilingDepth, "Upper ceiling depth");
if (upperCeilingDepth.isPresent() && upperCeilingDepth.getAsInt() <= 0) {
throw new IllegalArgumentException("Present upper ceiling depth must be positive");
}
}
}
public record Samples(VerticalLayout layout, BiomeEncoding biomes) {
public Samples {
Objects.requireNonNull(layout, "Vertical layout");
Objects.requireNonNull(biomes, "Biome samples");
if (biomes.sampleCount() != layout.sampleCount()) {
throw new IllegalArgumentException("Biome sample count must match the vertical layout");
}
}
}
public record VerticalLayout(int minimumY, int sampleStep, int sampleCount) {
public VerticalLayout {
if (sampleStep <= 0) {
throw new IllegalArgumentException("Sample step must be positive");
}
if (sampleCount < 0) {
throw new IllegalArgumentException("Sample count cannot be negative");
}
if (sampleCount > 0) {
Math.toIntExact((long) minimumY + (long) (sampleCount - 1) * sampleStep);
}
}
public int sampleY(int sampleIndex) {
validateSampleIndex(sampleIndex, sampleCount);
return Math.toIntExact((long) minimumY + (long) sampleIndex * sampleStep);
}
}
public static final class BiomeEncoding {
private final List<String> palette;
private final short[] paletteIndices;
public BiomeEncoding(List<String> palette, short[] paletteIndices) {
this.palette = List.copyOf(Objects.requireNonNull(palette, "Biome palette"));
this.paletteIndices = Objects.requireNonNull(paletteIndices, "Biome palette indices").clone();
validatePalette();
}
public List<String> palette() {
return palette;
}
public short[] paletteIndices() {
return paletteIndices.clone();
}
public int sampleCount() {
return paletteIndices.length;
}
public String biomeAtSample(int sampleIndex) {
validateSampleIndex(sampleIndex, paletteIndices.length);
return palette.get(paletteIndices[sampleIndex]);
}
@Override
public boolean equals(Object compared) {
if (this == compared) {
return true;
}
if (!(compared instanceof BiomeEncoding encoding)) {
return false;
}
return palette.equals(encoding.palette) && Arrays.equals(paletteIndices, encoding.paletteIndices);
}
@Override
public int hashCode() {
return 31 * palette.hashCode() + Arrays.hashCode(paletteIndices);
}
private void validatePalette() {
if (paletteIndices.length > 0 && palette.isEmpty()) {
throw new IllegalArgumentException("Biome palette is required when samples exist");
}
if (palette.size() > Short.MAX_VALUE + 1) {
throw new IllegalArgumentException("Biome palette exceeds compact index capacity");
}
for (short paletteIndex : paletteIndices) {
if (paletteIndex < 0 || paletteIndex >= palette.size()) {
throw new IllegalArgumentException("Biome palette index is outside the palette");
}
}
}
}
private static void validateSampleIndex(int sampleIndex, int sampleCount) {
if (sampleIndex < 0 || sampleIndex >= sampleCount) {
throw new IndexOutOfBoundsException("Sample index " + sampleIndex + " outside sample count " + sampleCount);
}
}
}
@@ -0,0 +1,367 @@
package art.arcane.iris.engine.history;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class TransitionBoundarySampler {
private static final int NEIGHBOUR_COUNT = 4;
private static final int MAXIMUM_CACHED_CHUNKS = 8;
private final int terrainWidth;
private final int searchWidth;
private final TerrainBoundarySignatureStore.Snapshot signatures;
private final LinkedHashMap<Long, CandidateIndex> chunkCandidates;
private final ThreadLocal<ChunkCache> cache;
private long candidateBuildCount;
TransitionBoundarySampler(
int terrainWidth,
TerrainBoundarySignatureStore.Snapshot signatures
) {
if (terrainWidth <= 0) {
throw new IllegalArgumentException("Terrain transition width must be positive");
}
this.terrainWidth = terrainWidth;
this.searchWidth = Math.multiplyExact(terrainWidth, 2);
this.signatures = Objects.requireNonNull(signatures, "Terrain boundary signatures");
this.chunkCandidates = new LinkedHashMap<>(MAXIMUM_CACHED_CHUNKS, 0.75F, true);
this.cache = ThreadLocal.withInitial(ChunkCache::new);
}
TransitionGenerationPlan.TerrainSample sample(int blockX, int blockZ) {
ChunkCache chunkCache = cache.get();
int chunkX = Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE);
if (chunkCache.chunkX != chunkX || chunkCache.chunkZ != chunkZ) {
chunkCache.reset(chunkX, chunkZ, candidatesForChunk(chunkX, chunkZ));
}
int localX = Math.floorMod(blockX, GenerationBoundary.CHUNK_SIZE);
int localZ = Math.floorMod(blockZ, GenerationBoundary.CHUNK_SIZE);
int index = localX * GenerationBoundary.CHUNK_SIZE + localZ;
TransitionGenerationPlan.TerrainSample existing = chunkCache.samples[index];
if (existing != null) {
return existing;
}
TransitionGenerationPlan.TerrainSample sampled = sampleUncached(
blockX,
blockZ,
chunkCache.candidates
);
chunkCache.samples[index] = sampled;
return sampled;
}
boolean intersectsTerrainBand(int minimumX, int minimumZ, int maximumX, int maximumZ) {
return signatures.intersectsTerrainBand(
new TerrainBoundarySignatureStore.BlockBounds(minimumX, minimumZ, maximumX, maximumZ),
terrainWidth
);
}
long catalogProbeCount() {
return signatures.catalogProbeCount();
}
long shardLoadCount() {
return signatures.shardLoadCount();
}
synchronized long candidateBuildCount() {
return candidateBuildCount;
}
private TransitionGenerationPlan.TerrainSample sampleUncached(
int blockX,
int blockZ,
CandidateIndex candidates
) {
Nearest nearest = candidates.nearest(blockX, blockZ, square(searchWidth));
if (nearest.count == 0) {
return TransitionGenerationPlan.TerrainSample.newTerrain(searchWidth);
}
double distance = Math.sqrt(nearest.distancesSquared[0]);
double newEpochWeight = GenerationBlend.newEpochWeight(
Math.min(distance, terrainWidth),
terrainWidth
);
double hydrologyDistance = Math.max(0D, distance - terrainWidth);
double hydrologyWeight = GenerationBlend.newEpochWeight(
Math.min(hydrologyDistance, terrainWidth),
terrainWidth
);
return new TransitionGenerationPlan.TerrainSample(
Math.min(distance, searchWidth),
newEpochWeight,
hydrologyWeight,
nearest.signatures[0],
weightedHeight(nearest, true),
weightedHeight(nearest, false),
weightedUpperCeilingDepth(nearest)
);
}
private CandidateIndex candidatesForChunk(int chunkX, int chunkZ) {
long chunkKey = pack(chunkX, chunkZ);
synchronized (this) {
CandidateIndex existing = chunkCandidates.get(chunkKey);
if (existing != null) {
return existing;
}
}
CandidateIndex built = CandidateIndex.build(signatures.nearestCandidatesForChunk(
chunkX,
chunkZ,
searchWidth
));
synchronized (this) {
CandidateIndex raced = chunkCandidates.get(chunkKey);
if (raced != null) {
return raced;
}
chunkCandidates.put(chunkKey, built);
candidateBuildCount++;
while (chunkCandidates.size() > MAXIMUM_CACHED_CHUNKS) {
Iterator<Map.Entry<Long, CandidateIndex>> entries = chunkCandidates.entrySet().iterator();
entries.next();
entries.remove();
}
return built;
}
}
private static double weightedHeight(Nearest nearest, boolean surface) {
if (nearest.distancesSquared[0] <= 1D) {
return surface
? nearest.signatures[0].surfaceHeight()
: nearest.signatures[0].oceanFloorHeight();
}
double weighted = 0D;
double totalWeight = 0D;
for (int index = 0; index < nearest.count; index++) {
double weight = 1D / nearest.distancesSquared[index];
int height = surface
? nearest.signatures[index].surfaceHeight()
: nearest.signatures[index].oceanFloorHeight();
weighted += height * weight;
totalWeight += weight;
}
return weighted / totalWeight;
}
private static double weightedUpperCeilingDepth(Nearest nearest) {
if (nearest.distancesSquared[0] <= 1D) {
return nearest.signatures[0].upperCeilingDepth().orElse(0);
}
double weighted = 0D;
double totalWeight = 0D;
for (int index = 0; index < nearest.count; index++) {
double weight = 1D / nearest.distancesSquared[index];
weighted += nearest.signatures[index].upperCeilingDepth().orElse(0) * weight;
totalWeight += weight;
}
return weighted / totalWeight;
}
private static long distanceToRange(long value, long minimum, long maximum) {
if (value < minimum) {
return minimum - value;
}
return value > maximum ? value - maximum : 0L;
}
private static double square(long value) {
return (double) value * value;
}
private static long pack(int x, int z) {
return ((long) x << Integer.SIZE) | (z & 0xffffffffL);
}
private static final class CandidateIndex {
private static final CandidateIndex EMPTY = new CandidateIndex(null);
private final Node root;
private CandidateIndex(Node root) {
this.root = root;
}
private static CandidateIndex build(List<TerrainBoundarySignature> candidates) {
if (candidates.isEmpty()) {
return EMPTY;
}
TerrainBoundarySignature[] values = candidates.toArray(new TerrainBoundarySignature[0]);
return new CandidateIndex(build(values, 0, values.length, 0));
}
private Nearest nearest(int blockX, int blockZ, double maximumDistanceSquared) {
Nearest nearest = new Nearest(maximumDistanceSquared);
search(root, blockX, blockZ, nearest);
return nearest;
}
private static Node build(
TerrainBoundarySignature[] values,
int from,
int to,
int depth
) {
if (from >= to) {
return null;
}
Comparator<TerrainBoundarySignature> comparator = depth % 2 == 0
? Comparator.comparingInt(TerrainBoundarySignature::blockX)
.thenComparingInt(TerrainBoundarySignature::blockZ)
: Comparator.comparingInt(TerrainBoundarySignature::blockZ)
.thenComparingInt(TerrainBoundarySignature::blockX);
Arrays.sort(values, from, to, comparator);
int middle = from + (to - from) / 2;
Node left = build(values, from, middle, depth + 1);
Node right = build(values, middle + 1, to, depth + 1);
return new Node(values[middle], left, right);
}
private static void search(Node node, int blockX, int blockZ, Nearest nearest) {
if (node == null || node.distanceSquaredToBounds(blockX, blockZ) > nearest.limitSquared()) {
return;
}
long deltaX = (long) node.signature.blockX() - blockX;
long deltaZ = (long) node.signature.blockZ() - blockZ;
nearest.offer(node.signature, square(deltaX) + square(deltaZ));
Node first = node.left;
Node second = node.right;
if (first == null || second != null
&& second.distanceSquaredToBounds(blockX, blockZ)
< first.distanceSquaredToBounds(blockX, blockZ)) {
first = node.right;
second = node.left;
}
search(first, blockX, blockZ, nearest);
search(second, blockX, blockZ, nearest);
}
}
private static final class Node {
private final TerrainBoundarySignature signature;
private final Node left;
private final Node right;
private final int minimumX;
private final int maximumX;
private final int minimumZ;
private final int maximumZ;
private Node(TerrainBoundarySignature signature, Node left, Node right) {
this.signature = signature;
this.left = left;
this.right = right;
minimumX = Math.min(signature.blockX(), Math.min(minimumX(left), minimumX(right)));
maximumX = Math.max(signature.blockX(), Math.max(maximumX(left), maximumX(right)));
minimumZ = Math.min(signature.blockZ(), Math.min(minimumZ(left), minimumZ(right)));
maximumZ = Math.max(signature.blockZ(), Math.max(maximumZ(left), maximumZ(right)));
}
private double distanceSquaredToBounds(int blockX, int blockZ) {
long distanceX = distanceToRange(blockX, minimumX, maximumX);
long distanceZ = distanceToRange(blockZ, minimumZ, maximumZ);
return square(distanceX) + square(distanceZ);
}
private static int minimumX(Node node) {
return node == null ? Integer.MAX_VALUE : node.minimumX;
}
private static int maximumX(Node node) {
return node == null ? Integer.MIN_VALUE : node.maximumX;
}
private static int minimumZ(Node node) {
return node == null ? Integer.MAX_VALUE : node.minimumZ;
}
private static int maximumZ(Node node) {
return node == null ? Integer.MIN_VALUE : node.maximumZ;
}
}
private static final class Nearest {
private final TerrainBoundarySignature[] signatures = new TerrainBoundarySignature[NEIGHBOUR_COUNT];
private final double[] distancesSquared = new double[NEIGHBOUR_COUNT];
private final double maximumDistanceSquared;
private int count;
private Nearest(double maximumDistanceSquared) {
this.maximumDistanceSquared = maximumDistanceSquared;
}
private void offer(TerrainBoundarySignature signature, double distanceSquared) {
if (distanceSquared > maximumDistanceSquared) {
return;
}
int insertion = 0;
while (insertion < count && compare(
distancesSquared[insertion],
signatures[insertion],
distanceSquared,
signature
) <= 0) {
insertion++;
}
if (insertion >= NEIGHBOUR_COUNT) {
return;
}
int copyLength = Math.min(count, NEIGHBOUR_COUNT - 1) - insertion;
if (copyLength > 0) {
System.arraycopy(signatures, insertion, signatures, insertion + 1, copyLength);
System.arraycopy(distancesSquared, insertion, distancesSquared, insertion + 1, copyLength);
}
signatures[insertion] = signature;
distancesSquared[insertion] = distanceSquared;
count = Math.min(NEIGHBOUR_COUNT, count + 1);
}
private double limitSquared() {
return count < NEIGHBOUR_COUNT
? maximumDistanceSquared
: Math.min(maximumDistanceSquared, distancesSquared[NEIGHBOUR_COUNT - 1]);
}
private static int compare(
double firstDistance,
TerrainBoundarySignature first,
double secondDistance,
TerrainBoundarySignature second
) {
int distanceComparison = Double.compare(firstDistance, secondDistance);
if (distanceComparison != 0) {
return distanceComparison;
}
int xComparison = Integer.compare(first.blockX(), second.blockX());
return xComparison != 0
? xComparison
: Integer.compare(first.blockZ(), second.blockZ());
}
}
private static final class ChunkCache {
private final TransitionGenerationPlan.TerrainSample[] samples =
new TransitionGenerationPlan.TerrainSample[
GenerationBoundary.CHUNK_SIZE * GenerationBoundary.CHUNK_SIZE
];
private int chunkX = Integer.MIN_VALUE;
private int chunkZ = Integer.MIN_VALUE;
private CandidateIndex candidates = CandidateIndex.EMPTY;
private void reset(int nextChunkX, int nextChunkZ, CandidateIndex nextCandidates) {
Arrays.fill(samples, null);
chunkX = nextChunkX;
chunkZ = nextChunkZ;
candidates = nextCandidates;
}
}
}
@@ -0,0 +1,221 @@
package art.arcane.iris.engine.history;
import java.util.Objects;
import java.util.Optional;
public final class TransitionGenerationPlan {
private final Specification specification;
private final GenerationBoundary boundary;
private final TerrainBoundarySignatureStore.Snapshot terrainSignatures;
private final TransitionBoundarySampler terrainSampler;
public TransitionGenerationPlan(
Specification specification,
GenerationBoundary boundary,
TerrainBoundarySignatureStore.Snapshot terrainSignatures
) {
this.specification = Objects.requireNonNull(specification, "Transition specification");
this.boundary = Objects.requireNonNull(boundary, "Generation boundary");
this.terrainSignatures = Objects.requireNonNull(terrainSignatures, "Terrain signatures");
if (!specification.boundaryIdentity().equals(boundary.identity())) {
throw new IllegalArgumentException("Transition boundary identity does not match the frozen boundary");
}
if (specification.activationId() != terrainSignatures.activationId()) {
throw new IllegalArgumentException("Terrain signatures belong to a different activation");
}
if (!specification.terrainSignatureIdentity().equals(terrainSignatures.identity())) {
throw new IllegalArgumentException("Transition terrain signature identity does not match the snapshot");
}
if (specification.algorithmVersion() != GenerationTransition.CURRENT_ALGORITHM_VERSION) {
throw new IllegalArgumentException("Unsupported generation transition algorithm version "
+ specification.algorithmVersion());
}
terrainSampler = new TransitionBoundarySampler(
specification.widthBlocks(),
terrainSignatures
);
}
public long activationId() {
return specification.activationId();
}
public String oldEpochId() {
return specification.oldEpochId();
}
public String newEpochId() {
return specification.newEpochId();
}
public int widthBlocks() {
return specification.widthBlocks();
}
public int algorithmVersion() {
return specification.algorithmVersion();
}
public String boundaryIdentity() {
return specification.boundaryIdentity();
}
public Specification specification() {
return specification;
}
public GenerationBoundary boundary() {
return boundary;
}
public TerrainBoundarySignatureStore.Snapshot terrainSignatures() {
return terrainSignatures;
}
public boolean isHistoricalBlock(int blockX, int blockZ) {
return boundary.isHistoricalBlock(blockX, blockZ);
}
public double distanceToHistoricalChunks(int blockX, int blockZ) {
return Math.min(terrainSampleAt(blockX, blockZ).distanceToHistoricalTerrain(), widthBlocks());
}
public double newEpochWeightAt(int blockX, int blockZ) {
return terrainSampleAt(blockX, blockZ).newEpochWeight();
}
public double hydrologyWeightAt(int blockX, int blockZ) {
return terrainSampleAt(blockX, blockZ).hydrologyWeight();
}
public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) {
return terrainSampleAt(blockX, blockZ).newEpochWeight() == 1D;
}
public TerrainSample terrainSampleAt(int blockX, int blockZ) {
if (boundary.isHistoricalBlock(blockX, blockZ)) {
return TerrainSample.historicalTerrain();
}
return terrainSampler.sample(blockX, blockZ);
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockX, int blockY, int blockZ) {
TerrainSample sample = terrainSampleAt(blockX, blockZ);
if (sample.newEpochWeight() == 1D) {
return Optional.empty();
}
return sample.historicalPhysicalBiomeKeyAt(blockY);
}
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);
}
long terrainCatalogProbeCount() {
return terrainSampler.catalogProbeCount();
}
long terrainShardLoadCount() {
return terrainSampler.shardLoadCount();
}
long terrainCandidateBuildCount() {
return terrainSampler.candidateBuildCount();
}
public record Specification(
long activationId,
String oldEpochId,
String newEpochId,
int algorithmVersion,
int widthBlocks,
String boundaryIdentity,
String terrainSignatureIdentity
) {
public Specification {
if (activationId <= 0L) {
throw new IllegalArgumentException("Activation ID must be positive");
}
oldEpochId = requireIdentifier(oldEpochId, "Old epoch ID");
newEpochId = requireIdentifier(newEpochId, "New epoch ID");
boundaryIdentity = requireIdentifier(boundaryIdentity, "Boundary identity");
terrainSignatureIdentity = requireIdentifier(
terrainSignatureIdentity,
"Terrain signature identity"
);
if (oldEpochId.equals(newEpochId)) {
throw new IllegalArgumentException("Transition epoch IDs must differ");
}
if (widthBlocks <= 0) {
throw new IllegalArgumentException("Transition width must be positive");
}
if (algorithmVersion < 1) {
throw new IllegalArgumentException("Transition algorithm version must be positive");
}
}
private static String requireIdentifier(String identifier, String label) {
String requiredIdentifier = Objects.requireNonNull(identifier, label);
if (requiredIdentifier.isBlank()) {
throw new IllegalArgumentException(label + " cannot be blank");
}
return requiredIdentifier;
}
}
public record TerrainSample(
double distanceToHistoricalTerrain,
double newEpochWeight,
double hydrologyWeight,
TerrainBoundarySignature nearestSignature,
double historicalSurfaceHeight,
double historicalOceanFloorHeight,
double historicalUpperCeilingDepth
) {
public TerrainSample {
if (!Double.isFinite(distanceToHistoricalTerrain) || distanceToHistoricalTerrain < 0D) {
throw new IllegalArgumentException("Historical terrain distance must be finite and non-negative");
}
if (!Double.isFinite(newEpochWeight) || newEpochWeight < 0D || newEpochWeight > 1D) {
throw new IllegalArgumentException("New epoch weight must be between zero and one");
}
if (!Double.isFinite(hydrologyWeight) || hydrologyWeight < 0D || hydrologyWeight > 1D) {
throw new IllegalArgumentException("Hydrology weight must be between zero and one");
}
if (nearestSignature != null
&& (!Double.isFinite(historicalSurfaceHeight)
|| !Double.isFinite(historicalOceanFloorHeight)
|| !Double.isFinite(historicalUpperCeilingDepth)
|| historicalUpperCeilingDepth < 0D)) {
throw new IllegalArgumentException("Historical terrain heights must be finite");
}
}
static TerrainSample newTerrain(double distance) {
return new TerrainSample(distance, 1D, 1D, null, 0D, 0D, 0D);
}
static TerrainSample historicalTerrain() {
return new TerrainSample(0D, 0D, 0D, null, 0D, 0D, 0D);
}
public boolean hasHistoricalSignature() {
return nearestSignature != null;
}
public Optional<String> historicalPhysicalBiomeKeyAt(int blockY) {
if (nearestSignature == null || nearestSignature.sampleCount() == 0) {
return Optional.empty();
}
TerrainBoundarySignature.VerticalLayout layout = nearestSignature.samples().layout();
long offset = (long) blockY - layout.minimumY();
long roundedIndex = Math.round(offset / (double) layout.sampleStep());
int sampleIndex = (int) Math.max(0L, Math.min(roundedIndex, layout.sampleCount() - 1L));
return Optional.of(nearestSignature.biomeAtSample(sampleIndex));
}
}
}
@@ -0,0 +1,238 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import art.arcane.iris.util.nbt.common.mca.MCAUtil;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.DirectoryStream;
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.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class WorldChunkInventory {
private static final int REGION_SIDE = 32;
private static final int SECTOR_BYTES = 4_096;
private static final int HEADER_SECTORS = 2;
private static final int HEADER_BYTES = SECTOR_BYTES * HEADER_SECTORS;
private static final Pattern REGION_FILE = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.mca$");
private final Long2ObjectOpenHashMap<BitSet> regionMasks;
private final long[] regionKeys;
private final int chunkCount;
private WorldChunkInventory(Long2ObjectOpenHashMap<BitSet> regionMasks) {
this.regionMasks = regionMasks;
this.regionKeys = regionMasks.keySet().toLongArray();
Arrays.sort(regionKeys);
int count = 0;
for (BitSet mask : regionMasks.values()) {
count = Math.addExact(count, mask.cardinality());
}
this.chunkCount = count;
}
public static WorldChunkInventory empty() {
return new WorldChunkInventory(new Long2ObjectOpenHashMap<>());
}
public static WorldChunkInventory ofPackedChunks(long... packedChunks) {
Long2ObjectOpenHashMap<BitSet> masks = new Long2ObjectOpenHashMap<>();
for (long packed : packedChunks) {
int chunkX = ChunkGenerationOwnership.chunkX(packed);
int chunkZ = ChunkGenerationOwnership.chunkZ(packed);
long regionKey = ChunkGenerationOwnership.packChunk(chunkX >> 5, chunkZ >> 5);
masks.computeIfAbsent(regionKey, ignored -> new BitSet(REGION_SIDE * REGION_SIDE))
.set(MCAFile.getChunkIndex(chunkX & 31, chunkZ & 31));
}
return new WorldChunkInventory(masks);
}
public static WorldChunkInventory scan(Path worldDirectory) throws IOException {
return scanRegionDirectory(worldDirectory.resolve("region"));
}
public static boolean isDurablyAllocated(Path worldDirectory, int chunkX, int chunkZ) throws IOException {
Path regionDirectory = Objects.requireNonNull(worldDirectory, "worldDirectory")
.toAbsolutePath()
.normalize()
.resolve("region");
if (!Files.exists(regionDirectory, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
if (!Files.isDirectory(regionDirectory, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("World region path is not a directory: " + regionDirectory);
}
int regionX = Math.floorDiv(chunkX, REGION_SIDE);
int regionZ = Math.floorDiv(chunkZ, REGION_SIDE);
Path regionFile = regionDirectory.resolve(MCAUtil.createNameFromRegionLocation(regionX, regionZ));
if (!Files.exists(regionFile, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
if (!Files.isRegularFile(regionFile, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("World region entry is not a regular file: " + regionFile);
}
BitSet mask = scanAllocationTable(regionFile);
return mask.get(MCAFile.getChunkIndex(chunkX & 31, chunkZ & 31));
}
public static WorldChunkInventory scanRegionDirectory(Path regionDirectory) throws IOException {
Path normalized = regionDirectory.toAbsolutePath().normalize();
if (!Files.exists(normalized, LinkOption.NOFOLLOW_LINKS)) {
return empty();
}
if (!Files.isDirectory(normalized, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("World region path is not a directory: " + normalized);
}
List<Path> regionFiles = collectRegionFiles(normalized);
Long2ObjectOpenHashMap<BitSet> masks = new Long2ObjectOpenHashMap<>(regionFiles.size());
for (Path regionFile : regionFiles) {
RegionPosition position = parseRegionPosition(regionFile);
long regionKey = ChunkGenerationOwnership.packChunk(position.x(), position.z());
if (masks.containsKey(regionKey)) {
throw new IOException(
"Duplicate world region file for " + position.x() + "," + position.z()
);
}
masks.put(regionKey, scanAllocationTable(regionFile));
}
return new WorldChunkInventory(masks);
}
public int size() {
return chunkCount;
}
public boolean isEmpty() {
return chunkCount == 0;
}
public boolean contains(int chunkX, int chunkZ) {
BitSet mask = regionMasks.get(ChunkGenerationOwnership.packChunk(chunkX >> 5, chunkZ >> 5));
return mask != null && mask.get(MCAFile.getChunkIndex(chunkX & 31, chunkZ & 31));
}
public void forEach(ChunkConsumer consumer) throws IOException {
Objects.requireNonNull(consumer, "consumer");
for (long regionKey : regionKeys) {
int baseX = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkX(regionKey));
int baseZ = MCAUtil.regionToChunk(ChunkGenerationOwnership.chunkZ(regionKey));
BitSet mask = regionMasks.get(regionKey);
for (int index = mask.nextSetBit(0); index >= 0; index = mask.nextSetBit(index + 1)) {
consumer.accept(baseX + (index & 31), baseZ + (index >> 5));
}
}
}
int retainedAllocationBytes() {
return Math.multiplyExact(regionKeys.length, REGION_SIDE * REGION_SIDE / Byte.SIZE);
}
private static List<Path> collectRegionFiles(Path directory) throws IOException {
List<Path> files = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*.mca")) {
for (Path file : stream) {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("World region entry is not a regular file: " + file);
}
files.add(file);
}
}
files.sort(Comparator.comparing(path -> path.getFileName().toString()));
return files;
}
private static RegionPosition parseRegionPosition(Path file) throws IOException {
String name = file.getFileName().toString();
Matcher matcher = REGION_FILE.matcher(name);
if (!matcher.matches()) {
throw new IOException("Invalid world region filename: " + file);
}
try {
int regionX = Integer.parseInt(matcher.group(1));
int regionZ = Integer.parseInt(matcher.group(2));
if (!MCAUtil.createNameFromRegionLocation(regionX, regionZ).equals(name)) {
throw new IOException("Noncanonical world region filename: " + file);
}
validateRegionCoordinate(file, regionX);
validateRegionCoordinate(file, regionZ);
return new RegionPosition(regionX, regionZ);
} catch (NumberFormatException error) {
throw new IOException("World region coordinate is outside the supported range: " + file, error);
}
}
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) {
throw new IOException("Truncated world region file: " + file);
}
long sectorCapacity = length / SECTOR_BYTES;
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();
if (location == 0) {
continue;
}
int sectorOffset = location >>> Byte.SIZE;
int sectorCount = location & 0xFF;
validateAllocation(file, sectorOffset, sectorCount, sectorCapacity, claimedSectors);
mask.set(index);
}
}
return mask;
} catch (ArithmeticException error) {
throw new IOException("World region file is too large to scan safely: " + file, error);
}
}
private static void validateAllocation(
Path file,
int sectorOffset,
int sectorCount,
long sectorCapacity,
BitSet claimedSectors
) throws IOException {
long allocationEnd = (long) sectorOffset + sectorCount;
if (sectorOffset < HEADER_SECTORS || sectorCount == 0 || allocationEnd > sectorCapacity) {
throw new IOException("Invalid chunk allocation in world region file: " + file);
}
int overlap = claimedSectors.nextSetBit(sectorOffset);
if (overlap >= 0 && overlap < allocationEnd) {
throw new IOException("Overlapping chunk allocations in world region file: " + file);
}
claimedSectors.set(sectorOffset, Math.toIntExact(allocationEnd));
}
private static void validateRegionCoordinate(Path file, int regionCoordinate) throws IOException {
int minimum = Math.floorDiv(Integer.MIN_VALUE, REGION_SIDE);
int maximum = Math.floorDiv(Integer.MAX_VALUE, REGION_SIDE);
if (regionCoordinate < minimum || regionCoordinate > maximum) {
throw new IOException("World region coordinate is outside the chunk coordinate range: " + file);
}
}
@FunctionalInterface
public interface ChunkConsumer {
void accept(int chunkX, int chunkZ) throws IOException;
}
private record RegionPosition(int x, int z) {
}
}
@@ -15,6 +15,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
public final class HydrologyTile {
private final HydrologyTileKey key;
@@ -172,8 +173,23 @@ public final class HydrologyTile {
int x,
int z,
int maximumDistance
) {
return nearestFeature(types, profileKey, x, z, maximumDistance, feature -> true);
}
public Optional<HydrologyFeatureRef> nearestFeature(
Set<HydrologyFeatureType> types,
String profileKey,
int x,
int z,
int maximumDistance,
Predicate<HydrologyFeatureRef> eligibility
) {
Objects.requireNonNull(types, "types");
Predicate<HydrologyFeatureRef> requiredEligibility = Objects.requireNonNull(
eligibility,
"eligibility"
);
if (maximumDistance < 0) {
throw new IllegalArgumentException("maximumDistance cannot be negative.");
}
@@ -196,7 +212,7 @@ public final class HydrologyTile {
continue;
}
HydrologyFeatureRef feature = publishedCenterlineFeature(segment, point);
if (feature == null) {
if (feature == null || !requiredEligibility.test(feature)) {
continue;
}
if (distance < nearestDistance || nearest == null || feature.id() < nearest.id()) {
@@ -72,6 +72,7 @@ import java.util.Set;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
public final class IrisHydrologyRuntime implements AutoCloseable {
private static final int MAXIMUM_CACHE_TILES = 64;
@@ -351,7 +352,15 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
int z,
int maximumDistance
) {
return nearestFeature(types, profileKey, x, z, maximumDistance, (int visited) -> { });
return nearestFeature(
types,
profileKey,
x,
z,
maximumDistance,
feature -> true,
(int visited) -> { }
);
}
/**
@@ -366,8 +375,32 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
int z,
int maximumDistance,
IntConsumer progress
) {
return nearestFeature(
types,
profileKey,
x,
z,
maximumDistance,
feature -> true,
progress
);
}
public Optional<HydrologyFeatureRef> nearestFeature(
Set<HydrologyFeatureType> types,
String profileKey,
int x,
int z,
int maximumDistance,
Predicate<HydrologyFeatureRef> eligibility,
IntConsumer progress
) {
Objects.requireNonNull(types);
Predicate<HydrologyFeatureRef> requiredEligibility = Objects.requireNonNull(
eligibility,
"eligibility"
);
Objects.requireNonNull(progress);
if (types.isEmpty() || maximumDistance < 0) {
return Optional.empty();
@@ -393,7 +426,13 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
for (HydrologyTile tile : cache.tiles(HydrologyFeatureSearch.ring(originTileX, originTileZ, ring))) {
visited++;
HydrologyFeatureRef feature = tile.nearestFeature(
types, profileKey, x, z, maximumDistance).orElse(null);
types,
profileKey,
x,
z,
maximumDistance,
requiredEligibility
).orElse(null);
if (feature == null) {
continue;
}
@@ -232,16 +232,13 @@ public interface EngineMantle extends MatterGenerator {
} else if (protectUpper) {
int chunkBlockX = x << 4;
int chunkBlockZ = z << 4;
int gap = getEngine().getDimension().getUpperDimensionGap();
int[] upperYs = new int[256];
for (int i = 0; i < 256; i++) {
int lx = i >> 4;
int lz = i & 15;
int worldX = chunkBlockX + lx;
int worldZ = chunkBlockZ + lz;
int he = (int) Math.round(getEngine().getComplex().getHeightStream().get((double) worldX, (double) worldZ));
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
upperYs[i] = Math.max(rawUpper, he + gap);
upperYs[i] = upperCtx.getEffectiveSurfaceY(worldX, worldZ);
}
chunk.iterate(PlatformBlockState.class, (lx, y, lz, value) -> {
int colIdx = (lx << 4) | (lz & 15);
@@ -28,6 +28,8 @@ import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.tools.WorldMaintenance;
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.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.TileData;
@@ -73,6 +75,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
private final int z;
private final int windowSide;
private final AtomicReferenceArray<MantleChunk<Matter>> window;
private final TransitionGenerationPlan transitionGenerationPlan;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
@@ -97,6 +100,8 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
this.radius = accessRadius;
this.x = x;
this.z = z;
IrisComplex complex = engineMantle.getComplex();
this.transitionGenerationPlan = complex == null ? null : complex.getTransitionGenerationPlan();
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
// boxed per-block map lookup on the placement and carve hot paths.
this.windowSide = (this.radius * 2) + 1;
@@ -230,7 +235,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public <T> void setData(int x, int y, int z, T t) {
if (t == null) {
if (t == null || !allowsWrite(x, z)) {
return;
}
@@ -264,7 +269,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public boolean setDataIfAbsent(int x, int y, int z, MatterCavern value) {
if (value == null) {
if (value == null || !allowsWrite(x, z)) {
return false;
}
@@ -305,7 +310,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public boolean carveDataIfAbsent(int x, int y, int z, MatterCavern value) {
if (value == null || y < 0 || y >= mantle.getWorldHeight()) {
if (value == null || !allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return false;
}
@@ -341,7 +346,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public void setForcedCarve(int x, int y, int z, MatterCavern value) {
if (value == null || y < 0 || y >= mantle.getWorldHeight()) {
if (value == null || !allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
@@ -366,7 +371,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public void clearBlock(int x, int y, int z) {
if (y < 0 || y >= mantle.getWorldHeight()) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return;
}
MantleChunk<Matter> chunk = acquireChunk(x >> 4, z >> 4);
@@ -402,6 +407,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return null;
}
if (!allowsWrite(x, z)) {
return getDataIfPresent(x, y, z, type);
}
MantleChunk<Matter> chunk = acquireChunk(cx, cz);
if (chunk == null) {
return null;
@@ -526,7 +535,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
public boolean restorePrerequisiteData(int x, int y, int z, Class<?> type) {
Objects.requireNonNull(type, "type");
if (y < 0 || y >= mantle.getWorldHeight() || !isPreObjectType(type)) {
if (!allowsWrite(x, z)
|| y < 0
|| y >= mantle.getWorldHeight()
|| !isPreObjectType(type)) {
return false;
}
@@ -551,7 +563,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public boolean restorePrerequisiteCell(int x, int y, int z) {
if (y < 0 || y >= mantle.getWorldHeight()) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return false;
}
@@ -584,7 +596,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
public void clearData(int x, int y, int z, Class<?> type) {
if (y < 0 || y >= mantle.getWorldHeight()) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return;
}
@@ -624,7 +636,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
private void setCustomBlock(int x, int y, int z, PlatformBlockState baseState, Identifier identifier) {
if (y < 0 || y >= mantle.getWorldHeight()) {
if (!allowsWrite(x, z) || y < 0 || y >= mantle.getWorldHeight()) {
return;
}
if (y == 0 && engineMantle.getEngine().getDimension().isBedrock()) {
@@ -711,6 +723,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return matter.getSlice(type).get(x, y, z);
}
private boolean allowsWrite(int blockX, int blockZ) {
return transitionGenerationPlan == null
|| !transitionGenerationPlan.isHistoricalBlock(blockX, blockZ);
}
@SuppressWarnings("unchecked")
private static void restoreRaw(Matter matter, int x, int y, int z, Class<?> type, Object value) {
MatterSlice<Object> slice;
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.spi.IrisLogging;
@@ -45,10 +46,23 @@ public interface MatterGenerator {
if (!getEngine().getDimension().isUseMantle()) {
return;
}
generateMatterWindow(x, z, multicore, context);
IrisComplex complex = context == null ? null : context.getComplex();
if (complex == null) {
complex = getEngine().getComplex();
}
if (complex != null && !complex.allowsMantleChunkWrite(x, z)) {
return;
}
generateMatterWindow(x, z, multicore, context, complex);
}
private void generateMatterWindow(int x, int z, boolean multicore, ChunkContext context) {
private void generateMatterWindow(
int x,
int z,
boolean multicore,
ChunkContext context,
IrisComplex complex
) {
MatterGenerationPlan generationPlan = resolveGenerationPlan(x, z, context);
MatterPassPlan[] passPlans = generationPlan.passPlans();
@@ -128,6 +142,11 @@ public interface MatterGenerator {
continue;
}
if (complex != null && !complex.allowsMantleChunkWrite(passX, passZ)) {
partialChunks.add(passKey);
continue;
}
if (partial) {
partialChunks.add(passKey);
}
@@ -207,6 +226,9 @@ public interface MatterGenerator {
if (partialChunks.contains(realKey)) {
continue;
}
if (complex != null && !complex.allowsMantleChunkWrite(realX, realZ)) {
continue;
}
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
@@ -67,7 +68,13 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
return CommitResult.EMPTY;
}
IrisComplex complex = engine == null ? null : engine.getComplex();
for (BufferedMutation mutation : mutations) {
if (complex != null
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
discard();
return CommitResult.REJECTED_TRANSITION;
}
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
discard();
return CommitResult.REJECTED_BOUNDS;
@@ -252,6 +259,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_TRANSITION,
REJECTED_BOUNDS,
REJECTED_HYDROLOGY
}
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.mantle.components;
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.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
@@ -47,7 +48,14 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
discard();
return CommitResult.EMPTY;
}
Engine engine = delegate.getEngine();
IrisComplex complex = engine == null ? null : engine.getComplex();
for (BufferedMutation mutation : mutations) {
if (complex != null
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
discard();
return CommitResult.REJECTED_TRANSITION;
}
if (!delegate.canWriteObjectBlock(mutation.x(), mutation.y(), mutation.z())) {
discard();
return CommitResult.REJECTED_SUPPORT;
@@ -170,6 +178,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
enum CommitResult {
COMMITTED,
EMPTY,
REJECTED_TRANSITION,
REJECTED_SUPPORT
}
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructurePlacementScope;
@@ -76,17 +77,27 @@ public class IrisStructureComponent extends IrisMantleComponent {
@Override
@ChunkCoordinates
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
if (!complex.allowsNewGenerationChunk(x, z)) {
return;
}
for (IrisStructurePlacement placement : StructurePlacementScope.placementsAt(
getEngineMantle().getEngine(), x, z)) {
if (!placement.hasIrisStructures()) {
continue;
}
placeFromPlacement(writer, placement, x, z);
placeFromPlacement(writer, placement, x, z, complex);
}
}
@ChunkCoordinates
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz) {
private void placeFromPlacement(
MantleWriter writer,
IrisStructurePlacement placement,
int cx,
int cz,
IrisComplex complex
) {
IrisStructureLocator.ResolvedPlacement resolved = IrisStructureLocator.resolvePlacement(
getEngineMantle().getEngine(), placement, cx, cz);
if (resolved == null) {
@@ -105,6 +116,19 @@ public class IrisStructureComponent extends IrisMantleComponent {
if (pieces == null) {
return;
}
IrisStructureTerrain terrain = placement.resolvedTerrain();
int[] bounds = computePieceBounds(pieces);
int horizontalPadding = terrain.resolvedMode() == IrisStructureTerrainMode.FORCE_CARVE
|| terrain.resolvedMode() == IrisStructureTerrainMode.BORE
? Math.max(0, terrain.getHorizontalPadding())
: 0;
if (bounds == null || !complex.allowsNewGenerationFootprint(
saturatedOffset(bounds[0], -horizontalPadding),
saturatedOffset(bounds[2], -horizontalPadding),
saturatedOffset(bounds[3], horizontalPadding),
saturatedOffset(bounds[5], horizontalPadding))) {
return;
}
RNG rng = resolved.rng();
int baseY = resolved.baseY();
if (trace) {
@@ -115,7 +139,6 @@ public class IrisStructureComponent extends IrisMantleComponent {
clearIntersectingObjectTrees(writer, resolved);
}
IrisStructureTerrain terrain = placement.resolvedTerrain();
IrisStructureTerrainMode terrainMode = terrain.resolvedMode();
if (terrainMode == IrisStructureTerrainMode.FORCE_CARVE) {
forceCarveStructure(writer, pieces, terrain);
@@ -529,6 +552,11 @@ public class IrisStructureComponent extends IrisMantleComponent {
return new int[]{minX, minY, minZ, maxX, maxY, maxZ};
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private int placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p,
ObjectPlaceMode mode, int y, RNG rng, Long2IntOpenHashMap foundationColumns,
boolean supportNonOccluding) {
@@ -98,6 +98,9 @@ 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)) {
return;
}
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
@@ -121,7 +124,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
if (upperCtx != null && getDimension().isUpperDimensionCarving()) {
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights, fluidSupportPlan);
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, fluidSupportPlan);
}
fluidSupportPlan.resolve(writer.acquireChunk(x, z));
@@ -154,11 +157,10 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private void carveUpperTerrain(UpperDimensionContext upperCtx, List<WeightedProfile> normalProfiles,
MantleWriter writer, int cx, int cz, int[] lowerSurfaceHeights,
MantleWriter writer, int cx, int cz,
CaveFluidSupportPlan fluidSupportPlan) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int worldMinHeight = getEngineMantle().getEngine().getWorld().minHeight();
int gap = getDimension().getUpperDimensionGap();
int baseX = PowerOfTwoCoordinates.chunkToBlock(cx);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(cz);
@@ -167,9 +169,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
int upperY = Math.max(rawUpper, lowerSurfaceHeights[columnIndex] + gap);
int upperY = upperCtx.getEffectiveSurfaceY(worldX, worldZ);
if (upperY < minUpperSurfaceY) {
minUpperSurfaceY = upperY;
}
@@ -65,6 +65,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
if (!complex.allowsNewGenerationChunk(x, z)) {
return;
}
IrisData data = getData();
int minX = x << 4;
int minZ = z << 4;
@@ -201,6 +204,14 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
int xx = minX + (key & 15);
int zz = minZ + (key >> 4);
IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey());
if (!MantleObjectComponent.allowsObjectPlacement(
complex,
obj,
floatingPlacement,
xx,
zz)) {
continue;
}
int id = rng.i(0, Integer.MAX_VALUE);
try {
@@ -279,6 +290,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
anchored.setRotation(ROTATION_NONE);
anchored.setForcePlace(true);
anchored.setBottom(false);
if (!MantleObjectComponent.allowsObjectPlacement(complex, obj, anchored, wx, wz)) {
continue;
}
int yv = pickTopY + 1 - fp.getLowestSolidKeyY();
@@ -385,6 +399,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
inverted.setRotation(invertedRotation);
inverted.setForcePlace(true);
inverted.setBottom(false);
if (!MantleObjectComponent.allowsObjectPlacement(complex, obj, inverted, wx, wz)) {
continue;
}
int yv = invertedBaseY(pickBottomY, fp, invertedRotation);
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
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.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -71,8 +72,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisHydrologyRuntime runtime = context.getComplex().getHydrologyRuntime();
if (runtime == null) {
IrisComplex complex = context.getComplex();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !complex.allowsNewGenerationChunk(chunkX, chunkZ)) {
return;
}
int minimumX = chunkX << 4;
@@ -88,14 +90,15 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
CaveVoxelView caveView = new MantleHydrologyCaveVoxelView(
writer.getMantle(),
writer.getMantle().getWorldHeight(),
(x, z) -> runtime.sample(x, z)
.map(HydrologyColumnSample::terrainHeight)
.orElseGet(() -> (int) Math.round(
context.getComplex().getNaturalHeightStream().getDouble(x, z)
)),
(x, z) -> {
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
return sample == null
? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
: sample.terrainHeight();
},
(inputChunkX, inputChunkZ) -> MantleHydrologyCaveVoxelView.generateCarvingInput(
getEngineMantle(),
context.getComplex(),
complex,
inputChunkX,
inputChunkZ
)
@@ -104,7 +107,7 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
chunkX,
chunkZ,
writer.getMantle().getWorldHeight(),
runtime::sample,
(x, z) -> Optional.ofNullable(complex.sampleHydrologyColumn(x, z)),
cavePlans,
caveView
);
@@ -183,6 +183,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
void generateOrigin(ObjectPassPlacer writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
if (!complex.allowsNewGenerationChunk(x, z)) {
return;
}
boolean traceRegen = isRegenTraceThread();
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
int xxx = 8 + (x << 4);
@@ -585,6 +588,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
return;
}
IrisComplex complex = getComplex();
int blockX = x << 4;
int blockZ = z << 4;
boolean golden = GoldenDebugObjectPlacer.isGoldenDebugChunk(x, z);
@@ -693,6 +697,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
placeResult = contained.resultY();
commitResult = contained.commitResult();
} else {
if (!allowsObjectPlacement(complex, variant, placement, xx, zz)) {
continue;
}
String marker = placementMarker(variant, id, "procedural");
placeResult = variant.place(xx, -1, zz, placer, placement, rng, (b, data) -> {
if (marker != null) {
@@ -813,6 +820,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean treePlacement,
RNG rng
) {
IrisComplex complex = placer.getEngine() == null ? null : placer.getEngine().getComplex();
if (!allowsObjectPlacement(complex, object, placement, x, z)) {
return new ContainedPlacementResult(
-1,
CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION);
}
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(placer, anchorY, minDepthBelowSurface);
int placeY = anchorY;
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
@@ -875,6 +888,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
int xx = rng.i(x, x + 16);
int zz = rng.i(z, z + 16);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
if (!allowsObjectPlacement(complex, v, effectivePlacement, xx, zz)) {
rejected++;
continue;
}
boolean treePlacement = isTreePlacement(v, effectivePlacement);
int id = rng.i(0, Integer.MAX_VALUE);
IObjectPlacer placePlacer = golden ? new GoldenDebugObjectPlacer(writer, scope + "/" + v.getLoadKey()) : writer;
@@ -1186,7 +1203,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
try {
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-biome-surface");
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-biome-surface");
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to place upper-dimension objects in biome " + upperBiome.getName()
@@ -1201,7 +1218,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
try {
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-region-surface");
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-region-surface");
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Failed to place upper-dimension objects in region " + upperRegion.getName()
@@ -1219,14 +1236,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
int chunkZ,
IrisObjectPlacement objectPlacement,
UpperDimensionContext upperCtx,
IrisDimension dimension,
IrisComplex complex,
boolean forcePlace,
boolean traceRegen,
String scope
) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int upperGap = dimension.getUpperDimensionGap();
int minX = chunkX << 4;
int minZ = chunkZ << 4;
int density = objectPlacement.getDensity(rng, minX, minZ, getData());
@@ -1239,9 +1254,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int xx = rng.i(minX, minX + 16);
int zz = rng.i(minZ, minZ + 16);
int columnLowerSurfaceY = getEngineMantle().getEngine().getHeight(xx, zz, true);
int rawUpperSurface = upperCtx.getUpperSurfaceY(xx, zz);
int upperSurfaceY = Math.max(rawUpperSurface, columnLowerSurfaceY + upperGap);
int upperSurfaceY = upperCtx.getEffectiveSurfaceY(xx, zz);
if (upperSurfaceY >= chunkHeight - 2) {
continue;
}
@@ -1260,6 +1273,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (forcePlace) {
placement.setForcePlace(true);
}
if (!allowsObjectPlacement(complex, v, placement, xx, zz)) {
continue;
}
boolean treePlacement = isTreePlacement(v, objectPlacement);
String marker = placementMarker(v, id, "upper");
@@ -1781,6 +1797,31 @@ public class MantleObjectComponent extends IrisMantleComponent {
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
}
static boolean allowsObjectPlacement(
IrisComplex complex,
IrisObject object,
IrisObjectPlacement placement,
int anchorX,
int anchorZ
) {
if (complex == null || object == null) {
return true;
}
int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement);
return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach),
saturatedOffset(anchorX, reach),
saturatedOffset(anchorZ, reach));
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale <= 1D) {
@@ -104,9 +104,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
if (protectUpper) {
int worldX = localX + chunkBlockX;
int worldZ = localZ + chunkBlockZ;
int rawUpper = upperCtx.getUpperSurfaceY(worldX, worldZ);
int gap = getEngine().getDimension().getUpperDimensionGap();
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
upperSurfaceHeights[columnIndex] = upperCtx.getEffectiveSurfaceY(worldX, worldZ);
}
if (stackLayouts != null) {
stackLayouts[columnIndex] = context.getDimensionStackLayout(localX, localZ);
@@ -286,15 +286,17 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
}
}
writeIslandSkyBiomes(
parent,
wx,
wz,
sample,
chunkHeight,
data,
context.getDimensionStackLayout(xf, zf)
);
if (complex.allowsNewDiscreteContentAt(wx, wz)) {
writeIslandSkyBiomes(
parent,
wx,
wz,
sample,
chunkHeight,
data,
context.getDimensionStackLayout(xf, zf)
);
}
}
}
@@ -312,6 +314,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
for (int zf = 0; zf < 16; zf++) {
int wx = x + xf;
int wz = z + zf;
if (!complex.allowsNewDiscreteContentAt(wx, wz)) {
continue;
}
IrisBiome parent = boundarySampler.parent(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
@@ -422,8 +427,11 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(
getDimension().getCustomBiomeKey(custom.getId())));
String resourceKey = getEngine().getData().customBiomeResourceKey(
getEngine().getDimension(),
custom
);
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(resourceKey));
}
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
@@ -114,30 +114,34 @@ final class IrisBiomeDerivatives {
static String getSkyBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) {
KList<String> biomeSkyScatter = biome.getBiomeSkyScatter();
String logicalKey;
if (biomeSkyScatter.size() == 1) {
return namespacedBiomeKey(biomeSkyScatter.get(0));
}
if (biomeSkyScatter.isEmpty()) {
logicalKey = namespacedBiomeKey(biomeSkyScatter.get(0));
} else if (biomeSkyScatter.isEmpty()) {
return getGroundBiomeKey(biome, rng, engine, x, y, z);
} else {
logicalKey = namespacedBiomeKey(biomeSkyScatter.get(
biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)
));
}
return namespacedBiomeKey(biomeSkyScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeSkyScatter.size() - 1, x, y, z)));
return physicalBiomeKey(engine, logicalKey);
}
static String getGroundBiomeKey(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) {
KList<String> biomeScatter = biome.getBiomeScatter();
String logicalKey;
if (biomeScatter.isEmpty()) {
return biome.getDerivativeKey();
logicalKey = biome.getDerivativeKey();
} else if (biomeScatter.size() == 1) {
logicalKey = namespacedBiomeKey(biomeScatter.get(0));
} else {
logicalKey = namespacedBiomeKey(biomeScatter.get(
biome.getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z)
));
}
if (biomeScatter.size() == 1) {
return namespacedBiomeKey(biomeScatter.get(0));
}
return namespacedBiomeKey(biomeScatter.get(biome.getBiomeGenerator(rng, engine).fit(0, biomeScatter.size() - 1, x, y, z)));
return physicalBiomeKey(engine, logicalKey);
}
static IrisBiomeCustom getCustomBiome(IrisBiome biome, RNG rng, Engine engine, double x, double y, double z) {
@@ -149,4 +153,11 @@ final class IrisBiomeDerivatives {
return customDerivitives.get(biome.getBiomeGenerator(rng, engine).fit(0, customDerivitives.size() - 1, x, y, z));
}
private static String physicalBiomeKey(Engine engine, String logicalKey) {
if (engine == null || logicalKey == null) {
return logicalKey;
}
return engine.getData().physicalBiomeResourceKey(engine.getDimension(), logicalKey);
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@FunctionalInterface
public interface IrisCustomBiomeAliasResolver {
Collection<String> aliases(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String physicalResourceKey
);
default String physicalResourceKey(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String currentPhysicalResourceKey
) throws IOException {
return currentPhysicalResourceKey;
}
default String generatedSource(
String registryKey,
String resourceKey,
String currentSource,
IDataFixer fixer
) throws IOException {
return currentSource;
}
static IrisCustomBiomeAliasResolver none() {
return (dimension, biome, customBiome, physicalResourceKey) -> List.of();
}
}
@@ -30,6 +30,7 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -64,6 +65,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
@@ -71,6 +73,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
@@ -730,22 +733,21 @@ public class IrisDimension extends IrisRegistrant {
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
String keyPrefix = getCustomBiomeKeyPrefix();
int separator = keyPrefix.indexOf(':');
String namespace = keyPrefix.substring(0, separator);
String pathPrefix = keyPrefix.substring(separator + 1);
if (pathPrefix.endsWith("/")) {
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
}
installBiomes(fixer, data, datapackRoots, namespace, pathPrefix, biomes);
installBiomes(fixer, data, datapackRoots, biomes, IrisCustomBiomeAliasResolver.none());
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
String namespace, String pathPrefix, KSet<String> biomes) throws IOException {
public void installBiomes(
IDataFixer fixer,
DataProvider data,
KList<File> datapackRoots,
KSet<String> biomes,
IrisCustomBiomeAliasResolver aliasResolver
) throws IOException {
// Tag membership is accumulated in memory and flushed once per tag at the end of the walk. Writing a
// tag file per (biome, tag) pair made staging quadratic: every write re-read, re-parsed and re-emitted
// a file that grows with every biome added to that tag.
KMap<String, KSet<String>> tagMembership = new KMap<>();
IrisData packData = Objects.requireNonNull(data.getData(), "Iris biome datapack data");
for (IrisBiome irisBiome : getAllBiomes(data)) {
if (!irisBiome.isCustom()) {
@@ -758,32 +760,58 @@ public class IrisDimension extends IrisRegistrant {
// comes from the chunk generator's generation-settings getter, not from the datapack.
String derivativeKey = irisBiome.getVanillaDerivativeKey();
ContentGate contentGate = getLoader() == null ? null : getLoader().getContentGate();
ContentGate contentGate = packData.getContentGate();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
String json = customBiome.generateJson(fixer, contentGate);
String currentPhysicalKey = GenerationRegistryContractFactory.customBiomeResourceKey(
getLoadKey(),
customBiome,
contentGate
);
String physicalKey = aliasResolver.physicalResourceKey(
this,
irisBiome,
customBiome,
currentPhysicalKey
);
String logicalKey = getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
synchronized (biomes) {
if (!biomes.add(customBiomePath)) {
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue;
biomes.add(physicalKey);
}
LinkedHashSet<String> resourceKeys = new LinkedHashSet<>();
resourceKeys.add(physicalKey);
Collection<String> aliases = aliasResolver.aliases(this, irisBiome, customBiome, physicalKey);
if (aliases == null) {
throw new IOException("Custom biome alias resolver returned null for '" + logicalKey + "'.");
}
if (!aliases.isEmpty()
&& physicalKey.equals(packData.customBiomeResourceKey(this, customBiome.getId()))) {
resourceKeys.addAll(aliases);
}
for (String resourceKey : resourceKeys) {
String normalizedKey = normalizeResourceKey(resourceKey);
if (normalizedKey == null) {
throw new IOException("Invalid custom biome resource key '" + resourceKey + "'.");
}
String emittedJson = aliasResolver.generatedSource(
GenerationRegistryContractFactory.BIOME_REGISTRY,
normalizedKey,
json,
fixer
);
for (File datapackRoot : datapackRoots) {
writeCustomBiomeDefinition(datapackRoot.toPath(), normalizedKey, emittedJson);
}
collectBiomeTags(
tagMembership,
normalizedKey,
customBiome.getEffectiveTags(derivativeKey)
);
}
String biomePath = pathPrefix.isBlank()
? customBiomePath
: pathPrefix + "/" + customBiomePath;
for (File datapackRoot : datapackRoots) {
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
IrisLogging.debug(" Installing Data Pack Biome: " + output.getPath());
output.getParentFile().mkdirs();
IO.writeAll(output, json);
}
collectBiomeTags(tagMembership, namespace + ":" + biomePath,
customBiome.getEffectiveTags(derivativeKey));
}
}
@@ -792,6 +820,39 @@ public class IrisDimension extends IrisRegistrant {
}
}
private static void writeCustomBiomeDefinition(
Path datapackRoot,
String resourceKey,
String json
) throws IOException {
int separator = resourceKey.indexOf(':');
String namespace = resourceKey.substring(0, separator);
String path = resourceKey.substring(separator + 1);
Path biomeRoot = datapackRoot.resolve("data")
.resolve(namespace)
.resolve("worldgen")
.resolve("biome")
.toAbsolutePath()
.normalize();
Path output = biomeRoot.resolve(path + ".json").normalize();
if (!output.startsWith(biomeRoot)) {
throw new IOException("Unsafe custom biome resource key '" + resourceKey + "'.");
}
if (Files.isRegularFile(output)) {
String existing = Files.readString(output, StandardCharsets.UTF_8);
String existingFingerprint = GenerationRegistryContractFactory
.fingerprintCustomBiomeDefinition(existing);
String candidateFingerprint = GenerationRegistryContractFactory
.fingerprintCustomBiomeDefinition(json);
if (!existingFingerprint.equals(candidateFingerprint)) {
throw new IOException("Conflicting custom biome output for '" + resourceKey + "'.");
}
return;
}
IrisLogging.debug(" Installing Data Pack Biome: " + output);
writeAtomic(output, json);
}
public static void clearGeneratedBiomeTags(KList<File> datapackRoots) {
for (File datapackRoot : datapackRoots) {
File dataFolder = new File(datapackRoot, "data");
@@ -993,11 +1054,25 @@ public class IrisDimension extends IrisRegistrant {
}
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
String dimensionTypeKey = getDimensionTypeKey();
installDimensionType(fixer, datapackRoots, IrisCustomBiomeAliasResolver.none());
}
IrisLogging.debug(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"');
public void installDimensionType(
IDataFixer fixer,
KList<File> datapackRoots,
IrisCustomBiomeAliasResolver sourceResolver
) throws IOException {
IrisDimensionType type = getDimensionType();
String dimensionTypeKey = getDimensionTypeKey();
String resourceKey = "iris:" + dimensionTypeKey;
String json = Objects.requireNonNull(sourceResolver, "sourceResolver").generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
resourceKey,
type.toJson(fixer),
fixer
);
IrisLogging.debug(" Installing Data Pack Dimension Type: \"" + resourceKey + '"');
for (File datapackRoot : datapackRoots) {
File output = new File(datapackRoot, "data/iris/dimension_type/" + dimensionTypeKey + ".json");
output.getParentFile().mkdirs();
@@ -45,6 +45,12 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.object.IrisDimensionContractException;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
@@ -84,7 +90,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -111,6 +119,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final IrisWorld world;
private final File dataLocation;
private final String dimensionKey;
private final @Nullable GenerationHistory generationHistory;
private final ReactiveFolder folder;
private final ReentrantLock lock = new ReentrantLock();
private final KList<BlockPopulator> populators;
@@ -137,7 +146,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Setter
private volatile StudioGenerator studioGenerator;
public BukkitChunkGenerator(IrisWorld world, boolean studio, File dataLocation, String dimensionKey) {
public BukkitChunkGenerator(
IrisWorld world,
boolean studio,
File dataLocation,
String dimensionKey,
@Nullable GenerationHistory generationHistory
) {
setup = new AtomicBoolean(false);
studioGenerator = null;
dummyBiomeProvider = new DummyBiomeProvider();
@@ -149,6 +164,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
this.studioEntryBootstrapActive = new AtomicBoolean(studio);
this.dataLocation = dataLocation;
this.dimensionKey = dimensionKey;
this.generationHistory = generationHistory;
this.folder = new ReactiveFolder(
dataLocation,
(_a, _b, _c) -> hotloadFromWatcher(),
@@ -385,9 +401,37 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
jigsawStudioActive = request != null
&& session != null
&& session.sessionId().equals(request.requestId());
IrisEngine createdEngine = new IrisEngine(
engineTarget,
selectInitializationMode(studio, jigsawStudioActive));
IrisEngine createdEngine = createEngine(engineTarget);
if (generationHistory != null) {
long initialActivationId = generationHistory.activeActivation().activationId();
int transitionWidthBlocks = IrisSettings.get()
.getGenerator()
.getGenerationTransitionWidthBlocks();
try {
createdEngine.attachGenerationHistory(
generationHistory,
IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks);
if (generationHistory.activeActivation().activationId() != initialActivationId) {
createdEngine.close();
targetCache.reset();
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
createdEngine.attachGenerationHistory(
generationHistory,
IrisBoundarySignatureSampler.INSTANCE,
transitionWidthBlocks);
}
} catch (Throwable failure) {
try {
createdEngine.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
throw propagateEngineSetupFailure(failure);
}
}
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
jigsawStudioActive,
studioEntryBootstrapActive.get(),
@@ -397,6 +441,66 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
targetCache.reset();
}
private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
Path packRoot = generationHistory.activePackRoot();
IrisData data = IrisData.openRuntime(packRoot.toFile());
try {
data.dump();
data.clearLists();
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IOException("Active Iris generation pack does not contain dimension '"
+ dimensionKey + "': " + packRoot);
}
return new EngineTarget(world, dimension, data);
} catch (Throwable failure) {
if (!data.isClosed()) {
data.close();
}
if (failure instanceof IOException ioFailure) {
throw ioFailure;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
if (failure instanceof Error error) {
throw error;
}
throw new IOException("Unable to load the active Iris generation pack.", failure);
}
}
private IrisEngine createEngine(EngineTarget engineTarget) {
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive);
if (generationHistory == null) {
return new IrisEngine(engineTarget, mode);
}
GenerationActivation active = generationHistory.activeActivation();
GenerationEpoch epoch = generationHistory.activeEpoch();
TransitionGenerationPlan plan;
try {
plan = active.isInitial() ? null : generationHistory.transitionPlan(active.activationId());
} catch (IOException failure) {
throw new IllegalStateException("Unable to load the active Iris generation transition.", failure);
}
return new IrisEngine(
engineTarget,
mode,
generationHistory.paths().activationMantleRoot(active.activationId()),
epoch.kernelVersion(),
plan);
}
private static RuntimeException propagateEngineSetupFailure(Throwable failure) {
if (failure instanceof RuntimeException runtimeFailure) {
return runtimeFailure;
}
if (failure instanceof Error error) {
throw error;
}
return new IllegalStateException("Unable to attach Iris generation history.", failure);
}
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive) {
if (!studio) {
return IrisEngine.InitializationMode.RUNTIME;
@@ -882,10 +986,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} else {
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, blocks, biomes, engine.shouldGenerateMulticore());
blocks.apply();
if (historyScope != null) {
historyScope.claimGeneratedSemantics();
}
}
}
@@ -909,6 +1018,23 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
private @Nullable GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
Engine currentEngine,
int blockX,
int blockZ
) throws IOException {
if (generationHistory == null) {
return null;
}
if (!(currentEngine instanceof IrisEngine irisEngine)) {
throw new IllegalStateException("Persistent Iris generation requires an IrisEngine runtime.");
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter()
.orElseThrow(() -> new IllegalStateException(
"Persistent Iris generation has no generation-history runtime router."));
return router.openCoordinateScope(blockX, blockZ);
}
private boolean isExpectedTeardown(Engine currentEngine, Throwable throwable) {
if (throwable instanceof GenerationSessionException generationSessionException && generationSessionException.isExpectedTeardown()) {
return true;
@@ -967,10 +1093,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
default -> false;
};
try (GenerationSessionLease lease = currentEngine.acquireGenerationLease("bukkit_base_height");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(currentEngine, x, z);
GenerationSessionLease lease = currentEngine.acquireGenerationLease("bukkit_base_height");
IrisContext.Scope ignored = IrisContext.open(currentEngine, lease.sessionId(), null)) {
return currentEngine.getMinHeight() + currentEngine.getHeight(x, z, ignoreFluid) + 1;
} catch (GenerationSessionException e) {
} catch (GenerationSessionException | IOException e) {
throw new IllegalStateException("Iris base height query was rejected for world '"
+ worldInfo.getName() + "'.", e);
}
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.platform;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.events.IrisLootEvent;
import art.arcane.iris.core.link.Identifier;
@@ -581,7 +583,7 @@ public final class EngineBukkitOps {
}
public static void gotoRegion(Engine engine, IrisRegion r, Player player, boolean teleport) {
if (!engine.getDimension().getRegions().contains(r.getLoadKey())) {
if (GenerationFindCatalog.region(engine, r.getLoadKey()) == null) {
ComponentMessenger.sendSection(player, IrisLanguage.text(BukkitRuntimeMessages.ENGINE_BUKKIT_OPS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("name", String.valueOf(r.getName()))));
return;
}
@@ -28,6 +28,7 @@ import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformNumericRange;
import art.arcane.iris.spi.PlatformRegistries;
@@ -54,6 +55,11 @@ import java.util.Map;
* Bukkit adapter resolving namespaced keys against the live Bukkit registries.
*/
public final class BukkitRegistries implements PlatformRegistries {
@Override
public PlatformGenerationRegistry generationRegistry() {
return INMS.get().generationRegistry();
}
@Override
public PlatformBlockState block(String key) {
BlockData data = BukkitBlockResolution.get(key);
@@ -0,0 +1,13 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.volmlib.util.collection.KList;
public class LocatableObjectHandler extends ObjectHandler {
@Override
public KList<String> getPossibilities() {
Engine activeEngine = engine();
return activeEngine == null ? super.getPossibilities() : GenerationFindCatalog.objectKeys(activeEngine);
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.volmlib.util.collection.KList;
@@ -14,7 +15,7 @@ public class ReachableBiomeHandler extends RegistrantHandler<IrisBiome> {
Engine activeEngine = engine();
return activeEngine == null
? super.getPossibilities()
: activeEngine.getAllBiomes();
: GenerationFindCatalog.biomes(activeEngine);
}
@Override
@@ -0,0 +1,23 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
public class ReachableRegionHandler extends RegistrantHandler<IrisRegion> {
public ReachableRegionHandler() {
super(IrisRegion.class, true);
}
@Override
public KList<IrisRegion> getPossibilities() {
Engine activeEngine = engine();
return activeEngine == null ? super.getPossibilities() : GenerationFindCatalog.regions(activeEngine);
}
@Override
public String getRandomDefault() {
return "region";
}
}
@@ -22,6 +22,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.spi.IrisPlatforms;
@@ -67,6 +68,9 @@ public class StructureHandler implements DirectorParameterHandler<String> {
}
}
for (String key : GenerationFindCatalog.retainedStructureKeys(activeEngine)) {
suggestions.putIfAbsent(normalizeKey(key), key);
}
return new KList<>(suggestions.values());
}
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more