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

@@ -1,6 +1,11 @@
package art.arcane.iris.core;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
@@ -13,6 +18,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@@ -201,12 +207,102 @@ public class IrisDatapackCompilerInputFingerprintTest {
nestedKeyPack.toAbsolutePath().normalize().toFile()), compilerRoots);
}
@Test
public void worldInputRootDiscoveryIncludesEveryRetainedGenerationEpoch() throws Exception {
Path dataDirectory = tmp.newFolder("history-root-data").toPath();
Path serverRoot = tmp.newFolder("history-root-server").toPath();
Path world = serverRoot.resolve("dimensions/iris/history");
Files.createDirectories(world);
Path firstPack = activePack("history-pack-first");
Path secondPack = activePack("history-pack-second");
write(secondPack.resolve("biomes/second.json"), "second-biome");
GenerationHistory history = GenerationHistory.create(
world,
firstPack,
GenerationPackFingerprint.compute(firstPack, GenerationPackFingerprint.CURRENT_VERSION),
41L,
generationContract(),
GenerationRegistryContract.empty()
);
history.stageUpdate(
secondPack,
GenerationPackFingerprint.compute(secondPack, GenerationPackFingerprint.CURRENT_VERSION),
generationContract(),
GenerationRegistryContract.empty(),
256
);
Path legacyDecoy = world.resolve("iris/pack");
write(legacyDecoy.resolve("dimensions/legacy.json"), "legacy");
List<File> compilerRoots = IrisDatapackCompiler.collectCompilerInputRoots(dataDirectory, serverRoot);
Set<File> expected = Set.of(
history.paths().packRoot(history.manifest().activeEpoch().epochId()).toFile(),
history.paths().packRoot(history.manifest().pendingEpoch().orElseThrow().epochId()).toFile()
);
assertEquals(expected, Set.copyOf(compilerRoots));
assertEquals(2, compilerRoots.size());
}
@Test
public void corruptGenerationHistoryFailsPackRootDiscoveryClosed() throws Exception {
Path dataDirectory = tmp.newFolder("corrupt-history-data").toPath();
Path serverRoot = tmp.newFolder("corrupt-history-server").toPath();
Files.createDirectories(serverRoot.resolve("dimensions/iris/corrupt/iris/generation"));
try {
IrisDatapackCompiler.collectCompilerInputRoots(dataDirectory, serverRoot);
fail("Generation history without a manifest must fail pack discovery");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("manifest"));
}
}
@Test
public void rootWorldGenerationHistoryIsACompilerInput() throws Exception {
Path dataDirectory = tmp.newFolder("root-history-data").toPath();
Path serverRoot = tmp.newFolder("root-history-server").toPath();
Path pack = activePack("root-history-pack");
GenerationHistory history = GenerationHistory.create(
serverRoot,
pack,
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION),
91L,
generationContract(),
GenerationRegistryContract.empty()
);
List<File> compilerRoots = IrisDatapackCompiler.collectCompilerInputRoots(dataDirectory, serverRoot);
assertEquals(List.of(history.activePackRoot().toFile()), compilerRoots);
}
private Path activePack(String name) throws IOException {
Path pack = tmp.newFolder(name).toPath();
write(pack.resolve("dimensions/overworld.json"), "dimension-a");
return pack;
}
private static GenerationEpoch.DimensionContract generationContract() {
return new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"c".repeat(64)
);
}
private Path write(Path path, String value) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, value, StandardCharsets.UTF_8);
@@ -1,10 +1,22 @@
package art.arcane.iris.core;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.core.pack.DefaultPackBootstrapProvisioner;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
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.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Rule;
@@ -19,12 +31,15 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDatapackCompilerTest {
@Rule
@@ -60,8 +75,9 @@ public class IrisDatapackCompilerTest {
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/iris/dimension_type/alpha.json")));
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/iris/dimension_type/beta.json")));
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/iris/dimension_type/world_local.json")));
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/alpha/worldgen/biome/alpha_custom.json")));
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/beta/worldgen/biome/beta_custom.json")));
assertEquals(3, regularFileCount(datapackRoot.resolve("data/iris/worldgen/biome/biomes")));
assertFalse(Files.exists(datapackRoot.resolve("data/alpha/worldgen/biome/alpha_custom.json")));
assertFalse(Files.exists(datapackRoot.resolve("data/beta/worldgen/biome/beta_custom.json")));
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/world_local/worldgen/biome/world_custom.json")));
assertFalse(Files.exists(datapackRoot.resolve("data/iris/dimension_type/hidden.json")));
assertFalse(Files.exists(datapackRoot.resolve("data/iris/dimension_type/deleted.json")));
@@ -91,7 +107,7 @@ public class IrisDatapackCompilerTest {
}
@Test
public void compilesRecursiveDimensionBiomeToCanonicalRegistryPath() throws Exception {
public void compilesRecursiveDimensionBiomeToContentAddressedRegistryPath() throws Exception {
Path packRoot = temporaryFolder.newFolder("recursive-dimension-pack").toPath();
Path datapackRoot = temporaryFolder.newFolder("recursive-dimension-datapack").toPath();
createPack(packRoot, "layers/sky", "Aurora");
@@ -104,7 +120,9 @@ public class IrisDatapackCompilerTest {
false
);
assertTrue(Files.isRegularFile(
assertEquals(1, regularFileCount(
datapackRoot.resolve("data/iris/worldgen/biome/biomes")));
assertFalse(Files.exists(
datapackRoot.resolve("data/layers/worldgen/biome/sky/aurora.json")));
assertTrue(Files.isRegularFile(
datapackRoot.resolve("data/iris/dimension_type/layers_sky.json")));
@@ -316,10 +334,22 @@ public class IrisDatapackCompilerTest {
new DataFixerV1217()
);
IrisBiomeCustom customBiome = data.getBiomeLoader().load("test")
.getCustomDerivitives().getFirst();
String hostBiomeKey = GenerationRegistryContractFactory.customBiomeResourceKey(
"host",
customBiome,
data.getContentGate()
);
String sourceBiomeKey = GenerationRegistryContractFactory.customBiomeResourceKey(
"layers/source",
customBiome,
data.getContentGate()
);
assertTrue(requirements.containsKey("dimension_type/iris:host"));
assertTrue(requirements.containsKey("dimension_type/iris:layers_source"));
assertTrue(requirements.containsKey("worldgen/biome/host:stack_custom"));
assertTrue(requirements.containsKey("worldgen/biome/layers:source/stack_custom"));
assertTrue(requirements.containsKey("worldgen/biome/" + hostBiomeKey));
assertTrue(requirements.containsKey("worldgen/biome/" + sourceBiomeKey));
} finally {
data.close();
}
@@ -386,10 +416,200 @@ public class IrisDatapackCompilerTest {
assertTrue(Files.isRegularFile(datapackRoot.resolve("data/iris/dimension/moon.json")));
}
@Test
public void changedCustomBiomeDefinitionsCoexistUnderContentKeys() throws Exception {
Path firstPack = temporaryFolder.newFolder("content-biome-first").toPath();
Path secondPack = temporaryFolder.newFolder("content-biome-second").toPath();
Path datapackRoot = temporaryFolder.newFolder("content-biome-datapack").toPath();
createPack(firstPack, "shared", "shared_custom");
createPack(secondPack, "shared", "shared_custom");
setCustomBiomeTemperature(secondPack, 1.25);
IrisDatapackCompiler.compile(
List.of(firstPack.toFile(), secondPack.toFile()),
new KList<File>().qadd(datapackRoot.toFile()),
List.of(),
new DataFixerV1217(),
false
);
assertEquals(2, regularFileCount(datapackRoot.resolve("data/iris/worldgen/biome/biomes")));
assertFalse(Files.exists(datapackRoot.resolve("data/shared/worldgen/biome/shared_custom.json")));
}
@Test
public void retainedPacksCompileBeforeAPlatformIsBound() throws Exception {
assertRetainedPackCompilesBeforePlatformBinding(false);
}
@Test
public void retainedMultiDimensionPacksCompileAndBootstrapBeforePlatformBinding() throws Exception {
assertRetainedPackCompilesBeforePlatformBinding(true);
}
private void assertRetainedPackCompilesBeforePlatformBinding(boolean multipleDimensions) throws Exception {
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
Path pack = temporaryFolder.newFolder("unbound-retained-pack").toPath();
Path server = temporaryFolder.newFolder("unbound-retained-server").toPath();
Path world = server.resolve("world/dimensions/iris/history");
Files.createDirectories(world);
Path output = temporaryFolder.newFolder("unbound-retained-output").toPath();
createPack(pack, "overworld", "retained_custom");
if (multipleDimensions) {
createPack(pack, "upper", "retained_custom", "NETHER");
}
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load("overworld");
String fingerprint = GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.blockTypeKeys()).thenReturn(List.of());
when(registries.entityKeys()).thenReturn(List.of());
PlatformGenerationRegistry registry = new BootstrapGenerationRegistry();
GenerationRegistryContract contract = GenerationRegistryContractFactory.create(
data, dimension, fingerprint, new DataFixerV1217(), registries, registry);
GenerationHistory history = GenerationHistory.create(world, pack, fingerprint, 77L,
GenerationEpochContractFactory.create(dimension, "overworld", "iris:overworld"), contract);
Path retainedPack = history.paths().packRoot(history.manifest().activeEpoch().epochId());
IrisDatapackCompiler.CompilationResult compiled = IrisDatapackCompiler.compile(List.of(retainedPack.toFile()),
new KList<File>().qadd(output.toFile()), List.of(), new DataFixerV1217(), false);
assertFalse(IrisPlatforms.isBound());
int expectedDimensions = multipleDimensions ? 2 : 1;
assertEquals(expectedDimensions, compiled.dimensionCount());
assertEquals(expectedDimensions, regularFileCount(output.resolve("data/iris/worldgen/biome/biomes")));
Map<String, String> requirements = IrisDatapackCompiler.computeRegistryRequirements(
List.of(retainedPack.toFile()), new DataFixerV1217());
assertTrue(requirements.containsKey("dimension_type/iris:overworld"));
if (multipleDimensions) {
assertTrue(requirements.containsKey("dimension_type/iris:upper"));
assertTrue(Files.isRegularFile(output.resolve("data/iris/dimension_type/upper.json")));
assertNotEquals(Files.readString(output.resolve("data/iris/dimension_type/overworld.json")),
Files.readString(output.resolve("data/iris/dimension_type/upper.json")));
Path dataDirectory = server.resolve("plugins/Iris");
BukkitStartupPaths startupPaths = BukkitStartupPaths.resolve(server, new String[0]);
DefaultPackBootstrapProvisioner.ProvisionResult installed =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/iris/dimension_type/upper.json")));
assertEquals(2, regularFileCount(installed.datapackRoot().resolve("data/iris/worldgen/biome/biomes")));
DefaultPackBootstrapProvisioner.ProvisionResult unchanged =
DefaultPackBootstrapProvisioner.provision(dataDirectory, ignored -> {
}, startupPaths);
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UNCHANGED, unchanged.status());
assertFalse(IrisPlatforms.isBound());
}
} finally {
data.close();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
}
@Test
public void duplicateCustomIdsPreservePhysicalBiomesAndDeterministicLegacyAlias() throws Exception {
Path pack = temporaryFolder.newFolder("duplicate-custom-biomes").toPath().resolve("iris/pack");
Path output = temporaryFolder.newFolder("duplicate-custom-output").toPath();
createPack(pack, "overworld", "savanna_plateau");
Path secondBiome = pack.resolve("biomes/second.json");
Files.writeString(secondBiome, """
{"name":"Second Plateau","derivative":"minecraft:plains",
"customDerivitives":[{"id":"savanna_plateau","grassColor":"#BFB755"}]}
""", StandardCharsets.UTF_8);
IrisDatapackCompiler.CompilationResult compiled = IrisDatapackCompiler.compile(
List.of(pack.toFile()), new KList<File>().qadd(output.toFile()),
List.of(), new DataFixerV1217(), false);
assertEquals(2, compiled.biomeCount());
assertEquals(2, regularFileCount(output.resolve("data/iris/worldgen/biome/biomes")));
Map<String, String> requirements = IrisDatapackCompiler.computeRegistryRequirements(
List.of(pack.toFile()), new DataFixerV1217());
assertTrue(requirements.containsKey("worldgen/biome/overworld:savanna_plateau"));
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load("overworld");
IrisBiomeCustom first = data.getBiomeLoader().load("test").getCustomDerivitives().getFirst();
IrisBiomeCustom second = data.getBiomeLoader().load("second").getCustomDerivitives().getFirst();
String firstKey = data.customBiomeResourceKey(dimension, first);
String secondKey = data.customBiomeResourceKey(dimension, second);
assertNotEquals(firstKey, secondKey);
String selected = firstKey.compareTo(secondKey) < 0 ? firstKey : secondKey;
assertEquals(selected, data.customBiomeResourceKey(dimension, "savanna_plateau"));
Path physicalFile = output.resolve("data/iris/worldgen/biome/" + selected.substring("iris:".length()) + ".json");
assertEquals(Files.readString(physicalFile),
Files.readString(output.resolve("data/overworld/worldgen/biome/savanna_plateau.json")));
} finally {
data.close();
}
}
@Test
public void sharedPhysicalBiomesMergeTagRequirementsAcrossPacks() throws Exception {
Path first = temporaryFolder.newFolder("biome-tags-first").toPath();
Path second = temporaryFolder.newFolder("biome-tags-second").toPath();
Path output = temporaryFolder.newFolder("biome-tags-output").toPath();
createPack(first, "shared", "shared_custom");
createPack(second, "shared", "shared_custom");
Path changed = second.resolve("biomes/test.json");
Files.writeString(changed, Files.readString(changed).replace(
"\"id\": \"shared_custom\"",
"\"id\": \"shared_custom\", \"tags\": [\"example:second\"]"));
Map<String, String> requirements = IrisDatapackCompiler.computeRegistryRequirements(
List.of(first.toFile(), second.toFile()), new DataFixerV1217());
Map<String, String> reversed = IrisDatapackCompiler.computeRegistryRequirements(
List.of(second.toFile(), first.toFile()), new DataFixerV1217());
assertEquals(requirements, reversed);
IrisDatapackCompiler.compile(List.of(first.toFile(), second.toFile()),
new KList<File>().qadd(output.toFile()), List.of(), new DataFixerV1217(), false);
assertEquals(1, regularFileCount(output.resolve("data/iris/worldgen/biome/biomes")));
assertTrue(Files.isRegularFile(output.resolve("data/example/tags/worldgen/biome/second.json")));
}
@Test
public void conflictingLegacyBiomeAliasesFailClosed() throws Exception {
Path firstPack = temporaryFolder.newFolder("legacy-biome-first").toPath().resolve("iris/pack");
Path secondPack = temporaryFolder.newFolder("legacy-biome-second").toPath().resolve("iris/pack");
Path datapackRoot = temporaryFolder.newFolder("legacy-biome-datapack").toPath();
createPack(firstPack, "shared", "shared_custom");
createPack(secondPack, "shared", "shared_custom");
setCustomBiomeTemperature(secondPack, 1.25);
IOException failure = assertThrows(
IOException.class,
() -> IrisDatapackCompiler.compile(
List.of(firstPack.toFile(), secondPack.toFile()),
new KList<File>().qadd(datapackRoot.toFile()),
List.of(),
new DataFixerV1217(),
false
)
);
assertTrue(failure.getMessage().contains("Conflicting custom biome output"));
}
private static void createPack(Path root, String dimensionKey, String biomeId) throws Exception {
createPack(root, dimensionKey, biomeId, "NORMAL");
}
private static long regularFileCount(Path root) throws IOException {
try (Stream<Path> files = Files.list(root)) {
return files.filter(Files::isRegularFile).count();
}
}
private static void setCustomBiomeTemperature(Path packRoot, double temperature) throws IOException {
Path biome = packRoot.resolve("biomes/test.json");
String source = Files.readString(biome, StandardCharsets.UTF_8);
Files.writeString(
biome,
source.replace("\"id\": \"shared_custom\"",
"\"id\": \"shared_custom\", \"temperature\": " + temperature),
StandardCharsets.UTF_8
);
}
private static void createPack(
Path root,
String dimensionKey,
@@ -439,4 +659,26 @@ public class IrisDatapackCompilerTest {
dimension
);
}
private static final class BootstrapGenerationRegistry implements PlatformGenerationRegistry {
@Override
public String runtimeIdentity() {
return "bootstrap-test";
}
@Override
public String generatedDefinitionRendererIdentity() {
return "bootstrap-renderer-v1";
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + dimensionTypeKey;
}
@Override
public Definition definition(String registryKey, String resourceKey) {
return Definition.resourceIdentity(registryKey, resourceKey);
}
}
}
@@ -40,6 +40,17 @@ public class IrisSettingsDefaultsTest {
assertFalse(settings.isAutoImportDatapackStructures());
}
@Test
public void generationTransitionWidthHasSafeBounds() {
IrisSettings.IrisSettingsGenerator settings = new IrisSettings.IrisSettingsGenerator();
assertEquals(256, settings.getGenerationTransitionWidthBlocks());
settings.setGenerationTransitionWidthBlocks(1);
assertEquals(16, settings.getGenerationTransitionWidthBlocks());
settings.setGenerationTransitionWidthBlocks(20_000);
assertEquals(8_192, settings.getGenerationTransitionWidthBlocks());
}
@Test
public void hotloadSnapshotIsValidatedBeforeReplacingLiveSettings() {
IrisSettings previous = IrisSettings.settings;
@@ -1,11 +1,17 @@
package art.arcane.iris.core;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import org.bukkit.NamespacedKey;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -406,26 +412,34 @@ public class IrisWorldStorageTest {
}
@Test
public void frozenPackRootRequiresRealWorldLocalSnapshot() throws Exception {
File dimensionRoot = temporaryFolder.newFolder("frozen-pack-world");
public void activeGenerationPackRequiresValidImmutableHistory() throws Exception {
File dimensionRoot = temporaryFolder.newFolder("generation-history-world");
assertThrows(
IllegalStateException.class,
() -> IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)
() -> IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot)
);
Path irisRoot = Files.createDirectory(dimensionRoot.toPath().resolve("iris"));
Path externalPack = temporaryFolder.newFolder("external-pack").toPath();
Files.createSymbolicLink(irisRoot.resolve("pack"), externalPack);
Path externalGeneration = temporaryFolder.newFolder("external-generation").toPath();
Files.createSymbolicLink(irisRoot.resolve("generation"), externalGeneration);
assertThrows(
IllegalStateException.class,
() -> IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)
() -> IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot)
);
Files.delete(irisRoot.resolve("pack"));
Path packRoot = Files.createDirectory(irisRoot.resolve("pack"));
assertEquals(packRoot.toFile(), IrisWorldStorage.requireFrozenPackRoot(dimensionRoot));
Files.delete(irisRoot.resolve("generation"));
Path sourcePack = createGenerationPack("storage-active-pack");
GenerationHistory history = createGenerationHistory(dimensionRoot.toPath(), sourcePack, 42L);
assertEquals(
history.activePackRoot().toFile(),
IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot, 42L)
);
assertThrows(
IllegalStateException.class,
() -> IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot, 43L)
);
}
@Test
@@ -444,4 +458,44 @@ public class IrisWorldStorageTest {
Files.createDirectory(namespace.resolve("moon"));
assertTrue(IrisWorldStorage.hasManagedWorldStorage(levelRoot));
}
private Path createGenerationPack(String name) throws IOException {
Path pack = temporaryFolder.newFolder(name).toPath();
Path dimensions = Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(dimensions.resolve("overworld.json"), "{}");
return pack;
}
private static GenerationHistory createGenerationHistory(Path world, Path pack, long seed)
throws IOException {
String fingerprint = GenerationPackFingerprint.compute(
pack,
GenerationPackFingerprint.CURRENT_VERSION
);
GenerationEpoch.DimensionContract dimensionContract = new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"c".repeat(64)
);
return GenerationHistory.create(
world,
pack,
fingerprint,
seed,
dimensionContract,
GenerationRegistryContract.empty()
);
}
}
@@ -85,6 +85,7 @@ public class ServerConfiguratorRegistryVerificationTest {
new File("packs/overworld/dimensions/overworld.json"));
when(dimension.getDimensionTypeKey()).thenReturn("iris:overworld");
when(dimension.getLoader()).thenReturn(data);
when(data.customBiomeResourceKey(dimension, customBiome)).thenReturn("overworld:missing");
ResourceLoader<IrisDimension> loader = mock(ResourceLoader.class);
when(loader.getPossibleKeys()).thenReturn(new String[]{"overworld"});
@@ -175,7 +175,7 @@ public class BukkitWorldConfigurationTest {
assertEquals(
levelRoot.resolve("dimensions/iris/broken").toAbsolutePath().normalize(),
entryOf(audited, "world_iris_broken").storagePath());
assertTrue(entryOf(audited, "world_iris_broken").detail().contains("pack"));
assertTrue(entryOf(audited, "world_iris_broken").detail().contains("generation history"));
}
@Test
@@ -6,6 +6,11 @@ import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSna
import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPaths;
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -59,18 +64,17 @@ public class WorldReplacementBootstrapTest {
}
@Test
public void publishesArmedReplacementAfterFinderAddsNestedMetadata() throws Exception {
public void rejectsNestedMetadataAddedAfterImmutableEpochPublication() throws Exception {
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
configureReplacement(transaction);
Path dimensions = paths(transaction).stage().resolve("iris/pack/dimensions");
Path dimensions = activePackRoot(paths(transaction).stage()).resolve("dimensions");
Files.writeString(dimensions.resolve(".DS_Store"), "Finder metadata");
WorldReplacementBootstrap.ReconcileResult result = reconcile();
assertThrows(IOException.class, this::reconcile);
assertEquals(1, result.published());
assertEquals("replacement", replacementContent(target.worldDirectory()));
assertEquals("original", Files.readString(backup(transaction).resolve("original.txt")));
assertEquals(Phase.PUBLISHED, loadSingle().phase());
assertTrue(Files.isDirectory(paths(transaction).stage()));
assertEquals("original", Files.readString(target.worldDirectory().resolve("original.txt")));
assertEquals(Phase.ARMED, loadSingle().phase());
}
@Test
@@ -267,12 +271,8 @@ public class WorldReplacementBootstrapTest {
UUID endId = UUID.randomUUID();
ReplacementPaths endPaths = WorldReplacementFilesystem.paths(endTarget, endId);
writeOriginalTarget(endPaths, "end-original");
Path endDimension = endPaths.stage().resolve("iris/pack/dimensions/theend.json");
Files.createDirectories(endDimension.getParent());
Files.writeString(endDimension, "end-replacement");
String endFingerprint = WorldReplacementFilesystem.fingerprintPack(
endPaths.stage().resolve("iris/pack")
);
Path endPack = createStagedHistory(endPaths.stage(), "theend", "end-replacement", SEED);
String endFingerprint = WorldReplacementFilesystem.fingerprintPack(endPack);
Transaction end = new Transaction(
endId,
endKey,
@@ -295,8 +295,8 @@ public class WorldReplacementBootstrapTest {
assertEquals(0, result.rolledBack());
assertEquals(0, result.retained());
assertEquals("replacement", replacementContent(target.worldDirectory()));
assertEquals("end-replacement", Files.readString(endTarget.worldDirectory()
.resolve("iris/pack/dimensions/theend.json")));
assertEquals("end-replacement", Files.readString(activePackRoot(endTarget.worldDirectory())
.resolve("dimensions/theend.json")));
assertEquals("nether-original", Files.readString(backup(nether).resolve("original.txt")));
assertEquals("end-original", Files.readString(endPaths.backup().resolve("original.txt")));
List<Transaction> published = WorldReplacementJournal.load(dataDirectory, levelRoot);
@@ -317,13 +317,14 @@ public class WorldReplacementBootstrapTest {
UUID overworldId = UUID.randomUUID();
ReplacementPaths overworldPaths = WorldReplacementFilesystem.paths(overworldTarget, overworldId);
writeOriginalTarget(overworldPaths, "overworld-original");
Path overworldDimension = overworldPaths.stage().resolve("iris/pack/dimensions/overworld.json");
Files.createDirectories(overworldDimension.getParent());
Files.writeString(overworldDimension, "overworld-replacement");
WorldReplacementEntryGuard.stage(levelRoot, overworldPaths.stage(), overworldId);
String overworldFingerprint = WorldReplacementFilesystem.fingerprintPack(
overworldPaths.stage().resolve("iris/pack")
Path overworldPack = createStagedHistory(
overworldPaths.stage(),
"overworld",
"overworld-replacement",
overworldSeed
);
WorldReplacementEntryGuard.stage(levelRoot, overworldPaths.stage(), overworldId);
String overworldFingerprint = WorldReplacementFilesystem.fingerprintPack(overworldPack);
Transaction overworld = new Transaction(
overworldId,
overworldKey,
@@ -358,7 +359,7 @@ public class WorldReplacementBootstrapTest {
target.worldDirectory()
);
assertEquals("overworld-replacement", Files.readString(
overworldTarget.worldDirectory().resolve("iris/pack/dimensions/overworld.json")
activePackRoot(overworldTarget.worldDirectory()).resolve("dimensions/overworld.json")
));
assertEquals("replacement", replacementContent(target.worldDirectory()));
assertEquals("overworld-original", Files.readString(overworldPaths.backup().resolve("original.txt")));
@@ -474,10 +475,8 @@ public class WorldReplacementBootstrapTest {
if (originalPresent) {
writeOriginalTarget(paths, originalContent);
}
Path dimension = paths.stage().resolve("iris/pack/dimensions/underworld.json");
Files.createDirectories(dimension.getParent());
Files.writeString(dimension, "replacement");
String fingerprint = WorldReplacementFilesystem.fingerprintPack(paths.stage().resolve("iris/pack"));
Path pack = createStagedHistory(paths.stage(), "underworld", "replacement", SEED);
String fingerprint = WorldReplacementFilesystem.fingerprintPack(pack);
Transaction transaction = new Transaction(
id,
WORLD_KEY,
@@ -545,6 +544,51 @@ public class WorldReplacementBootstrapTest {
}
private String replacementContent(Path worldDirectory) throws Exception {
return Files.readString(worldDirectory.resolve("iris/pack/dimensions/underworld.json"));
return Files.readString(activePackRoot(worldDirectory).resolve("dimensions/underworld.json"));
}
private Path createStagedHistory(Path world, String dimensionKey, String content, long seed)
throws Exception {
Path source = world.resolveSibling(world.getFileName() + "." + dimensionKey + ".pack-source");
Path dimension = source.resolve("dimensions").resolve(dimensionKey + ".json");
Files.createDirectories(dimension.getParent());
Files.writeString(dimension, content);
String fingerprint = GenerationPackFingerprint.compute(
source,
GenerationPackFingerprint.CURRENT_VERSION
);
Files.createDirectory(world);
return GenerationHistory.create(
world,
source,
fingerprint,
seed,
generationContract(dimensionKey),
GenerationRegistryContract.empty()
).activePackRoot();
}
private static Path activePackRoot(Path world) throws IOException {
return GenerationHistory.open(world).activePackRoot();
}
private static GenerationEpoch.DimensionContract generationContract(String dimensionKey) {
return new GenerationEpoch.DimensionContract(
dimensionKey,
"iris:" + dimensionKey + "_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"c".repeat(64)
);
}
}
@@ -1,5 +1,10 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
@@ -216,7 +221,7 @@ public class WorldReplacementFilesystemTest {
WorldReplacementFilesystem.rollback(paths, true);
assertEquals("original", Files.readString(paths.target().resolve("original.txt")));
assertFalse(Files.exists(paths.target().resolve("iris/pack")));
assertFalse(Files.exists(paths.target().resolve("iris/generation")));
assertFalse(Files.exists(paths.stage()));
assertFalse(Files.exists(paths.backup()));
}
@@ -492,10 +497,24 @@ public class WorldReplacementFilesystemTest {
}
private String writeStage(WorldReplacementFilesystem.ReplacementPaths paths, String content) throws Exception {
Path contentFile = packContent(paths.stage());
Path source = paths.stage().resolveSibling(paths.stage().getFileName() + ".pack-source");
Path contentFile = source.resolve("dimensions/underworld.json");
Files.createDirectories(contentFile.getParent());
Files.writeString(contentFile, content);
return WorldReplacementFilesystem.fingerprintPack(paths.stage().resolve("iris/pack"));
String generationFingerprint = GenerationPackFingerprint.compute(
source,
GenerationPackFingerprint.CURRENT_VERSION
);
Files.createDirectory(paths.stage());
GenerationHistory history = GenerationHistory.create(
paths.stage(),
source,
generationFingerprint,
42L,
generationContract("underworld"),
GenerationRegistryContract.empty()
);
return WorldReplacementFilesystem.fingerprintPack(history.activePackRoot());
}
private void writeOriginalTarget(WorldReplacementFilesystem.ReplacementPaths paths, String content) throws Exception {
@@ -535,8 +554,30 @@ public class WorldReplacementFilesystemTest {
return Files.readString(packContent(worldDirectory));
}
private Path packContent(Path worldDirectory) {
return worldDirectory.resolve("iris/pack/dimensions/underworld.json");
private Path packContent(Path worldDirectory) throws IOException {
return GenerationHistory.open(worldDirectory)
.activePackRoot()
.resolve("dimensions/underworld.json");
}
private static GenerationEpoch.DimensionContract generationContract(String dimensionKey) {
return new GenerationEpoch.DimensionContract(
dimensionKey,
"iris:" + dimensionKey + "_type",
"NETHER",
"OVERWORLD",
0,
0,
256,
256,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"c".repeat(64)
);
}
private String artifactStem(String name, UUID transactionId) {
@@ -0,0 +1,63 @@
package art.arcane.iris.core.loader;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
public class IrisDataRetainedPackTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisData retained;
@Before
public void openRetainedData() throws Exception {
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
retained = spy(IrisData.openRuntime(temporaryFolder.newFolder("retained-pack")));
retained.bindGenerationRegistryContract(GenerationRegistryContract.empty());
}
@After
public void closeData() {
if (retained != null) {
retained.close();
}
IrisServices.clear();
}
@Test
public void missingRetainedResourceNeverSearchesMutableGlobalPacks() {
doReturn(null).when(retained).load(IrisDimension.class, "missing", false);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
assertNull(IrisData.loadAnyDimension("missing", retained));
platforms.verifyNoInteractions();
}
}
@Test
public void existingRetainedResourceComesFromItsBoundPack() {
IrisDimension dimension = new IrisDimension();
doReturn(dimension).when(retained).load(IrisDimension.class, "overworld", false);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
assertSame(dimension, IrisData.loadAnyDimension("overworld", retained));
platforms.verifyNoInteractions();
}
}
}
@@ -379,15 +379,13 @@ public class IrisLanguageTest {
{
"locale": "de_DE",
"messages": {
"iris.bukkit.runtime.commanddeveloper.update_world_warning": [
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning": [
"Sicherung erstellen.",
"Mögliche Probleme:",
" - Beschädigte Chunks",
" - Neu generierte Chunks",
" - Fehlende Strukturen",
" - Nicht verbundene Höhlen",
" - Nicht verbundene Geländeschichten",
"Risiken bestätigt.",
"Vorhandene Chunks bleiben erhalten.",
"Neue Chunks verwenden das neue Paket.",
"Die Grenze bleibt gespeichert.",
"Den vollständigen Verlauf sichern.",
"Neustart bestätigen.",
"Welt {world}, Paket {pack}"
],
"iris.bukkit.runtime.commandpack.more_warning_s": {
@@ -401,13 +399,11 @@ public class IrisLanguageTest {
assertTrue(IrisLanguage.reload(dataFolder, "de_DE"));
assertEquals(
"Sicherung erstellen.\n"
+ "Mögliche Probleme:\n"
+ " - Beschädigte Chunks\n"
+ " - Neu generierte Chunks\n"
+ " - Fehlende Strukturen\n"
+ " - Nicht verbundene Höhlen\n"
+ " - Nicht verbundene Geländeschichten\n"
+ "Risiken bestätigt.\n"
+ "Vorhandene Chunks bleiben erhalten.\n"
+ "Neue Chunks verwenden das neue Paket.\n"
+ "Die Grenze bleibt gespeichert.\n"
+ "Den vollständigen Verlauf sichern.\n"
+ "Neustart bestätigen.\n"
+ "Welt world, Paket pack",
IrisLanguage.plain(
BukkitRuntimeMessages.COMMAND_DEVELOPER_UPDATE_WORLD_WARNING,
@@ -2,20 +2,26 @@ package art.arcane.iris.core.nms;
import org.junit.Test;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.reflect.Method;
import java.util.function.BooleanSupplier;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ServerShutdownBoundaryTest {
@Test
public void await_returnsImmediatelyWhenBoundaryIsAlreadyReached() {
assertTrue(ServerShutdownBoundary.await(
assertTrue(new ServerShutdownBoundary(
() -> true,
Thread.currentThread(),
Thread.currentThread()).await(
0L,
TimeUnit.MILLISECONDS
));
@@ -23,9 +29,9 @@ public class ServerShutdownBoundaryTest {
@Test
public void await_doesNotJoinTheCallingServerThread() {
assertFalse(ServerShutdownBoundary.await(
assertFalse(new ServerShutdownBoundary(
() -> false,
Thread.currentThread(),
Thread.currentThread()).await(
5L,
TimeUnit.SECONDS
));
@@ -44,11 +50,10 @@ public class ServerShutdownBoundaryTest {
await(releaseServer);
boundaryReached.set(true);
}, "server-boundary-test");
ServerShutdownBoundary boundary = new ServerShutdownBoundary(boundaryReached::get, serverThread);
Thread waiterThread = new Thread(() -> {
waiterStarted.countDown();
result.set(ServerShutdownBoundary.await(
boundaryReached::get,
serverThread,
result.set(boundary.await(
5L,
TimeUnit.SECONDS
));
@@ -76,9 +81,9 @@ public class ServerShutdownBoundaryTest {
serverThread.start();
try {
assertFalse(ServerShutdownBoundary.await(
assertFalse(new ServerShutdownBoundary(
() -> false,
serverThread,
serverThread).await(
0L,
TimeUnit.MILLISECONDS
));
@@ -88,6 +93,34 @@ public class ServerShutdownBoundaryTest {
}
}
@Test
public void preparationLinksTheBoundarySupplierBeforeShutdown() {
AtomicInteger reads = new AtomicInteger();
ServerShutdownBoundary boundary = new ServerShutdownBoundary(
() -> reads.incrementAndGet() > 1, Thread.currentThread());
assertEquals(1, reads.get());
assertTrue(boundary.await(0L, TimeUnit.MILLISECONDS));
}
@Test
public void preparedBoundaryRemainsUsableAfterItsClassLoaderCloses() throws Exception {
URL classes = ServerShutdownBoundary.class.getProtectionDomain().getCodeSource().getLocation();
AtomicBoolean reached = new AtomicBoolean(false);
BooleanSupplier supplier = reached::get;
Object boundary;
Method awaitMethod;
try (URLClassLoader loader = new URLClassLoader(new URL[]{classes}, ClassLoader.getPlatformClassLoader())) {
Class<?> type = Class.forName(ServerShutdownBoundary.class.getName(), true, loader);
boundary = type.getConstructor(BooleanSupplier.class, Thread.class)
.newInstance(supplier, Thread.currentThread());
awaitMethod = type.getMethod("await", long.class, TimeUnit.class);
}
reached.set(true);
assertEquals(Boolean.TRUE, awaitMethod.invoke(boundary, 0L, TimeUnit.MILLISECONDS));
}
private static void await(CountDownLatch latch) {
try {
latch.await();
@@ -1,6 +1,8 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import org.junit.Assume;
@@ -118,8 +120,8 @@ public class DefaultPackBootstrapProvisionerTest {
assertTrue(Files.isRegularFile(installed.packRoots().get("underworld").resolve("dimensions/underworld_roof.json")));
assertEquals(root.resolve("datapacks/iris"), installed.datapackRoot());
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("pack.mcmeta")));
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/overworld/worldgen/biome/bootstrap_biome.json")));
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("data/underworld/worldgen/biome/underworld_biome.json")));
assertTrue(Files.isRegularFile(installedBiomePath(installed.datapackRoot(), "overworld", "bootstrap_biome")));
assertTrue(Files.isRegularFile(installedBiomePath(installed.datapackRoot(), "underworld", "underworld_biome")));
assertFalse(Files.exists(dataDirectory.resolve("bootstrap/datapack")));
assertTrue(DefaultPackBootstrapProvisioner.isProvisioned(
dataDirectory,
@@ -176,8 +178,8 @@ public class DefaultPackBootstrapProvisionerTest {
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, rebuilt.status());
assertEquals(2, requests.get());
assertTrue(Files.isRegularFile(rebuilt.datapackRoot().resolve("pack.mcmeta")));
assertTrue(Files.isRegularFile(rebuilt.datapackRoot().resolve("data/overworld/worldgen/biome/bootstrap_biome.json")));
assertTrue(Files.isRegularFile(rebuilt.datapackRoot().resolve("data/underworld/worldgen/biome/underworld_biome.json")));
assertTrue(Files.isRegularFile(installedBiomePath(rebuilt.datapackRoot(), "overworld", "bootstrap_biome")));
assertTrue(Files.isRegularFile(installedBiomePath(rebuilt.datapackRoot(), "underworld", "underworld_biome")));
} finally {
if (!serverStopped) {
server.stop(0);
@@ -323,7 +325,7 @@ public class DefaultPackBootstrapProvisionerTest {
assertEquals(1, requests.get());
assertTrue(Files.isSymbolicLink(link));
assertEquals(target.toRealPath(), link.toRealPath());
assertTrue(Files.isRegularFile(result.datapackRoot().resolve("data/overworld/worldgen/biome/local_biome.json")));
assertTrue(Files.isRegularFile(installedBiomePath(result.datapackRoot(), "overworld", "local_biome")));
Files.writeString(target.resolve("biomes/local.json"), biomeJson("changed_biome"), StandardCharsets.UTF_8);
assertFalse(DefaultPackBootstrapProvisioner.isProvisioned(
@@ -375,7 +377,7 @@ public class DefaultPackBootstrapProvisionerTest {
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.UPDATED, updated.status());
assertEquals(2, requests.get());
assertTrue(Files.isRegularFile(updated.datapackRoot().resolve("data/second/worldgen/biome/second_biome.json")));
assertTrue(Files.isRegularFile(installedBiomePath(updated.datapackRoot(), "second", "second_biome")));
assertTrue(Files.isRegularFile(updated.datapackRoot().resolve("data/world_local/worldgen/biome/world_local_biome.json")));
} finally {
server.stop(0);
@@ -406,7 +408,7 @@ public class DefaultPackBootstrapProvisionerTest {
assertEquals(3, requests.get());
assertTrue(Files.readString(editedBiome).contains("locally_edited_biome"));
assertTrue(Files.isRegularFile(updated.datapackRoot().resolve("data/overworld/worldgen/biome/locally_edited_biome.json")));
assertTrue(Files.isRegularFile(installedBiomePath(updated.datapackRoot(), "overworld", "locally_edited_biome")));
Properties marker = new Properties();
try (InputStream input = Files.newInputStream(dataDirectory.resolve("bootstrap/provisioned.properties"))) {
marker.load(input);
@@ -446,8 +448,7 @@ public class DefaultPackBootstrapProvisionerTest {
assertEquals(3, requests.get());
assertTrue(Files.readString(editedBiome).contains("underworld_local_edit"));
assertTrue(Files.isRegularFile(updated.datapackRoot()
.resolve("data/underworld/worldgen/biome/underworld_local_edit.json")));
assertTrue(Files.isRegularFile(installedBiomePath(updated.datapackRoot(), "underworld", "underworld_local_edit")));
Properties marker = loadProperties(dataDirectory.resolve("bootstrap/provisioned.properties"));
assertEquals("true", marker.getProperty("pack.overworld.managed"));
assertEquals("false", marker.getProperty("pack.underworld.managed"));
@@ -491,12 +492,9 @@ public class DefaultPackBootstrapProvisionerTest {
.contains("overworld_first"));
assertTrue(Files.readString(dataDirectory.resolve("packs/underworld/biomes/local.json"))
.contains("underworld_second"));
assertTrue(Files.isRegularFile(updated.datapackRoot()
.resolve("data/overworld/worldgen/biome/overworld_first.json")));
assertTrue(Files.isRegularFile(updated.datapackRoot()
.resolve("data/underworld/worldgen/biome/underworld_second.json")));
assertFalse(Files.exists(updated.datapackRoot()
.resolve("data/underworld/worldgen/biome/underworld_first.json")));
assertTrue(Files.isRegularFile(installedBiomePath(updated.datapackRoot(), "overworld", "overworld_first")));
assertTrue(Files.isRegularFile(installedBiomePath(updated.datapackRoot(), "underworld", "underworld_second")));
assertFalse(Files.exists(installedBiomePath(updated.datapackRoot(), "underworld", "underworld_first")));
} finally {
if (initialServer != null) {
initialServer.stop(0);
@@ -668,6 +666,17 @@ public class DefaultPackBootstrapProvisionerTest {
}
}
private static Path installedBiomePath(Path datapackRoot, String dimension, String id) {
String resourceKey = GenerationRegistryContractFactory.customBiomeResourceKey(
dimension, new IrisBiomeCustom().setId(id)
);
int separator = resourceKey.indexOf(':');
return datapackRoot.resolve("data")
.resolve(resourceKey.substring(0, separator))
.resolve("worldgen/biome")
.resolve(resourceKey.substring(separator + 1) + ".json");
}
private static DefaultPackBootstrapProvisioner.ProvisionOptions options(
HttpServer server,
Path serverRoot,
@@ -0,0 +1,61 @@
package art.arcane.iris.core.service;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class LogFilterSVCTest {
@Test
public void removesFilterFromItsOriginalLoggerAfterLoggingShutdown() {
Logger logger = mock(Logger.class);
LoggerConfig configuration = mock(LoggerConfig.class);
when(logger.get()).thenReturn(configuration);
LogFilterSVC service = new LogFilterSVC();
try (MockedStatic<LogManager> logging = mockStatic(LogManager.class)) {
logging.when(LogManager::getRootLogger).thenReturn(logger);
service.onEnable();
service.onEnable();
logging.when(LogManager::getRootLogger).thenThrow(new IllegalStateException("Logging stopped"));
service.onDisable();
service.onDisable();
logging.verify(LogManager::getRootLogger, times(1));
verify(configuration, times(1)).addFilter(service);
verify(configuration, times(1)).removeFilter(service);
}
}
@Test
public void bindsToTheNewConfigurationAfterReenable() {
Logger firstLogger = mock(Logger.class);
Logger secondLogger = mock(Logger.class);
LoggerConfig firstConfiguration = mock(LoggerConfig.class);
LoggerConfig secondConfiguration = mock(LoggerConfig.class);
when(firstLogger.get()).thenReturn(firstConfiguration);
when(secondLogger.get()).thenReturn(secondConfiguration);
LogFilterSVC service = new LogFilterSVC();
try (MockedStatic<LogManager> logging = mockStatic(LogManager.class)) {
logging.when(LogManager::getRootLogger).thenReturn(firstLogger, secondLogger);
service.onEnable();
service.onDisable();
service.onEnable();
service.onDisable();
verify(firstConfiguration).addFilter(service);
verify(firstConfiguration).removeFilter(service);
verify(secondConfiguration).addFilter(service);
verify(secondConfiguration).removeFilter(service);
}
}
}
@@ -0,0 +1,68 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.loader.IrisData;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class PreservationSVCTest {
@Test
public void retainsShutdownExecutorsUntilTheyActuallyTerminate() {
PreservationSVC preservation = new PreservationSVC();
ExecutorService executor = mock(ExecutorService.class);
when(executor.isShutdown()).thenReturn(true);
when(executor.isTerminated()).thenReturn(false);
preservation.register(executor);
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
preservation.dereference();
assertTrue(preservation.hasActiveResources());
when(executor.isTerminated()).thenReturn(true);
preservation.dereference();
assertFalse(preservation.hasActiveResources());
}
}
@Test
public void tracksOwnedThreadsUntilTheyExit() throws Exception {
PreservationSVC preservation = new PreservationSVC();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stop = new CountDownLatch(1);
Thread worker = new Thread(() -> awaitStop(started, stop), "preservation-shutdown-test");
preservation.register(worker);
worker.start();
try {
assertTrue(started.await(5L, TimeUnit.SECONDS));
assertTrue(preservation.hasActiveResources());
} finally {
stop.countDown();
worker.join(5000L);
}
assertFalse(worker.isAlive());
assertFalse(preservation.hasActiveResources());
}
@Test
public void emptyPreservationDoesNotPreventLoaderRelease() {
assertFalse(new PreservationSVC().hasActiveResources());
}
private static void awaitStop(CountDownLatch started, CountDownLatch stop) {
started.countDown();
try {
stop.await();
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
}
}
@@ -15,8 +15,8 @@ public class StudioSVCWorldPackFeedbackContractTest {
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
)).replace("\r\n", "\n");
assertEquals(1, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD"));
assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED"));
assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD"));
assertEquals(3, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED"));
assertFalse(source.contains("STUDIO_S_V_C_INSTALLING_PACKAGE"));
assertFalse(source.contains("Publishing snapshot"));
assertFalse(source.contains("sender.sendMessage(\"Iris refused to copy a pack"));
@@ -444,6 +444,26 @@ public class StudioSVCWorldPackPublishTest {
assertEquals("second", second.join());
}
@Test
public void productionWorldPublicationUsesImmutableGenerationHistory() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
)).replace("\r\n", "\n");
int productionStart = source.indexOf("private IrisDimension publishGenerationHistory(");
int transientStart = source.indexOf("private IrisDimension installIntoDirectory(", productionStart);
String production = source.substring(productionStart, transientStart);
assertTrue(production.contains("GenerationHistory.create("));
assertTrue(production.contains("GenerationHistory.openIfPresent(dimensionRoot, worldSeed)"));
assertTrue(production.contains("GenerationHistory.adoptLegacyPack("));
assertTrue(production.contains("history.stageUpdate("));
assertTrue(production.contains("getGenerationTransitionWidthBlocks()"));
assertTrue(production.contains("history.activePackRoot()"));
assertFalse(production.contains("AtomicDirectoryPublisher.publish("));
assertTrue(source.contains("installIntoTransientWorld("));
assertTrue(source.contains("new File(dimensionRoot, \"iris/pack\")"));
}
private static void writeValidPack(Path packRoot) throws Exception {
Files.createDirectories(packRoot.resolve("dimensions"));
Files.createDirectories(packRoot.resolve("regions"));
@@ -19,7 +19,7 @@ public class IrisPersistentWorldCreationContractTest {
int dimensionRoot = create.indexOf("WorldCreatorCompat.persistentDimensionRoot(worldKey)");
int levelRoot = create.indexOf("WorldCreatorCompat.persistentLevelRoot(worldKey)");
int existingStorage = create.indexOf("Files.exists(storageRoot.toPath())");
int freezePack = create.indexOf(".installIntoWorld(sender, resolvedDimension, dimensionRoot)");
int freezePack = create.indexOf("studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed)");
int persistentCreator = create.indexOf(".persistent(!studio && !benchmark)");
int bukkitCreate = create.indexOf("INMS.get().createWorldAsync(wc, request)");
int rollbackStorage = create.indexOf(
@@ -48,13 +48,16 @@ public class IrisPersistentWorldCreationContractTest {
int persistentStorage = create.indexOf("WorldCreatorCompat.persistentDimensionRoot(worldKey)");
int canonicalIdentity = create.indexOf(".platformIdentity(worldKey.toString())");
int configuredName = create.indexOf(".name(creator.name())");
int exactPack = create.indexOf("new File(w.worldFolder(), \"iris/pack\")");
int history = create.indexOf("requireGenerationHistory(w.worldFolder(), seed)");
int exactPack = create.indexOf("requireActivePack(generationHistory)");
assertTrue(persistentCreator >= 0);
assertTrue(persistentStorage > persistentCreator);
assertTrue(canonicalIdentity > persistentStorage);
assertTrue(configuredName > canonicalIdentity);
assertTrue(exactPack > configuredName);
assertTrue(history > configuredName);
assertTrue(exactPack > history);
assertTrue(create.contains("new File(w.worldFolder(), \"iris/pack\")"));
}
@Test
@@ -0,0 +1,26 @@
package art.arcane.iris.core.tools;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisToolbeltShutdownContractTest {
@Test
public void disabledPluginDoesNotImplyServerShutdown() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.irisToolbeltSource")));
int start = source.indexOf("public static boolean isServerStopping()");
int end = source.indexOf("public static void beginWorldMaintenance", start);
assertTrue(start >= 0);
assertTrue(end > start);
String stopping = source.substring(start, end);
assertTrue(stopping.contains("return INMS.isBound() && INMS.get().isServerStopping();"));
assertFalse(stopping.contains("isEnabled()"));
assertFalse(stopping.contains("hasPlugin()"));
assertFalse(stopping.contains("getMethod("));
}
}
@@ -6,6 +6,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EngineRuntimePublicationContractTest {
@@ -38,17 +39,85 @@ public class EngineRuntimePublicationContractTest {
}
@Test
public void dimensionStackRunsAfterHostStaticObjects() throws IOException {
public void dimensionStackRegistrationRunsAfterHostStaticObjects() throws IOException {
String builderSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java")).replace("\r\n", "\n");
int buildStart = builderSource.indexOf("private EngineRuntime buildRuntime(");
int buildEnd = builderSource.indexOf("GenerationRuntime buildDetachedGenerationRuntime(", buildStart);
String build = builderSource.substring(buildStart, buildEnd);
assertBefore(build, "assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode)",
"assembly.runtimeKernel.registerDimensionStack(");
String kernelSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/history/GenerationKernelV1.java")).replace("\r\n", "\n");
assertBefore(kernelSource, "staticObjects.apply(engine, x, z, blocks)",
"dimensionStack.actuate(x, z, blocks, multicore, chunkContext)");
assertBefore(kernelSource, "dimensionStack.actuate(x, z, blocks, multicore, chunkContext)",
"stackCustom.modify(x, z, blocks, multicore, chunkContext)");
}
@Test
public void detachedRuntimeBuildDoesNotCreateWorldOwnedServices() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java")).replace("\r\n", "\n");
int buildStart = source.indexOf("EngineRuntime buildRuntime(EngineTarget runtimeTarget)");
int buildEnd = source.indexOf("DimensionStackContext buildDimensionStackContext", buildStart);
int buildStart = source.indexOf("GenerationRuntime buildDetachedGenerationRuntime(");
int buildEnd = source.indexOf("void publishRuntime(", buildStart);
String build = source.substring(buildStart, buildEnd);
assertBefore(build, "staticObjects.apply(engine, x, z, blocks)",
"dimensionStack.actuate(x, z, blocks, multicore, context)");
assertBefore(build, "dimensionStack.actuate(x, z, blocks, multicore, context)",
"stackCustom.modify(x, z, blocks, multicore, context)");
assertTrue(build.contains("assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan)"));
assertTrue(build.contains("assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory)"));
assertTrue(build.contains("assembly.mantle.hotload()"));
assertTrue(build.contains("assembly.runtimeKernel.createMode(engine)"));
assertTrue(build.contains("assembly.runtimeKernel.createUpperContext(engine)"));
assertTrue(build.contains("assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode)"));
assertFalse(build.contains("EngineEffectsProvider"));
assertFalse(build.contains("EngineWorldManagerProvider"));
assertFalse(build.contains("worldManager().start()"));
}
@Test
public void runtimePublicationTransfersAnIdenticalMantleWithoutClosingIt() throws IOException {
String builderSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineRuntimeBuilder.java")).replace("\r\n", "\n");
String shutdownSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java")).replace("\r\n", "\n");
assertTrue(builderSource.contains("next.generation().mantle()"));
assertTrue(shutdownSource.contains("generationRuntime.mantle() != retainedMantle"));
}
@Test
public void engineAndDetachedRuntimeAcceptExplicitMantleDirectories() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/IrisEngine.java")).replace("\r\n", "\n");
assertTrue(source.contains("InitializationMode initializationMode,\n Path mantleStorageDirectory"));
assertTrue(source.contains("EngineTarget runtimeTarget,\n Path mantleStorageDirectory"));
assertTrue(source.contains("exclusive mantle storage directory"));
}
@Test
public void mantleIoPinsTheRuntimeDataAcrossWorkerThreads() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/IrisEngineMantle.java")).replace("\r\n", "\n");
assertTrue(source.contains("runtimeData::get"));
assertTrue(source.contains("runtimeData.set(Objects.requireNonNull(engine.getData()"));
assertFalse(source.contains("createDataAdapter(engine::getData)"));
}
@Test
public void failedDetachedBuildReleasesTransferredTargetOwnership() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/IrisEngine.java")).replace("\r\n", "\n");
int buildStart = source.indexOf("public GenerationRuntimeBinding buildDetachedGenerationRuntime(");
int buildEnd = source.indexOf("public void closeDetachedGenerationRuntime(", buildStart);
String build = source.substring(buildStart, buildEnd);
assertTrue(build.contains("detachedData.registerEngine(this)"));
assertTrue(build.contains("shutdownSequence.closeDetachedTarget(requiredTarget, null)"));
assertTrue(build.contains("shutdownSequence.closeDetachedGenerationRuntime(detached, null)"));
}
private static void assertBefore(String source, String first, String second) {
@@ -10,16 +10,28 @@ import static org.junit.Assert.assertTrue;
public class EngineShutdownOwnershipContractTest {
@Test
public void ownershipCloseMustSucceedBeforeMantleRelease() throws IOException {
public void attachedHistoryRouterClosesBeforeRuntimeOwnershipReleases() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java")).replace("\r\n", "\n");
int closeStart = source.indexOf("void close()");
int routerClose = source.indexOf("engine::closeAttachedGenerationHistoryRuntimeRouter", closeStart);
int runtimeRelease = source.indexOf("releaseRuntime(failure)", routerClose);
assertTrue(routerClose > closeStart);
assertTrue(runtimeRelease > routerClose);
}
@Test
public void ownershipCloseMustSucceedBeforeRuntimeRelease() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java")).replace("\r\n", "\n");
int ownershipClose = source.indexOf("NativeStructureOwnershipStore.close(engine)");
int ownershipGate = source.indexOf("if (ownershipFailure == null)", ownershipClose);
int mantleRelease = source.indexOf("releaseMantle(failure)", ownershipGate);
int runtimeRelease = source.indexOf("releaseRuntime(failure)", ownershipGate);
assertTrue(ownershipClose >= 0);
assertTrue(ownershipGate > ownershipClose);
assertTrue(mantleRelease > ownershipGate);
assertTrue(runtimeRelease > ownershipGate);
}
@Test
@@ -31,12 +43,32 @@ public class EngineShutdownOwnershipContractTest {
String cleanup = source.substring(cleanupStart, cleanupEnd);
int ownershipClose = cleanup.indexOf("NativeStructureOwnershipStore.close(engine)");
int ownershipGate = cleanup.indexOf("if (ownershipFailure == null)", ownershipClose);
int mantleRelease = cleanup.indexOf("engine.getMantle()::close", ownershipGate);
int detachedRelease = cleanup.indexOf("closeDetachedGenerationRuntimes(cleanupFailure)", ownershipGate);
int runtimeRelease = cleanup.indexOf("closeRuntime(engine.runtime, cleanupFailure)", ownershipGate);
int closedPublication = cleanup.indexOf("engine.closed = true", ownershipGate);
assertTrue(ownershipClose >= 0);
assertTrue(ownershipGate > ownershipClose);
assertTrue(mantleRelease > ownershipGate);
assertTrue(detachedRelease > ownershipGate);
assertTrue(runtimeRelease > detachedRelease);
assertTrue(closedPublication > ownershipGate);
}
@Test
public void detachedRuntimesCloseBeforeTheBaseRuntimeAndTarget() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java")).replace("\r\n", "\n");
int releaseStart = source.indexOf("private Throwable releaseRuntime(");
int releaseEnd = source.indexOf("private Throwable closeDetachedGenerationRuntimes(", releaseStart);
String release = source.substring(releaseStart, releaseEnd);
int detachedRelease = release.indexOf("closeDetachedGenerationRuntimes(null)");
int detachedGate = release.indexOf("if (detachedFailure != null)", detachedRelease);
int baseRelease = release.indexOf("closeRuntime(engine.runtime, null)", detachedGate);
int targetRelease = source.indexOf("engine.publishedTarget::close", releaseEnd);
assertTrue(detachedRelease >= 0);
assertTrue(detachedGate > detachedRelease);
assertTrue(baseRelease > detachedGate);
assertTrue(targetRelease > releaseEnd);
}
}
@@ -0,0 +1,123 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
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.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import org.junit.Test;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
public class IrisComplexTransitionTest {
@Test
public void blendsDryBoundaryTerrainFromTheFrozenSurface() {
TransitionGenerationPlan.TerrainSample sample = new TransitionGenerationPlan.TerrainSample(
8D,
0.5D,
0D,
signature(OptionalInt.empty()),
64D,
55D,
0D
);
assertEquals(82D, IrisComplex.blendNaturalTerrainHeight(100D, sample), 0D);
}
@Test
public void blendsSubmergedBoundaryTerrainFromTheFrozenOceanFloor() {
TransitionGenerationPlan.TerrainSample sample = new TransitionGenerationPlan.TerrainSample(
8D,
0.5D,
0D,
signature(OptionalInt.of(63)),
64D,
55D,
0D
);
assertEquals(75D, IrisComplex.blendNaturalTerrainHeight(95D, sample), 0D);
}
@Test
public void preservesNewTerrainAndHydrologyObjectsAtFullWeight() {
TransitionGenerationPlan.TerrainSample terrain = new TransitionGenerationPlan.TerrainSample(
32D,
1D,
1D,
signature(OptionalInt.empty()),
64D,
55D,
0D
);
HydrologyColumnSample hydrology = hydrologySample();
assertEquals(101.25D, IrisComplex.blendNaturalTerrainHeight(101.25D, terrain), 0D);
assertSame(hydrology, IrisComplex.taperHydrologySample(hydrology, 1D));
}
@Test
public void tapersHydrologyGeometryTowardNaturalTerrain() {
HydrologyColumnSample tapered = IrisComplex.taperHydrologySample(hydrologySample(), 0.5D);
HydrologyColumnLayer layer = tapered.layers().getFirst();
assertEquals(73, layer.bedY());
assertEquals(75, layer.fluidHeadY());
assertEquals(75, layer.ceilingY());
assertEquals(73, tapered.terrainHeight());
assertEquals("river", layer.profileKey());
}
private static TerrainBoundarySignature signature(OptionalInt fluidHeight) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(15, 8, 64, 55, fluidHeight, OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(0, 1, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("iris:frozen"), new short[]{0})
)
);
}
private static HydrologyColumnSample hydrologySample() {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
1L,
HydrologyFeatureType.SURFACE_POOL,
2L,
3L,
4,
70,
6,
1,
0,
true
);
HydrologyColumnLayer layer = new HydrologyColumnLayer(
feature,
66,
70,
70,
true,
false,
false,
true,
false,
false,
true,
true,
false,
"river",
"surface",
"mouth",
"shore",
"bank",
"cave"
);
return new HydrologyColumnSample(4, 6, 80, 63, false, "parent", List.of(layer));
}
}
@@ -0,0 +1,625 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.framework.IrisEngineMode;
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntConsumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisEngineGenerationRuntimeScopeTest {
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(block);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void scopedGettersResolveDetachedGenerationStateAndWorldServicesStayActive() throws Exception {
RuntimeFixture active = runtime(1, 1D, 2D, 3D);
RuntimeFixture detached = runtime(2, 4D, 5D, 6D);
EngineEffects effects = mock(EngineEffects.class);
EngineWorldManager worldManager = mock(EngineWorldManager.class);
IrisEngine engine = engine(active.runtime, effects, worldManager);
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
assertSame(detached.target, engine.getTarget());
assertSame(detached.data, engine.getData());
assertSame(detached.dimension, engine.getDimension());
assertSame(detached.runtime.seedManager(), engine.getSeedManager());
assertSame(detached.complex, engine.getComplex());
assertSame(detached.upperContext, engine.getUpperContext());
assertSame(detached.dimensionStackContext, engine.getDimensionStackContext());
assertSame(detached.mode, engine.getMode());
assertSame(detached.mantle, engine.getMantle());
assertSame(detached.hash32, engine.getHash32());
assertEquals(2, engine.getCacheID());
assertEquals(4D, engine.getMaxBiomeObjectDensity(), 0D);
assertEquals(5D, engine.getMaxBiomeLayerDensity(), 0D);
assertEquals(6D, engine.getMaxBiomeDecoratorDensity(), 0D);
assertSame(effects, engine.getEffects());
assertSame(worldManager, engine.getWorldManager());
}
assertSame(active.target, engine.getTarget());
assertSame(active.data, engine.getData());
assertSame(active.runtime.seedManager(), engine.getSeedManager());
assertSame(active.mantle, engine.getMantle());
assertEquals(1, engine.getCacheID());
}
@Test
public void scopedKernelVersionProvidesTheAlgorithmDispatchKey() throws Exception {
GenerationKernelRegistry.Version activeVersion = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version historicalVersion = new GenerationKernelRegistry.Version(2, 3, 4);
RuntimeFixture active = runtime(1, 1D, 1D, 1D, activeVersion);
RuntimeFixture historical = runtime(2, 1D, 1D, 1D, historicalVersion);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, historical.runtime);
assertEquals(activeVersion, engine.getGenerationKernelVersion());
assertSame(active.runtime.runtimeKernel(), engine.getGenerationRuntimeKernel());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
assertEquals(historicalVersion, engine.getGenerationKernelVersion());
assertSame(historical.runtime.runtimeKernel(), engine.getGenerationRuntimeKernel());
assertNotSame(active.runtime.runtimeKernel(), engine.getGenerationRuntimeKernel());
}
assertEquals(activeVersion, engine.getGenerationKernelVersion());
}
@Test
public void executableKernelFactoriesProduceDistinctRuntimeBindings() throws Exception {
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
IrisComplex complexOne = mock(IrisComplex.class);
IrisComplex complexTwo = mock(IrisComplex.class);
GenerationKernelRegistry kernels = new GenerationKernelRegistry(
versionTwo,
Set.of(
new GenerationKernelRegistry.Kernel(
1,
"1".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> complexOne
)
),
new GenerationKernelRegistry.Kernel(
2,
"2".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> complexTwo
)
)
)
);
GenerationKernelRegistry.RuntimeKernel kernelOne = kernels.select(versionOne);
GenerationKernelRegistry.RuntimeKernel kernelTwo = kernels.select(versionTwo);
IrisEngine factoryContext = mock(IrisEngine.class);
RuntimeFixture first = runtime(
1, 1D, 1D, 1D, kernelOne, kernelOne.createComplex(factoryContext, null));
RuntimeFixture second = runtime(
2, 1D, 1D, 1D, kernelTwo, kernelTwo.createComplex(factoryContext, null));
IrisEngine engine = engine(first.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding firstBinding = new IrisEngine.GenerationRuntimeBinding(
engine, first.runtime);
IrisEngine.GenerationRuntimeBinding secondBinding = detachedBinding(engine, second.runtime);
assertSame(kernelOne, firstBinding.runtimeKernel());
assertSame(kernelTwo, secondBinding.runtimeKernel());
assertSame(complexOne, engine.getComplex());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(secondBinding)) {
assertSame(kernelTwo, engine.getGenerationRuntimeKernel());
assertSame(complexTwo, engine.getComplex());
}
}
@Test
public void burstWorkerReopensCapturedGenerationRuntimeScope() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
EngineMode mode = new IrisEngineMode(engine) {
};
MultiBurst workerPool = new MultiBurst("Generation Runtime Scope Test", Thread.NORM_PRIORITY, () -> 2);
when(detached.target.getBurster()).thenReturn(workerPool);
AtomicReference<EngineTarget> observedTarget = new AtomicReference<>();
AtomicReference<Thread> observedThread = new AtomicReference<>();
EngineStage stage = (x, z, blocks, biomes, multicore, context) -> {
observedTarget.set(engine.getTarget());
observedThread.set(Thread.currentThread());
};
ChunkContext context = mock(ChunkContext.class);
when(context.getGenerationSessionId()).thenReturn(17L);
Thread caller = Thread.currentThread();
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
mode.burst(stage).generate(0, 0, null, null, true, context);
} finally {
mode.close();
workerPool.close();
}
assertSame(detached.target, observedTarget.get());
assertNotSame(caller, observedThread.get());
}
@Test
public void scopeRejectsForeignClosedAndOutOfOrderBindings() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture first = runtime(2, 2D, 2D, 2D);
RuntimeFixture second = runtime(3, 3D, 3D, 3D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine other = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding foreign = new IrisEngine.GenerationRuntimeBinding(other, first.runtime);
assertThrows(IllegalArgumentException.class, () -> engine.openGenerationRuntimeScope(foreign));
IrisEngine.GenerationRuntimeBinding closed = new IrisEngine.GenerationRuntimeBinding(engine, first.runtime);
assertThrows(IllegalStateException.class, () -> engine.openGenerationRuntimeScope(closed));
IrisEngine.GenerationRuntimeBinding firstBinding = detachedBinding(engine, first.runtime);
IrisEngine.GenerationRuntimeBinding secondBinding = detachedBinding(engine, second.runtime);
IrisEngine.GenerationRuntimeScope outer = engine.openGenerationRuntimeScope(firstBinding);
IrisEngine.GenerationRuntimeScope inner = engine.openGenerationRuntimeScope(secondBinding);
assertThrows(IllegalStateException.class, outer::close);
inner.close();
outer.close();
assertSame(active.target, engine.getTarget());
}
@Test
public void closingDetachedRuntimeReleasesGenerationTargetAndData() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
doAnswer(invocation -> {
assertSame(detached.mantle, engine.getMantle());
return null;
}).when(detached.mode).close();
engine.closeDetachedGenerationRuntime(binding);
verify(detached.mode).close();
verify(detached.complex).close();
verify(detached.mantle).saveAllNow();
verify(detached.mantle).close();
verify(detached.data).unregisterEngine(engine);
verify(detached.target).close();
verify(detached.data).close();
assertTrue(detached.hash32.isCancelled());
engine.closeDetachedGenerationRuntime(binding);
}
@Test
public void detachedRuntimeRetirementEvictsCachesOutsideLifecycleLock() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
AtomicInteger retiredRuntime = new AtomicInteger(-1);
AtomicBoolean lifecycleLockHeld = new AtomicBoolean(true);
engine.addGenerationRuntimeRetirementListener(runtimeId -> {
retiredRuntime.set(runtimeId);
lifecycleLockHeld.set(Thread.holdsLock(engine.lifecycleLock));
});
doAnswer(invocation -> {
assertEquals(2, retiredRuntime.get());
return null;
}).when(detached.mode).close();
engine.closeDetachedGenerationRuntime(binding);
assertEquals(2, binding.runtimeId());
assertEquals(2, retiredRuntime.get());
assertTrue(!lifecycleLockHeld.get());
}
@Test
public void detachedRuntimeRejectsNewScopesAfterRetirementBegins() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
AtomicReference<Throwable> rejected = new AtomicReference<>();
engine.addGenerationRuntimeRetirementListener(runtimeId -> {
try {
engine.openGenerationRuntimeScope(binding);
} catch (Throwable failure) {
rejected.set(failure);
}
});
engine.closeDetachedGenerationRuntime(binding);
assertTrue(rejected.get() instanceof IllegalStateException);
assertTrue(rejected.get().getMessage().contains("retiring"));
}
@Test
public void closedDetachedScopeFallsBackToTheBaseRuntime() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, detached.runtime);
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
assertSame(detached.target, engine.getTarget());
engine.closeDetachedGenerationRuntime(binding);
assertSame(active.target, engine.getTarget());
assertSame(active.mantle, engine.getMantle());
}
}
@Test
public void defaultRuntimeSelectionKeepsThePreviousRuntimeRoutable() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding activeBinding = new IrisEngine.GenerationRuntimeBinding(
engine,
active.runtime);
IrisEngine.GenerationRuntimeBinding detachedRuntimeBinding = detachedBinding(engine, detached.runtime);
engine.setDefaultGenerationRuntime(detachedRuntimeBinding);
assertSame(detached.target, engine.getTarget());
assertThrows(
IllegalArgumentException.class,
() -> engine.closeDetachedGenerationRuntime(detachedRuntimeBinding));
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(activeBinding)) {
assertSame(active.target, engine.getTarget());
}
engine.setDefaultGenerationRuntime(activeBinding);
assertSame(active.target, engine.getTarget());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(detachedRuntimeBinding)) {
assertSame(detached.target, engine.getTarget());
}
engine.closeDetachedGenerationRuntime(detachedRuntimeBinding);
assertThrows(
IllegalStateException.class,
() -> engine.openGenerationRuntimeScope(detachedRuntimeBinding));
}
@Test
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
assertTrue(engine.beginShutdown());
engine.closeAttachedGenerationHistoryRuntimeRouter();
engine.closeAttachedGenerationHistoryRuntimeRouter();
verify(router).close();
}
@Test
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
assertTrue(failure.getMessage().contains("immutable Iris generation history"));
assertSame(active.runtime, engine.runtime.generation());
verify(active.complex, never()).close();
}
@Test
public void detachedHistoryCannotFallBackToTheDefaultGenerationRuntime() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
engine.detachGenerationHistoryRuntimeRouter(router);
assertThrows(IllegalStateException.class, () -> engine.openGenerationHistoryCoordinateScope(0, 0));
assertThrows(IllegalStateException.class, () -> engine.getHeight(0, 0));
}
@Test
public void transferredMantleIsNotClosedWithTheRetiredRuntime() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
EngineShutdownSequence shutdownSequence = new EngineShutdownSequence(engine);
Throwable failure = shutdownSequence.closeRuntime(engine.runtime, active.mantle, null);
assertNull(failure);
verify(active.mantle, never()).saveAllNow();
verify(active.mantle, never()).close();
}
@Test
public void ownedAssemblyMantleIsSavedAndClosedOnlyOnce() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
EngineRuntimeBuilder.RuntimeAssembly assembly = new EngineRuntimeBuilder.RuntimeAssembly(
2,
active.target,
Path.of("build", "test-mantles", "assembly"));
assembly.mantle = mock(EngineMantle.class);
assembly.ownsMantle = true;
EngineShutdownSequence shutdownSequence = new EngineShutdownSequence(engine);
assertNull(shutdownSequence.closeAssembly(assembly, null));
assertNull(shutdownSequence.closeAssembly(assembly, null));
verify(assembly.mantle).saveAllNow();
verify(assembly.mantle).close();
}
@Test
public void detachedRuntimeRejectsTheActiveMantleDirectory() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisWorld activeWorld = active.target.getWorld();
when(detached.target.getWorld()).thenReturn(activeWorld);
setField(engine, "closing", new AtomicBoolean(false));
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
assertThrows(
IllegalArgumentException.class,
() -> engine.buildDetachedGenerationRuntime(detached.target, active.mantleStorageDirectory));
verify(detached.data, never()).registerEngine(engine);
}
@Test
public void failedDetachedBuildReleasesTransferredTargetOwnership() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture detached = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisWorld activeWorld = active.target.getWorld();
when(detached.target.getWorld()).thenReturn(activeWorld);
setField(engine, "closing", new AtomicBoolean(false));
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
EngineRuntimeBuilder runtimeBuilder = mock(EngineRuntimeBuilder.class);
setField(engine, "runtimeBuilder", runtimeBuilder);
IllegalStateException failure = new IllegalStateException("build failed");
Path storageDirectory = Path.of("build", "test-mantles", "failed-detached").toAbsolutePath();
when(runtimeBuilder.buildDetachedGenerationRuntime(detached.target, storageDirectory)).thenThrow(failure);
IllegalStateException thrown = assertThrows(
IllegalStateException.class,
() -> engine.buildDetachedGenerationRuntime(detached.target, storageDirectory));
assertSame(failure, thrown);
verify(detached.data).registerEngine(engine);
verify(detached.data).unregisterEngine(engine);
verify(detached.target).close();
verify(detached.data).close();
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
EngineWorldManager worldManager
) throws Exception {
IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS);
setField(engine, "lifecycleLock", new Object());
setField(engine, "generationHistoryRuntimeRouterLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
Set<GenerationRuntime> detached = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>()));
setField(engine, "detachedGenerationRuntimes", detached);
Set<GenerationRuntime> retiring = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>()));
setField(engine, "retiringGenerationRuntimes", retiring);
setField(engine, "generationRuntimeRetirementListeners", new CopyOnWriteArraySet<IntConsumer>());
setField(engine, "shutdownSequence", new EngineShutdownSequence(engine));
setField(engine, "hotloader", new EngineHotloader(engine));
setField(engine, "nativeStructureVolumeMemo", new NativeStructureVolumeMemo());
setField(engine, "closing", new AtomicBoolean(false));
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
engine.runtime = new EngineRuntime(generationRuntime, effects, worldManager);
engine.publishedTarget = generationRuntime.target();
return engine;
}
@SuppressWarnings("unchecked")
private static IrisEngine.GenerationRuntimeBinding detachedBinding(
IrisEngine engine,
GenerationRuntime runtime
) throws Exception {
Field field = IrisEngine.class.getDeclaredField("detachedGenerationRuntimes");
field.setAccessible(true);
Set<GenerationRuntime> detached = (Set<GenerationRuntime>) field.get(engine);
detached.add(runtime);
return new IrisEngine.GenerationRuntimeBinding(engine, runtime);
}
private static RuntimeFixture runtime(
int cacheId,
double objectDensity,
double layerDensity,
double decoratorDensity
) throws IOException {
return runtime(
cacheId,
objectDensity,
layerDensity,
decoratorDensity,
GenerationKernelRegistry.standard().current()
);
}
private static RuntimeFixture runtime(
int cacheId,
double objectDensity,
double layerDensity,
double decoratorDensity,
GenerationKernelRegistry.Version kernelVersion
) throws IOException {
GenerationKernelRegistry.RuntimeKernel runtimeKernel = runtimeKernel(kernelVersion);
return runtime(
cacheId,
objectDensity,
layerDensity,
decoratorDensity,
runtimeKernel,
mock(IrisComplex.class)
);
}
private static RuntimeFixture runtime(
int cacheId,
double objectDensity,
double layerDensity,
double decoratorDensity,
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
IrisComplex complex
) {
IrisWorld world = mock(IrisWorld.class);
IrisData data = mock(IrisData.class);
IrisDimension dimension = mock(IrisDimension.class);
EngineTarget target = mock(EngineTarget.class);
when(target.getWorld()).thenReturn(world);
when(target.getDimension()).thenReturn(dimension);
when(target.getData()).thenReturn(data);
when(target.getBurster()).thenReturn(MultiBurst.burst);
UpperDimensionContext upperContext = mock(UpperDimensionContext.class);
DimensionStackContext dimensionStackContext = mock(DimensionStackContext.class);
EngineMode mode = mock(EngineMode.class);
EngineMantle mantle = mock(EngineMantle.class);
Path mantleStorageDirectory = Path.of("build", "test-mantles", Integer.toString(cacheId)).toAbsolutePath();
CompletableFuture<Long> hash32 = new CompletableFuture<>();
GenerationRuntime runtime = new GenerationRuntime(
cacheId,
target,
data,
dimension,
runtimeKernel.version(),
runtimeKernel,
mock(SeedManager.class),
null,
complex,
upperContext,
dimensionStackContext,
mode,
mantle,
mantleStorageDirectory,
hash32,
new BiomeMaxes(objectDensity, layerDensity, decoratorDensity));
return new RuntimeFixture(
runtime,
target,
data,
dimension,
complex,
upperContext,
dimensionStackContext,
mode,
mantle,
mantleStorageDirectory,
hash32);
}
private static GenerationKernelRegistry.RuntimeKernel runtimeKernel(
GenerationKernelRegistry.Version version
) throws IOException {
if (GenerationKernelRegistry.standard().supports(
version.generatorAbi(),
version.rngVersion(),
version.seedDerivationVersion())) {
return GenerationKernelRegistry.standard().select(version);
}
return new GenerationKernelRegistry.RuntimeKernel(
version,
"f".repeat(64),
(engine, transitionPlan) -> mock(IrisComplex.class)
);
}
private static void setField(Object target, String name, Object value) throws Exception {
Field field = IrisEngine.class.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
}
private record RuntimeFixture(
GenerationRuntime runtime,
EngineTarget target,
IrisData data,
IrisDimension dimension,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineMode mode,
EngineMantle mantle,
Path mantleStorageDirectory,
CompletableFuture<Long> hash32
) {
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.junit.Test;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class UpperDimensionTransitionTest {
@Test
public void gapLimitedTargetDoesNotCreateAnIntermediateCeilingDip() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("main");
when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 139D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 75D + 4D * x));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.terrainSampleAt(0, 8)).thenReturn(sample(75D, 0D));
when(plan.terrainSampleAt(8, 8)).thenReturn(sample(75D, 0.5D));
when(plan.terrainSampleAt(16, 8)).thenReturn(sample(75D, 1D));
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(256);
UpperDimensionContext context = UpperDimensionContext.create(engine, dimension);
assertEquals(180, context.getEffectiveSurfaceY(0, 8));
assertEquals(175, context.getEffectiveSurfaceY(8, 8));
assertEquals(171, context.getEffectiveSurfaceY(16, 8));
}
@Test
public void blendsFrozenEffectiveCeilingTowardTheUnblendedSelfReference() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("main");
when(dimension.getUpperDimensionGap()).thenReturn(32);
IrisComplex complex = mock(IrisComplex.class);
when(complex.getUnblendedNaturalHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 80D));
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.terrainSampleAt(-17, 8)).thenReturn(sample(40D, 0.5D));
when(complex.getTransitionGenerationPlan()).thenReturn(plan);
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getHeight()).thenReturn(256);
UpperDimensionContext context = UpperDimensionContext.create(engine, dimension);
assertEquals(195, context.getEffectiveSurfaceY(-17, 8));
verify(complex, never()).getNaturalHeightStream();
verify(complex, never()).getNaturalTrueBiomeStream();
}
@Test
public void preservesBoundaryAndCurrentEndpointsAndFixedGap() {
UpperDimensionContext.CeilingLayout layout = new UpperDimensionContext.CeilingLayout(384, 32);
double frozen = UpperDimensionContext.blendCeilingDepth(180D, sample(60D, 0D));
double current = UpperDimensionContext.blendCeilingDepth(180D, sample(60D, 1D));
assertEquals(323, UpperDimensionContext.effectiveSurfaceY(frozen, layout, 100));
assertEquals(203, UpperDimensionContext.effectiveSurfaceY(current, layout, 100));
assertEquals(252, UpperDimensionContext.effectiveSurfaceY(current, layout, 220));
assertEquals(188, -64 + UpperDimensionContext.effectiveSurfaceY(current, layout, 220));
assertEquals(384, UpperDimensionContext.effectiveSurfaceY(current, layout, Integer.MAX_VALUE));
}
@Test
public void absentCeilingsRemainOutsideTheHunkAndAppearGradually() {
UpperDimensionContext.CeilingLayout layout = new UpperDimensionContext.CeilingLayout(256, 32);
assertEquals(256, UpperDimensionContext.effectiveSurfaceY(
UpperDimensionContext.blendCeilingDepth(80D, sample(0D, 0D)), layout, 64));
assertEquals(215, UpperDimensionContext.effectiveSurfaceY(
UpperDimensionContext.blendCeilingDepth(80D, sample(0D, 0.5D)), layout, 64));
assertEquals(215, UpperDimensionContext.effectiveSurfaceY(
UpperDimensionContext.blendCeilingDepth(0D, sample(80D, 0.5D)), layout, 64));
assertEquals(256, UpperDimensionContext.effectiveSurfaceY(
UpperDimensionContext.blendCeilingDepth(0D, sample(80D, 1D)), layout, 64));
assertEquals(256, UpperDimensionContext.effectiveSurfaceY(0.49D, layout, 64));
assertEquals(254, UpperDimensionContext.effectiveSurfaceY(0.51D, layout, 64));
}
@Test
public void rejectsInvalidCeilingLayouts() {
assertThrows(IllegalArgumentException.class, () -> new UpperDimensionContext.CeilingLayout(0, 32));
assertThrows(IllegalArgumentException.class, () -> new UpperDimensionContext.CeilingLayout(256, -1));
}
private static TransitionGenerationPlan.TerrainSample sample(double depth, double weight) {
TerrainBoundarySignature signature = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(15, 8, 64, 64, OptionalInt.empty(),
depth == 0D ? OptionalInt.empty() : OptionalInt.of((int) depth)),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 4, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})));
return new TransitionGenerationPlan.TerrainSample(8D, weight, 0D, signature, 64D, 64D, depth);
}
}
@@ -1,8 +1,10 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.spi.IrisPlatform;
@@ -23,14 +25,18 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -45,7 +51,9 @@ public class IrisBiomeActuatorCoordinateTest {
@SuppressWarnings("unchecked")
public void derivativeScatterUsesEachWorldColumnCoordinate() {
bindPlatform();
Engine engine = engine();
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsNewDiscreteContentAt(anyInt(), anyInt())).thenReturn(true);
Engine engine = engine(complex);
IrisBiomeActuator actuator = new IrisBiomeActuator(engine);
Hunk<PlatformBiome> output = mock(Hunk.class);
when(output.getWidth()).thenReturn(2);
@@ -65,6 +73,7 @@ public class IrisBiomeActuatorCoordinateTest {
ChunkedDataCache<IrisBiome> biomeCache = mock(ChunkedDataCache.class);
when(biomeCache.get(anyInt(), anyInt())).thenReturn(biome);
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
when(context.getBiome()).thenReturn(biomeCache);
actuator.onActuate(100, -200, output, false, context);
@@ -85,8 +94,83 @@ public class IrisBiomeActuatorCoordinateTest {
verifyNoMoreInteractions(mantle);
}
@Test
@SuppressWarnings("unchecked")
private Engine engine() {
public void transitionColumnEmitsFrozenPhysicalKeyWithoutResolvingANewBiome() {
bindPlatform();
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
when(terrainSample.historicalPhysicalBiomeKeyAt(0))
.thenReturn(Optional.of("iris:old-physical"));
Engine engine = engine(complex);
IrisBiomeActuator actuator = new IrisBiomeActuator(engine);
Hunk<PlatformBiome> output = mock(Hunk.class);
when(output.getWidth()).thenReturn(1);
when(output.getDepth()).thenReturn(1);
when(output.getHeight()).thenReturn(1);
ChunkedDataCache<IrisBiome> biomeCache = mock(ChunkedDataCache.class);
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
when(context.getBiome()).thenReturn(biomeCache);
actuator.onActuate(100, -200, output, false, context);
verify(biomeCache, never()).get(anyInt(), anyInt());
verify(IrisPlatforms.get().registries()).biome("iris:old-physical");
verify(IrisPlatforms.get().biomeWriter(), never()).biomeIdFor(anyString());
Mantle<Matter> mantle = engine.getMantle().getMantle();
verify(mantle).set(eq(100), eq(0), eq(-200), argThat(value -> {
MatterBiomeInject injection = (MatterBiomeInject) value;
return !injection.isCustom() && "iris:old-physical".equals(injection.getBiomeKey());
}));
}
@Test
@SuppressWarnings("unchecked")
public void transitionColumnRestoresEveryFrozenVerticalBiomeRange() {
bindPlatform();
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsNewDiscreteContentAt(100, -200)).thenReturn(false);
TransitionGenerationPlan transitionPlan = mock(TransitionGenerationPlan.class);
TransitionGenerationPlan.TerrainSample terrainSample = mock(TransitionGenerationPlan.TerrainSample.class);
when(complex.getTransitionGenerationPlan()).thenReturn(transitionPlan);
when(transitionPlan.terrainSampleAt(100, -200)).thenReturn(terrainSample);
when(terrainSample.historicalPhysicalBiomeKeyAt(anyInt())).thenAnswer(invocation ->
Optional.of(invocation.<Integer>getArgument(0) < -60
? "iris:old-bottom"
: "iris:old-top"));
Engine engine = engine(complex);
when(engine.getMinHeight()).thenReturn(-64);
IrisBiomeActuator actuator = new IrisBiomeActuator(engine);
Hunk<PlatformBiome> output = mock(Hunk.class);
when(output.getWidth()).thenReturn(1);
when(output.getDepth()).thenReturn(1);
when(output.getHeight()).thenReturn(8);
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
when(context.getBiome()).thenReturn(mock(ChunkedDataCache.class));
actuator.onActuate(100, -200, output, false, context);
verify(output).set(eq(0), eq(0), eq(0), eq(0), eq(3), eq(0), any(PlatformBiome.class));
verify(output).set(eq(0), eq(4), eq(0), eq(0), eq(7), eq(0), any(PlatformBiome.class));
Mantle<Matter> mantle = engine.getMantle().getMantle();
verify(mantle).set(eq(100), eq(0), eq(-200), argThat(value -> {
MatterBiomeInject injection = (MatterBiomeInject) value;
return "iris:old-bottom".equals(injection.getBiomeKey());
}));
verify(mantle).set(eq(100), eq(4), eq(-200), argThat(value -> {
MatterBiomeInject injection = (MatterBiomeInject) value;
return "iris:old-top".equals(injection.getBiomeKey());
}));
}
@SuppressWarnings("unchecked")
private Engine engine(IrisComplex complex) {
Mantle<Matter> mantle = mock(Mantle.class);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getMantle()).thenReturn(mantle);
@@ -96,6 +180,7 @@ public class IrisBiomeActuatorCoordinateTest {
when(engine.getCacheID()).thenReturn(1);
when(engine.getSeedManager()).thenReturn(seedManager);
when(engine.getMantle()).thenReturn(engineMantle);
when(engine.getComplex()).thenReturn(complex);
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
return engine;
}
@@ -9,9 +9,14 @@ import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
import java.util.ArrayList;
import java.util.List;
@@ -23,6 +28,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
@@ -31,6 +37,28 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisTerrainNormalActuatorHydrologyTest {
private IrisPlatform previousPlatform;
@Before
public void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(block);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
PlatformBlockState water = mock(PlatformBlockState.class);
@@ -5,11 +5,16 @@ import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.IrisComplex;
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.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.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.history.GenerationSemanticIndex;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.project.stream.ProceduralStream;
@@ -25,6 +30,7 @@ import java.io.File;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
@@ -39,6 +45,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -94,6 +101,7 @@ public class HintedLocatorTest {
when(engine.getFocusRegion()).thenReturn(null);
when(engine.isClosed()).thenReturn(false);
when(engine.acquireGenerationLease(any(String.class))).thenReturn(GenerationSessionLease.noop());
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
}
@@ -463,6 +471,267 @@ public class HintedLocatorTest {
assertTrue(elapsed < 10_000);
}
@Test
public void locatorMergesRecordedAndActiveCandidatesByDistance() throws Exception {
Position2 origin = new Position2(0, 0);
Position2 activeChunk = new Position2(1, 0);
Locator<String> locator = new Locator<>() {
@Override
public boolean matches(Engine ignoredEngine, Position2 chunk) {
return activeChunk.equals(chunk);
}
@Override
public SearchCandidate nearestRecordedCandidate(
Engine ignoredEngine,
Position2 ignoredOrigin,
int ignoredRadius
) {
return SearchCandidate.atChunkCenter(new Position2(20, 0));
}
};
Position2 result = locator.find(engine, origin, 60_000, ignored -> { }).get();
assertEquals(activeChunk, result);
}
@Test
public void impossibleTargetWithNoHistoricalFallbackReturnsWithoutScanning() throws Exception {
IrisEngine routedEngine = mock(IrisEngine.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(routedEngine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(routedEngine.acquireGenerationLease(any(String.class))).thenReturn(GenerationSessionLease.noop());
when(router.history()).thenReturn(history);
when(history.hasHistoricalFallbackInSquare(anyInt(), anyInt(), anyInt())).thenReturn(false);
Locator<String> locator = new HintedLocator<>((ignoredEngine, chunk) -> {
throw new AssertionError("Impossible target must not enter procedural search.");
}, ignoredEngine -> HintedLocator.SearchPlan.impossible());
assertNull(locator.find(routedEngine, new Position2(0, 0), 60_000, ignored -> { })
.get(3, TimeUnit.SECONDS));
}
@Test
public void hintedLocatorMergesRecordedAndActiveCandidatesByDistance() throws Exception {
Position2 origin = new Position2(0, 0);
Position2 activeChunk = new Position2(1, 0);
Locator<String> exact = new Locator<>() {
@Override
public boolean matches(Engine ignoredEngine, Position2 chunk) {
return activeChunk.equals(chunk);
}
@Override
public SearchCandidate nearestRecordedCandidate(
Engine ignoredEngine,
Position2 ignoredOrigin,
int ignoredRadius
) {
return SearchCandidate.atChunkCenter(new Position2(20, 0));
}
};
Locator<String> locator = new HintedLocator<>(
exact,
ignoredEngine -> HintedLocator.SearchPlan.unpruned()
);
Position2 result = locator.find(engine, origin, 60_000, ignored -> { }).get();
assertEquals(activeChunk, result);
}
@Test
public void exactBlockPositionsDecideStructureCandidateDistance() {
Position2 origin = new Position2(0, 0);
Locator.SearchCandidate fartherRecorded = new Locator.SearchCandidate(
new Position2(1, 0),
31,
8
);
Locator.SearchCandidate nearerActive = new Locator.SearchCandidate(
new Position2(1, 0),
16,
8
);
assertEquals(nearerActive, Locator.nearer(origin, fartherRecorded, nearerActive));
}
@Test
public void plainLocatorChecksLaterRingsThatCanContainACloserMatch() throws Exception {
Position2 corner = new Position2(-40, -40);
Position2 nearer = new Position2(50, 0);
Locator<String> locator = (ignoredEngine, chunk) -> chunk.equals(corner) || chunk.equals(nearer);
assertEquals(nearer, locator.find(engine, new Position2(0, 0), 10_000, ignored -> { })
.get(15, TimeUnit.SECONDS));
}
@Test
public void fineLocatorChoosesTheSameNearestTieRegardlessOfWorkerOrder() throws Exception {
Set<Position2> matches = Set.of(new Position2(-12, 0), new Position2(0, -12),
new Position2(0, 12), new Position2(12, 0));
Locator<String> locator = new HintedLocator<>((ignoredEngine, chunk) -> matches.contains(chunk),
ignoredEngine -> HintedLocator.SearchPlan.unpruned());
assertEquals(new Position2(-12, 0), locator.find(engine, new Position2(0, 0), 10_000, ignored -> { })
.get(15, TimeUnit.SECONDS));
}
@Test
public void coarseRefinementRanksMatchesFromTheSearchOrigin() throws Exception {
Locator<String> locator = new HintedLocator<>(
(ignoredEngine, chunk) -> chunk.equals(new Position2(8, 0)) || chunk.equals(new Position2(5, 0)),
ignoredEngine -> HintedLocator.SearchPlan.of((x, z) -> x == 136 && z == 8, 4, null));
assertEquals(new Position2(5, 0), locator.find(engine, new Position2(0, 0), 10_000, ignored -> { })
.get(15, TimeUnit.SECONDS));
}
@Test
public void ringCoordinatesNeverWrapAcrossTheBlockCoordinateLimit() {
KList<Position2> cells = new KList<>();
int maximumChunk = Integer.MAX_VALUE >> 4;
HintedLocator.appendRing(cells, new Position2(maximumChunk, maximumChunk), 1, 1);
assertEquals(Set.of(new Position2(maximumChunk - 1, maximumChunk - 1),
new Position2(maximumChunk - 1, maximumChunk),
new Position2(maximumChunk, maximumChunk - 1)), new HashSet<>(cells));
}
@Test
public void transitionGatePreventsProceduralLocatorEvaluation() {
AtomicInteger evaluations = new AtomicInteger();
HistoryAwareLocator<String> locator = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:temperate",
(ignoredEngine, ignoredChunk) -> {
evaluations.incrementAndGet();
return true;
}
);
when(complex.allowsNewGenerationChunk(3, 4)).thenReturn(false);
assertFalse(locator.matchesForSearch(engine, new Position2(3, 4)));
assertEquals(0, evaluations.get());
}
@Test
public void semanticRecordsAndOwnershipSelectTheOnlyTruthfulLocatorSource() throws Exception {
IrisEngine routedEngine = mock(IrisEngine.class);
IrisComplex routedComplex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
AtomicInteger scopeDepth = new AtomicInteger();
AtomicInteger evaluations = new AtomicInteger();
ChunkGenerationSemantics negative = semantics(1, "iris:other");
ChunkGenerationSemantics positive = semantics(2, "iris:target");
HistoryAwareLocator<String> locator = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:target",
(ignoredEngine, chunk) -> {
evaluations.incrementAndGet();
return scopeDepth.get() > 0 && (chunk.getX() == 0 || chunk.getX() == 3);
}
);
when(routedEngine.getComplex()).thenReturn(routedComplex);
when(routedEngine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(history.semantics(anyInt(), anyInt())).thenAnswer(invocation -> switch (
invocation.<Integer>getArgument(0)
) {
case 1 -> Optional.of(negative);
case 2 -> Optional.of(positive);
default -> Optional.empty();
});
when(history.isHistoricallyOwned(anyInt(), anyInt())).thenAnswer(invocation -> {
int chunkX = invocation.getArgument(0);
return chunkX >= 0 && chunkX <= 2;
});
when(history.isActiveUnowned(anyInt(), anyInt())).thenAnswer(invocation ->
invocation.<Integer>getArgument(0) == 3);
when(routedComplex.allowsNewGenerationChunk(anyInt(), anyInt())).thenAnswer(invocation ->
invocation.<Integer>getArgument(0) == 3);
when(routedEngine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
GenerationHistoryRuntimeRouter.CoordinateScope scope =
mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
scopeDepth.incrementAndGet();
doAnswer(close -> {
scopeDepth.decrementAndGet();
return null;
}).when(scope).close();
return scope;
});
assertTrue(locator.matches(routedEngine, new Position2(0, 0)));
assertTrue(locator.matchesForSearch(routedEngine, new Position2(0, 0)));
assertFalse(locator.matches(routedEngine, new Position2(1, 0)));
assertFalse(locator.matchesForSearch(routedEngine, new Position2(1, 0)));
assertTrue(locator.matches(routedEngine, new Position2(2, 0)));
assertFalse(locator.matchesForSearch(routedEngine, new Position2(2, 0)));
assertTrue(locator.matches(routedEngine, new Position2(3, 0)));
assertTrue(locator.matchesForSearch(routedEngine, new Position2(3, 0)));
assertEquals(4, evaluations.get());
assertEquals(0, scopeDepth.get());
}
@Test
public void historicalFallbackBypassesCurrentPackCoarseHints() throws Exception {
IrisEngine routedEngine = mock(IrisEngine.class);
IrisComplex routedComplex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
AtomicInteger scopeDepth = new AtomicInteger();
HistoryAwareLocator<String> exact = new HistoryAwareLocator<>(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:old_only",
(ignoredEngine, chunk) -> chunk.equals(new Position2(1, 0)) && scopeDepth.get() > 0
);
Locator<String> locator = new HintedLocator<>(
exact,
ignoredEngine -> HintedLocator.SearchPlan.of((blockX, blockZ) -> false, 4, null)
);
when(routedEngine.getComplex()).thenReturn(routedComplex);
when(routedEngine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(routedEngine.isClosed()).thenReturn(false);
when(routedEngine.acquireGenerationLease(any(String.class))).thenReturn(GenerationSessionLease.noop());
when(router.history()).thenReturn(history);
when(history.findRecorded(any())).thenReturn(Optional.empty());
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
when(history.isHistoricallyOwned(anyInt(), anyInt())).thenAnswer(invocation ->
invocation.<Integer>getArgument(0) == 1
&& invocation.<Integer>getArgument(1) == 0);
when(history.hasHistoricalFallbackInSquare(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
Math.abs((long) invocation.<Integer>getArgument(0) - 1L) <= invocation.<Integer>getArgument(2)
&& Math.abs((long) invocation.<Integer>getArgument(1)) <= invocation.<Integer>getArgument(2));
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(false);
when(routedComplex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(false);
when(routedEngine.openGenerationHistoryCoordinateScope(anyInt(), anyInt())).thenAnswer(invocation -> {
GenerationHistoryRuntimeRouter.CoordinateScope scope =
mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
scopeDepth.incrementAndGet();
doAnswer(close -> {
scopeDepth.decrementAndGet();
return null;
}).when(scope).close();
return scope;
});
Position2 result = locator.find(
routedEngine,
new Position2(0, 0),
60_000,
ignored -> { }
).get(10, TimeUnit.SECONDS);
assertEquals(new Position2(1, 0), result);
assertEquals(0, scopeDepth.get());
}
@Test
public void startingAnotherSearchDoesNotCancelTheActiveRequest() throws Exception {
CountDownLatch planning = new CountDownLatch(1);
@@ -492,6 +761,13 @@ public class HintedLocatorTest {
assertEquals(origin, first.get(10, TimeUnit.SECONDS));
}
private static ChunkGenerationSemantics semantics(int chunkX, String regionKey) {
return ChunkGenerationSemantics.builder(chunkX, 0, 1L)
.addRegion(regionKey)
.seal()
.build();
}
@Test
public void findLocatesObjectThroughCoarseCascade() throws Exception {
IrisRegion regionA = region("reg_a");
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisDimension;
@@ -41,6 +42,7 @@ import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.StructureDistribution;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.junit.Test;
import java.util.Set;
@@ -53,6 +55,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -447,6 +450,14 @@ public class IrisStructureLocatorContractTest {
@Test
public void locateRanksEditableStartsByResolvedBlockOrigin() {
Engine engine = densityEngine(1.0, false, -64, 384, -2032, 2032);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(true);
@SuppressWarnings("unchecked")
ProceduralStream<Double> riverHeight = mock(ProceduralStream.class);
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHeight);
when(riverHeight.get(anyDouble(), anyDouble())).thenReturn(63D);
when(engine.getAllBiomes()).thenReturn(new KList<>());
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(128);
when(engine.getDimension().getFluidHeight()).thenReturn(63);
IrisStructurePlacement placement = engine.getDimension().getStructures().get(0);
@@ -1,11 +1,24 @@
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.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.IrisStructureTerrain;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@@ -14,17 +27,34 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NativeStructureOwnershipStoreTest {
private static final String FINGERPRINT = "34".repeat(32);
@BeforeClass
public static void initializeMantleBlockState() throws Exception {
PlatformBlockState air = mock(PlatformBlockState.class);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("AIR")).thenReturn(air);
Class.forName(EngineMantle.class.getName());
}
}
@Test
public void fullReferenceEnvelopeWritesOnlyItsOriginAuthority() {
Engine engine = engine();
@@ -400,6 +430,74 @@ public class NativeStructureOwnershipStoreTest {
assertEquals(2, storage.flushes);
}
@Test
public void queuedOwnershipFlushUsesMantleCapturedBeforeRuntimePromotion() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
IrisEngine.GenerationRuntimeBinding bindingA = runtimeBinding(11);
IrisEngine.GenerationRuntimeBinding bindingB = runtimeBinding(12);
MantleFixture mantleA = mantleFixture();
MantleFixture mantleB = mantleFixture();
AtomicReference<GenerationHistoryRuntimeRouter.RuntimeOwnership> ownership =
new AtomicReference<>(new GenerationHistoryRuntimeRouter.RuntimeOwnership(1L, bindingA));
AtomicReference<EngineMantle> currentMantle = new AtomicReference<>(mantleA.engineMantle());
AtomicInteger currentRuntime = new AtomicInteger(11);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.currentRuntimeOwnership()).thenAnswer(ignored -> Optional.of(ownership.get()));
when(engine.getMantle()).thenAnswer(ignored -> currentMantle.get());
when(engine.getCacheID()).thenAnswer(ignored -> currentRuntime.get());
NativeStructureOwnershipStore.MantleStorage storage =
new NativeStructureOwnershipStore.MantleStorage(engine);
NativeStructureOwnershipRecord record = record("test:captured_mantle", 4, -5, 109L);
storage.write(NativeStructureOwnershipStore.pack(4, -5), record);
ownership.set(new GenerationHistoryRuntimeRouter.RuntimeOwnership(2L, bindingB));
currentMantle.set(mantleB.engineMantle());
currentRuntime.set(12);
assertTrue(storage.flush());
verify(mantleA.mantle(), times(2)).getChunk(4, -5);
verify(mantleA.mantle()).saveIdleTectonicPlates(any());
verify(mantleB.mantle(), never()).getChunk(anyInt(), anyInt());
verify(mantleB.mantle(), never()).saveIdleTectonicPlates(any());
storage.close();
}
@Test
public void flushPartitionsPendingOwnershipByCapturedRuntime() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
IrisEngine.GenerationRuntimeBinding bindingA = runtimeBinding(21);
IrisEngine.GenerationRuntimeBinding bindingB = runtimeBinding(22);
MantleFixture mantleA = mantleFixture();
MantleFixture mantleB = mantleFixture();
AtomicReference<GenerationHistoryRuntimeRouter.RuntimeOwnership> ownership =
new AtomicReference<>(new GenerationHistoryRuntimeRouter.RuntimeOwnership(3L, bindingA));
AtomicReference<EngineMantle> currentMantle = new AtomicReference<>(mantleA.engineMantle());
AtomicInteger currentRuntime = new AtomicInteger(21);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.currentRuntimeOwnership()).thenAnswer(ignored -> Optional.of(ownership.get()));
when(engine.getMantle()).thenAnswer(ignored -> currentMantle.get());
when(engine.getCacheID()).thenAnswer(ignored -> currentRuntime.get());
NativeStructureOwnershipStore.MantleStorage storage =
new NativeStructureOwnershipStore.MantleStorage(engine);
NativeStructureOwnershipRecord recordA = record("test:partition_a", -7, 8, 110L);
NativeStructureOwnershipRecord recordB = record("test:partition_b", 9, -10, 111L);
storage.write(NativeStructureOwnershipStore.pack(-7, 8), recordA);
ownership.set(new GenerationHistoryRuntimeRouter.RuntimeOwnership(4L, bindingB));
currentMantle.set(mantleB.engineMantle());
currentRuntime.set(22);
storage.write(NativeStructureOwnershipStore.pack(9, -10), recordB);
assertTrue(storage.flush());
verify(mantleA.mantle(), times(2)).getChunk(-7, 8);
verify(mantleA.mantle()).saveIdleTectonicPlates(any());
verify(mantleB.mantle(), times(2)).getChunk(9, -10);
verify(mantleB.mantle()).saveIdleTectonicPlates(any());
storage.close();
}
private static Engine engine() {
Engine engine = mock(Engine.class);
when(engine.isClosing()).thenReturn(false);
@@ -407,6 +505,30 @@ public class NativeStructureOwnershipStoreTest {
return engine;
}
private static IrisEngine.GenerationRuntimeBinding runtimeBinding(int runtimeId) {
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
when(binding.runtimeId()).thenReturn(runtimeId);
return binding;
}
@SuppressWarnings("unchecked")
private static MantleFixture mantleFixture() {
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Matter section = mock(Matter.class);
MatterSlice<NativeStructureOwnershipBundle> slice = mock(MatterSlice.class);
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
when(chunk.getOrCreate(0)).thenReturn(section);
when(section.<NativeStructureOwnershipBundle>slice(
NativeStructureOwnershipBundle.class)).thenReturn(slice);
when(slice.get(0, 0, 0)).thenReturn(null);
when(mantle.saveIdleTectonicPlates(any())).thenReturn(Set.of());
return new MantleFixture(engineMantle, mantle);
}
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
long placementIdentity) {
return record(key, originX, originZ, placementIdentity, new IrisStructureTerrain(), 8);
@@ -583,4 +705,7 @@ public class NativeStructureOwnershipStoreTest {
}
}
}
private record MantleFixture(EngineMantle engineMantle, Mantle<Matter> mantle) {
}
}
@@ -145,6 +145,18 @@ public class NativeStructurePlacementPlannerTest {
assertSame(expected, plans.getFirst().placement());
}
@Test
public void transitionBandRejectsNativeStructureStarts() {
Engine engine = engine(77L, -64, 384, 150);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.allowsNewGenerationChunk(0, 0)).thenReturn(false);
IrisStructurePlacement placement = nativePlacement();
assertNull(NativeStructurePlacementPlanner.planAt(engine, placement, 0, 0));
assertTrue(NativeStructurePlacementPlanner.plansAt(engine, 0, 0).isEmpty());
}
@Test
public void weightedNativeSelectionPreservesAuthoredSourceOrder() {
KList<IrisNativeStructure> first = new KList<>();
@@ -8,15 +8,18 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructureVolumeMemoTest {
@Test
public void repeatedQueriesInsideOneChunkAskThePlatformOnce() {
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 15, 78, 15));
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
Engine engine = engine(1);
KList<NativeStructureVolume> first = memo.volumes(null, hooks, 2, 2, 6, 6);
KList<NativeStructureVolume> second = memo.volumes(null, hooks, 8, 8, 12, 12);
KList<NativeStructureVolume> first = memo.volumes(engine, hooks, 2, 2, 6, 6);
KList<NativeStructureVolume> second = memo.volumes(engine, hooks, 8, 8, 12, 12);
assertEquals(1, hooks.queries().size());
assertEquals(first, second);
@@ -27,7 +30,7 @@ public class NativeStructureVolumeMemoTest {
RecordingHooks hooks = new RecordingHooks();
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
memo.volumes(null, hooks, 20, 36, 21, 37);
memo.volumes(engine(1), hooks, 20, 36, 21, 37);
assertEquals(1, hooks.queries().size());
assertEquals(List.of(16, 32, 31, 47), hooks.queries().getFirst());
@@ -39,7 +42,7 @@ public class NativeStructureVolumeMemoTest {
RecordingHooks hooks = new RecordingHooks(shared);
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
KList<NativeStructureVolume> volumes = memo.volumes(null, hooks, 12, 12, 20, 20);
KList<NativeStructureVolume> volumes = memo.volumes(engine(1), hooks, 12, 12, 20, 20);
assertEquals(4, hooks.queries().size());
assertEquals(1, volumes.size());
@@ -51,7 +54,7 @@ public class NativeStructureVolumeMemoTest {
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 3, 78, 3));
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
assertTrue(memo.volumes(null, hooks, 5, 5, 9, 9).isEmpty());
assertTrue(memo.volumes(engine(1), hooks, 5, 5, 9, 9).isEmpty());
}
@Test
@@ -59,9 +62,10 @@ public class NativeStructureVolumeMemoTest {
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 15, 78, 15));
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
memo.volumes(null, hooks, 2, 2, 6, 6);
Engine engine = engine(1);
memo.volumes(engine, hooks, 2, 2, 6, 6);
memo.clear();
memo.volumes(null, hooks, 2, 2, 6, 6);
memo.volumes(engine, hooks, 2, 2, 6, 6);
assertEquals(2, hooks.queries().size());
}
@@ -70,7 +74,29 @@ public class NativeStructureVolumeMemoTest {
public void missingHooksResolveNoVolumes() {
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
assertTrue(memo.volumes(null, null, 0, 0, 15, 15).isEmpty());
assertTrue(memo.volumes(engine(1), null, 0, 0, 15, 15).isEmpty());
}
@Test
public void evictingOneRuntimePreservesOtherRuntimeBuckets() {
RecordingHooks hooks = new RecordingHooks(volume(0, 64, 0, 15, 78, 15));
NativeStructureVolumeMemo memo = new NativeStructureVolumeMemo();
Engine firstRuntime = engine(1);
Engine secondRuntime = engine(2);
memo.volumes(firstRuntime, hooks, 2, 2, 6, 6);
memo.volumes(secondRuntime, hooks, 2, 2, 6, 6);
memo.evictRuntime(1);
memo.volumes(secondRuntime, hooks, 2, 2, 6, 6);
memo.volumes(firstRuntime, hooks, 2, 2, 6, 6);
assertEquals(3, hooks.queries().size());
}
private static Engine engine(int runtimeId) {
Engine engine = mock(Engine.class);
when(engine.getCacheID()).thenReturn(runtimeId);
return engine;
}
private static NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
@@ -0,0 +1,260 @@
package art.arcane.iris.engine.history;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class ChunkGenerationOwnershipTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void unassignedChunksUseTheSuppliedCurrentActivation() throws Exception {
Path directory = temporaryFolder.newFolder("fallback").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
assertEquals(4L, ownership.resolve(0, 0, 4L));
assertEquals(9L, ownership.resolve(0, 0, 9L));
assertFalse(ownership.isExplicitlyAssigned(0, 0));
assertThrows(IllegalStateException.class, () -> ownership.explicitActivation(0, 0));
}
@Test
public void boundedQueryOnlyVisitsAssignedChunksInsideTheSquare() throws Exception {
Path directory = temporaryFolder.newFolder("bounded-query").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(-33, 0, 1L);
ownership.assign(-32, 1, 2L);
ownership.assign(-31, 2, 3L);
ownership.assign(8192, 8192, 4L);
ownership.persist();
ChunkGenerationOwnership loaded = ChunkGenerationOwnership.load(directory);
Set<Long> visited = new HashSet<>();
assertFalse(loaded.anyMatchingInSquare(-33, 0, 1, (x, z, activation) -> {
visited.add(ChunkGenerationOwnership.packChunk(x, z));
return false;
}));
assertEquals(Set.of(ChunkGenerationOwnership.packChunk(-33, 0),
ChunkGenerationOwnership.packChunk(-32, 1)), visited);
assertTrue(loaded.anyMatchingInSquare(-32, 1, 0, (x, z, activation) -> activation == 2L));
assertFalse(loaded.anyMatchingInSquare(0, 0, 16, (x, z, activation) -> {
throw new AssertionError("Unassigned chunks must not be evaluated.");
}));
assertThrows(IllegalArgumentException.class,
() -> loaded.anyMatchingInSquare(0, 0, -1, (x, z, activation) -> true));
}
@Test
public void sparseWholeCoordinateQueryUsesTheCatalogWithoutOverflow() throws Exception {
Path directory = temporaryFolder.newFolder("whole-coordinate-query").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(Integer.MAX_VALUE, Integer.MAX_VALUE, 1L);
ownership.assign(Integer.MIN_VALUE, Integer.MIN_VALUE, 2L);
assertTrue(ownership.anyMatchingInSquare(0, 0, Integer.MAX_VALUE,
(x, z, activation) -> activation == 1L));
assertFalse(ownership.anyMatchingInSquare(0, 0, Integer.MAX_VALUE,
(x, z, activation) -> activation == 2L));
assertTrue(ownership.anyMatchingInSquare(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE,
(x, z, activation) -> activation == 2L));
}
@Test
public void assignmentsPersistAcrossEverySignedRegionBoundary() throws Exception {
Path directory = temporaryFolder.newFolder("boundaries").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
int[][] coordinates = {
{-33, -33},
{-32, -32},
{-1, -1},
{0, 0},
{31, 31},
{32, 32}
};
for (int index = 0; index < coordinates.length; index++) {
ownership.assign(coordinates[index][0], coordinates[index][1], index + 1L);
}
assertEquals(4, ownership.persist());
assertEquals(0, ownership.persist());
ChunkGenerationOwnership loaded = ChunkGenerationOwnership.load(directory);
assertEquals(coordinates.length, loaded.explicitChunkCount());
for (int index = 0; index < coordinates.length; index++) {
int chunkX = coordinates[index][0];
int chunkZ = coordinates[index][1];
assertEquals(index + 1L, loaded.resolve(chunkX, chunkZ, 99L));
assertTrue(loaded.isExplicitlyAssigned(chunkX, chunkZ));
}
}
@Test
public void assignmentCannotReplaceHistoricalOwnership() throws Exception {
Path directory = temporaryFolder.newFolder("immutable").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
assertTrue(ownership.assign(8, -2, 41L));
assertFalse(ownership.assign(8, -2, 41L));
assertThrows(IllegalStateException.class, () -> ownership.assign(8, -2, 42L));
assertEquals(41L, ownership.explicitActivation(8, -2));
}
@Test
public void bulkConflictDoesNotPartiallyAssignInventory() throws Exception {
Path directory = temporaryFolder.newFolder("bulk").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(0, 0, 3L);
WorldChunkInventory inventory = WorldChunkInventory.ofPackedChunks(
ChunkGenerationOwnership.packChunk(0, 0),
ChunkGenerationOwnership.packChunk(1, 0)
);
assertThrows(IllegalStateException.class, () -> ownership.assignAll(inventory, 4L));
assertFalse(ownership.isExplicitlyAssigned(1, 0));
assertEquals(1, ownership.explicitChunkCount());
assertEquals(1, ownership.assignAll(inventory, 3L));
assertEquals(3L, ownership.explicitActivation(1, 0));
}
@Test
public void cutoverAssignmentOnlyMaterializesPreviouslyUnassignedChunks() throws Exception {
Path directory = temporaryFolder.newFolder("cutover").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(-1, 0, 2L);
WorldChunkInventory inventory = WorldChunkInventory.ofPackedChunks(
ChunkGenerationOwnership.packChunk(-1, 0),
ChunkGenerationOwnership.packChunk(0, 0),
ChunkGenerationOwnership.packChunk(1, 0)
);
assertEquals(2, ownership.assignUnassigned(inventory, 3L));
assertEquals(2L, ownership.explicitActivation(-1, 0));
assertEquals(3L, ownership.explicitActivation(0, 0));
assertEquals(3L, ownership.explicitActivation(1, 0));
}
@Test
public void snapshotUsesTheStablePackedCoordinateContract() throws Exception {
Path directory = temporaryFolder.newFolder("snapshot").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(Integer.MIN_VALUE, Integer.MAX_VALUE, 1L);
ownership.assign(-1, 0, 2L);
ownership.assign(32, -33, 3L);
long[] expected = {
ChunkGenerationOwnership.packChunk(Integer.MIN_VALUE, Integer.MAX_VALUE),
ChunkGenerationOwnership.packChunk(-1, 0),
ChunkGenerationOwnership.packChunk(32, -33)
};
Arrays.sort(expected);
long[] snapshot = ownership.snapshotExplicitChunkKeys();
assertArrayEquals(expected, snapshot);
for (long packed : snapshot) {
assertEquals(packed, ChunkGenerationOwnership.packChunk(
ChunkGenerationOwnership.chunkX(packed),
ChunkGenerationOwnership.chunkZ(packed)
));
}
}
@Test
public void loadedLookupsDecodeOnlyTheRequestedRegion() throws Exception {
Path directory = temporaryFolder.newFolder("lazy-load").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
ownership.assign(-65, 63, 17L);
ownership.persist();
ChunkGenerationOwnership loaded = ChunkGenerationOwnership.load(directory);
assertEquals(0, loaded.cachedRegionCount());
assertEquals(17L, loaded.resolve(-65, 63, 30L));
assertEquals(1, loaded.cachedRegionCount());
assertEquals(30L, loaded.resolve(-64, 63, 30L));
}
@Test
public void regionCacheRemainsBoundedAndReloadsEvictedOwnership() throws Exception {
Path directory = temporaryFolder.newFolder("bounded-cache").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
for (int regionX = 0; regionX < 80; regionX++) {
ownership.assign(regionX << 5, 0, regionX + 1L);
assertTrue(ownership.cachedRegionCount() <= 64);
}
ownership.persist();
ChunkGenerationOwnership loaded = ChunkGenerationOwnership.load(directory);
assertEquals(0, loaded.cachedRegionCount());
assertEquals(80, loaded.regionCount());
for (int regionX = 79; regionX >= 0; regionX--) {
assertEquals(regionX + 1L, loaded.explicitActivation(regionX << 5, 0));
assertTrue(loaded.cachedRegionCount() <= 64);
}
assertEquals(1L, loaded.explicitActivation(0, 0));
}
@Test
public void activationIdsMustBePositive() throws Exception {
Path directory = temporaryFolder.newFolder("activation-range").toPath();
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(directory);
assertThrows(IllegalArgumentException.class, () -> ownership.resolve(0, 0, 0L));
assertThrows(IllegalArgumentException.class, () -> ownership.assign(0, 0, -2L));
assertThrows(
IllegalArgumentException.class,
() -> ownership.assignAll(WorldChunkInventory.empty(), 0L)
);
}
@Test
public void noncanonicalShardNameFailsClosed() throws Exception {
Path sourceDirectory = temporaryFolder.newFolder("canonical-source").toPath();
ChunkGenerationOwnership source = ChunkGenerationOwnership.load(sourceDirectory);
source.assign(0, 0, 1L);
source.persist();
Path invalidDirectory = temporaryFolder.newFolder("canonical-invalid").toPath();
Files.copy(
sourceDirectory.resolve(RegionGenerationOwnership.fileName(0, 0)),
invalidDirectory.resolve("unexpected.irow")
);
assertThrows(IOException.class, () -> ChunkGenerationOwnership.load(invalidDirectory));
}
@Test
public void symbolicLinkStorageRootAndParentFailClosed() throws Exception {
Path probe = temporaryFolder.newFolder("ownership-symlink-probe").toPath();
Path probeTarget = temporaryFolder.newFolder("ownership-symlink-probe-target").toPath();
try {
Files.createSymbolicLink(probe.resolve("link"), probeTarget);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
Path rootContainer = temporaryFolder.newFolder("ownership-linked-root-container").toPath();
Path linkedRoot = rootContainer.resolve("ownership");
Files.createSymbolicLink(linkedRoot, temporaryFolder.newFolder("ownership-linked-root-target").toPath());
assertThrows(IOException.class, () -> ChunkGenerationOwnership.load(linkedRoot));
Path parentContainer = temporaryFolder.newFolder("ownership-linked-parent-container").toPath();
Path linkedParent = parentContainer.resolve("generation");
Files.createSymbolicLink(linkedParent, temporaryFolder.newFolder("ownership-linked-parent-target").toPath());
assertThrows(IOException.class, () -> ChunkGenerationOwnership.load(linkedParent.resolve("ownership")));
}
}
@@ -0,0 +1,131 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class ChunkGenerationSemanticsTest {
@Test
public void builderCreatesADeeplyImmutableCanonicalRecord() {
ChunkGenerationSemantics semantics = ChunkGenerationSemantics.builder(-4, 9, 12L)
.addSurfaceBiomes(List.of("iris:wetlands", "iris:forest"))
.addCaveBiome("iris:limestone_caves")
.addRegion("temperate")
.addRiverProfile("rivers/default")
.addObject("procedural/tree/towering-oak#3@2")
.addStructure("minecraft:village_plains", -55, 71, 151)
.build();
assertEquals(List.of("iris:forest", "iris:wetlands"), List.copyOf(semantics.surfaceBiomeKeys()));
assertEquals(Set.of("iris:limestone_caves"), semantics.caveBiomeKeys());
assertEquals(Set.of("temperate"), semantics.regionKeys());
assertEquals(Set.of("rivers/default"), semantics.riverProfileKeys());
assertEquals(Set.of("procedural/tree/towering-oak#3@2"), semantics.objectKeys());
assertEquals(
Set.of(new ChunkGenerationSemantics.StructureOccurrence(
"minecraft:village_plains",
new ChunkGenerationSemantics.BlockPosition(-55, 71, 151)
)),
semantics.structures()
);
assertThrows(UnsupportedOperationException.class, () -> semantics.surfaceBiomeKeys().add("iris:desert"));
assertThrows(UnsupportedOperationException.class, () -> semantics.structures().clear());
}
@Test
public void activationAndResourceKeysAreStrict() {
assertThrows(IllegalArgumentException.class, () -> ChunkGenerationSemantics.builder(0, 0, 0L));
assertThrows(IllegalArgumentException.class, () -> ChunkGenerationSemantics.builder(0, 0, -1L));
assertThrows(
IllegalArgumentException.class,
() -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("")
);
assertThrows(
IllegalArgumentException.class,
() -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome(" Iris:Forest")
);
assertThrows(
IllegalArgumentException.class,
() -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("iris:Forest")
);
assertThrows(
IllegalArgumentException.class,
() -> ChunkGenerationSemantics.builder(0, 0, 1L).addSurfaceBiome("iris\\forest")
);
}
@Test
public void equivalentBuilderOrderProducesEqualRecords() {
ChunkGenerationSemantics first = ChunkGenerationSemantics.builder(2, 3, 4L)
.addSurfaceBiome("iris:b")
.addSurfaceBiome("iris:a")
.addStructure("iris:tower", 34, 90, 50)
.addStructure("iris:ruin", 35, 64, 51)
.build();
ChunkGenerationSemantics second = ChunkGenerationSemantics.builder(2, 3, 4L)
.addStructure("iris:ruin", 35, 64, 51)
.addSurfaceBiome("iris:a")
.addStructure("iris:tower", 34, 90, 50)
.addSurfaceBiome("iris:b")
.build();
assertEquals(first, second);
assertEquals(first.hashCode(), second.hashCode());
}
@Test
public void mergeUnionsStageFactsAndPropagatesSeal() {
ChunkGenerationSemantics first = ChunkGenerationSemantics.builder(2, 3, 4L)
.addSurfaceBiome("iris:forest")
.addRegion("temperate")
.build();
ChunkGenerationSemantics second = ChunkGenerationSemantics.builder(2, 3, 4L)
.addCaveBiome("iris:limestone_caves")
.addRegion("temperate")
.addStructure("iris:tower", 40, 90, 55)
.seal()
.build();
ChunkGenerationSemantics merged = first.merge(second);
assertEquals(Set.of("iris:forest"), merged.surfaceBiomeKeys());
assertEquals(Set.of("iris:limestone_caves"), merged.caveBiomeKeys());
assertEquals(Set.of("temperate"), merged.regionKeys());
assertEquals(second.structures(), merged.structures());
assertTrue(merged.sealed());
assertFalse(first.sealed());
}
@Test
public void sealedRecordAcceptsRetriesButRejectsNewFacts() {
ChunkGenerationSemantics sealed = ChunkGenerationSemantics.builder(2, 3, 4L)
.addSurfaceBiome("iris:forest")
.addRegion("temperate")
.seal()
.build();
ChunkGenerationSemantics retry = ChunkGenerationSemantics.builder(2, 3, 4L)
.addSurfaceBiome("iris:forest")
.build();
ChunkGenerationSemantics newFact = ChunkGenerationSemantics.builder(2, 3, 4L)
.addSurfaceBiome("iris:desert")
.build();
assertSame(sealed, sealed.merge(retry));
assertThrows(IllegalStateException.class, () -> sealed.merge(newFact));
assertThrows(
IllegalStateException.class,
() -> sealed.merge(ChunkGenerationSemantics.builder(2, 3, 5L).build())
);
assertThrows(
IllegalArgumentException.class,
() -> sealed.merge(ChunkGenerationSemantics.builder(3, 3, 4L).build())
);
}
}
@@ -0,0 +1,100 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationAdmissionTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void cutoverWaitsForEveryOpenGenerationStageAcrossInstances() throws Exception {
GenerationAdmission first = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission second = new GenerationAdmission(temporaryFolder.getRoot().toPath().resolve(".").normalize());
GenerationAdmission.StageLease stage = first.enterStage();
CountDownLatch requested = new CountDownLatch(1);
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
requested.countDown();
try (GenerationAdmission.CutoverLease ignored = second.beginCutover()) {
}
});
assertTrue(requested.await(5L, TimeUnit.SECONDS));
Thread.sleep(100L);
assertFalse(cutover.isDone());
stage.close();
cutover.get(5L, TimeUnit.SECONDS);
}
@Test
public void aWaitingCutoverPreventsLaterStagesFromOvertakingIt() throws Exception {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.StageLease firstStage = admission.enterStage();
CountDownLatch cutoverEntered = new CountDownLatch(1);
CountDownLatch releaseCutover = new CountDownLatch(1);
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationAdmission.CutoverLease ignored = admission.beginCutover()) {
cutoverEntered.countDown();
await(releaseCutover);
}
});
CompletableFuture<Void> laterStage = CompletableFuture.runAsync(() -> {
try (GenerationAdmission.StageLease ignored = admission.enterStage()) {
}
});
firstStage.close();
assertTrue(cutoverEntered.await(5L, TimeUnit.SECONDS));
assertFalse(laterStage.isDone());
releaseCutover.countDown();
cutover.get(5L, TimeUnit.SECONDS);
laterStage.get(5L, TimeUnit.SECONDS);
}
@Test
public void aStageMayCompleteOnAThreadOtherThanItsCaller() throws Exception {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.StageLease stage = admission.enterStage();
CompletableFuture<Void> close = CompletableFuture.runAsync(stage::close);
close.get(5L, TimeUnit.SECONDS);
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationAdmission.CutoverLease ignored = admission.beginCutover()) {
}
});
cutover.get(5L, TimeUnit.SECONDS);
}
@Test
public void startupCutoverIsPermanentlyClosedByFirstStageAdmission() {
GenerationAdmission admission = new GenerationAdmission(
temporaryFolder.getRoot().toPath().resolve("startup-only")
);
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
}
try (GenerationAdmission.StageLease ignored = admission.enterStage()) {
}
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
}
private static void await(CountDownLatch latch) {
try {
if (!latch.await(5L, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting for test latch.");
}
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for test latch.", exception);
}
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationBlendTest {
@Test
public void smootherstepUsesExactEndpointsAndMidpoint() {
assertEquals(0D, GenerationBlend.newEpochWeight(0D, 128), 0D);
assertEquals(0.5D, GenerationBlend.newEpochWeight(64D, 128), 0D);
assertEquals(1D, GenerationBlend.newEpochWeight(128D, 128), 0D);
assertEquals(1D, GenerationBlend.newEpochWeight(256D, 128), 0D);
}
@Test
public void smootherstepIsMonotonicAcrossTransition() {
double previous = 0D;
for (int distance = 1; distance <= 128; distance++) {
double weight = GenerationBlend.newEpochWeight(distance, 128);
assertTrue(weight > previous);
previous = weight;
}
}
@Test
public void interpolationPreservesExactBoundaryValues() {
assertEquals(72D, GenerationBlend.interpolate(72D, 160D, 0D), 0D);
assertEquals(160D, GenerationBlend.interpolate(72D, 160D, 1D), 0D);
assertEquals(116D, GenerationBlend.interpolate(72D, 160D, 0.5D), 0D);
assertEquals(72, GenerationBlend.interpolateHeight(72, 160, 0D));
assertEquals(160, GenerationBlend.interpolateHeight(72, 160, 1D));
assertEquals(116, GenerationBlend.interpolateHeight(72, 160, 0.5D));
}
@Test
public void rejectsInvalidWeightsDistancesAndWidths() {
assertThrows(IllegalArgumentException.class, () -> GenerationBlend.newEpochWeight(-1D, 32));
assertThrows(IllegalArgumentException.class, () -> GenerationBlend.newEpochWeight(Double.NaN, 32));
assertThrows(IllegalArgumentException.class, () -> GenerationBlend.newEpochWeight(1D, 0));
assertThrows(IllegalArgumentException.class, () -> GenerationBlend.interpolate(0D, 1D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> GenerationBlend.interpolateHeight(0, 1, 1.1D));
}
}
@@ -0,0 +1,277 @@
package art.arcane.iris.engine.history;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.CRC32;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationBoundaryStoreTest {
private static final Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}");
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void publishesDeterministicNegativeCoordinateSnapshot() throws Exception {
Path firstRoot = temporaryFolder.newFolder("first-dimension").toPath();
Path secondRoot = temporaryFolder.newFolder("second-dimension").toPath();
GenerationBoundaryStore firstStore = new GenerationBoundaryStore(firstRoot);
GenerationBoundaryStore secondStore = new GenerationBoundaryStore(secondRoot);
List<GenerationBoundary.ChunkCoordinate> chunks = List.of(
new GenerationBoundary.ChunkCoordinate(2, -3),
new GenerationBoundary.ChunkCoordinate(Integer.MIN_VALUE, Integer.MAX_VALUE),
new GenerationBoundary.ChunkCoordinate(-1, -1),
new GenerationBoundary.ChunkCoordinate(2, -3)
);
GenerationBoundary published = firstStore.publish(42L, chunks);
GenerationBoundary reordered = secondStore.publish(42L, List.of(
chunks.get(3),
chunks.get(2),
chunks.get(1),
chunks.get(0)
));
GenerationBoundary loaded = firstStore.load(42L);
assertEquals(firstRoot.toAbsolutePath().resolve("iris/generation/boundaries"), firstStore.directory());
assertTrue(SHA_256.matcher(published.identity()).matches());
assertEquals(published.identity(), reordered.identity());
assertEquals(published.identity(), loaded.identity());
assertEquals(3, loaded.historicalChunkCount());
assertTrue(loaded.isHistoricalChunk(2, -3));
assertTrue(loaded.isHistoricalChunk(-1, -1));
assertTrue(loaded.isHistoricalChunk(Integer.MIN_VALUE, Integer.MAX_VALUE));
assertArrayEquals(
Files.readAllBytes(firstStore.snapshotPath(42L)),
Files.readAllBytes(secondStore.snapshotPath(42L))
);
}
@Test
public void keepsPublishedActivationImmutableAndAllowsIdempotentPublication() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("immutable-dimension").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
long[] original = {
GenerationBoundary.packChunk(-2, 7),
GenerationBoundary.packChunk(0, 0)
};
GenerationBoundary first = store.publishPacked(7L, original);
GenerationBoundary repeated = store.publishPacked(7L, original);
assertEquals(first.identity(), repeated.identity());
assertThrows(
FileAlreadyExistsException.class,
() -> store.publishPacked(7L, new long[]{GenerationBoundary.packChunk(9, 9)})
);
assertArrayEquals(first.packedHistoricalChunks(), store.load(7L).packedHistoricalChunks());
}
@Test
public void failsClosedForCorruptionAndTruncation() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("corrupt-dimension").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
store.publishPacked(3L, new long[]{GenerationBoundary.packChunk(-4, 8)});
Path corrupted = store.snapshotPath(3L);
byte[] corruptedBytes = Files.readAllBytes(corrupted);
corruptedBytes[8] ^= 0x01;
Files.write(corrupted, corruptedBytes);
IOException checksumFailure = assertThrows(IOException.class, () -> store.load(3L));
assertTrue(checksumFailure.getMessage().contains("checksum"));
store.publishPacked(4L, new long[]{GenerationBoundary.packChunk(5, -6)});
Path truncated = store.snapshotPath(4L);
byte[] complete = Files.readAllBytes(truncated);
Files.write(truncated, Arrays.copyOf(complete, complete.length - 3));
assertThrows(IOException.class, () -> store.load(4L));
}
@Test
public void verifiesContentIdentityAfterChecksumValidation() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("identity-dimension").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
store.publishPacked(11L, new long[]{GenerationBoundary.packChunk(-8, -13)});
Path snapshot = store.snapshotPath(11L);
byte[] encoded = Files.readAllBytes(snapshot);
encoded[20] ^= 0x01;
updateChecksum(encoded);
Files.write(snapshot, encoded);
IOException failure = assertThrows(IOException.class, () -> store.load(11L));
assertTrue(failure.getMessage().contains("identity mismatch"));
}
@Test
public void rejectsInvalidActivationAndDeclaredBounds() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("bounds-dimension").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
assertThrows(IllegalArgumentException.class, () -> store.publishPacked(0L, new long[0]));
store.publishPacked(5L, new long[0]);
Path snapshot = store.snapshotPath(5L);
byte[] encoded = Files.readAllBytes(snapshot);
ByteBuffer.wrap(encoded).putInt(16, Integer.MAX_VALUE);
updateChecksum(encoded);
Files.write(snapshot, encoded);
assertThrows(IOException.class, () -> store.load(5L));
}
@Test
public void rejectsSymbolicLinksAtEveryStorageComponent() throws Exception {
Path probe = temporaryFolder.newFolder("boundary-symlink-probe").toPath();
Path probeTarget = temporaryFolder.newFolder("boundary-symlink-probe-target").toPath();
try {
Files.createSymbolicLink(probe.resolve("link"), probeTarget);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
for (String component : new String[]{"dimension", "iris", "generation", "boundaries"}) {
assertStorageLinkRejected(component);
}
}
@Test
public void concurrentStoreInstancesCannotClobberABoundary() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("boundary-race").toPath();
GenerationBoundaryStore firstStore = new GenerationBoundaryStore(dimensionRoot);
GenerationBoundaryStore secondStore = new GenerationBoundaryStore(dimensionRoot);
long firstChunk = GenerationBoundary.packChunk(1, 2);
long secondChunk = GenerationBoundary.packChunk(3, 4);
CyclicBarrier barrier = new CyclicBarrier(2);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> first = executor.submit(
() -> publishAfterBarrier(firstStore, 21L, firstChunk, barrier)
);
Future<Boolean> second = executor.submit(
() -> publishAfterBarrier(secondStore, 21L, secondChunk, barrier)
);
assertTrue(first.get() ^ second.get());
GenerationBoundary loaded = firstStore.load(21L);
assertTrue(loaded.isHistoricalChunk(1, 2) ^ loaded.isHistoricalChunk(3, 4));
} finally {
executor.shutdownNow();
}
}
@Test
public void contentAddressedRegionMasksAreReusedAcrossActivationHistory() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("boundary-history-reuse").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
store.publishPacked(1L, new long[]{
GenerationBoundary.packChunk(0, 0),
GenerationBoundary.packChunk(32, 0)
});
store.publishPacked(2L, new long[]{
GenerationBoundary.packChunk(0, 0),
GenerationBoundary.packChunk(1, 0),
GenerationBoundary.packChunk(32, 0)
});
store.publishPacked(3L, new long[]{
GenerationBoundary.packChunk(0, 0),
GenerationBoundary.packChunk(1, 0),
GenerationBoundary.packChunk(32, 0),
GenerationBoundary.packChunk(33, 0)
});
long maskCount;
try (Stream<Path> files = Files.list(store.directory())) {
maskCount = files.filter(path -> path.getFileName().toString().endsWith(".irbm")).count();
}
assertEquals(2L, maskCount);
GenerationBoundary reopened = new GenerationBoundaryStore(dimensionRoot).load(3L);
assertEquals(4, reopened.historicalChunkCount());
assertTrue(reopened.isHistoricalChunk(33, 0));
}
@Test
public void loadedBoundaryKeepsOnlyAFixedNumberOfRegionMasksResident() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("boundary-cache").toPath();
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
long[] chunks = new long[80];
for (int regionX = 0; regionX < chunks.length; regionX++) {
chunks[regionX] = GenerationBoundary.packChunk(regionX << 5, 0);
}
store.publishPacked(1L, chunks);
GenerationBoundary boundary = new GenerationBoundaryStore(dimensionRoot).load(1L);
for (int regionX = 0; regionX < chunks.length; regionX++) {
assertTrue(boundary.isHistoricalChunk(regionX << 5, 0));
assertTrue(boundary.cachedRegionCount() <= 64);
}
assertTrue(boundary.isHistoricalChunk(0, 0));
assertTrue(boundary.cachedRegionCount() <= 64);
}
private void assertStorageLinkRejected(String component) throws Exception {
Path container = temporaryFolder.newFolder("boundary-linked-" + component).toPath();
Path dimensionRoot = container.resolve("dimension");
Path external = temporaryFolder.newFolder("boundary-linked-target-" + component).toPath();
if (component.equals("dimension")) {
Files.createSymbolicLink(dimensionRoot, external);
} else {
Files.createDirectory(dimensionRoot);
Path iris = dimensionRoot.resolve("iris");
if (component.equals("iris")) {
Files.createSymbolicLink(iris, external);
} else {
Files.createDirectory(iris);
Path generation = iris.resolve("generation");
if (component.equals("generation")) {
Files.createSymbolicLink(generation, external);
} else {
Files.createDirectory(generation);
Files.createSymbolicLink(generation.resolve("boundaries"), external);
}
}
}
GenerationBoundaryStore store = new GenerationBoundaryStore(dimensionRoot);
assertThrows(IOException.class, () -> store.publishPacked(1L, new long[0]));
}
private static boolean publishAfterBarrier(
GenerationBoundaryStore store,
long activationId,
long chunk,
CyclicBarrier barrier
) throws Exception {
barrier.await();
try {
store.publishPacked(activationId, new long[]{chunk});
return true;
} catch (IOException expected) {
return false;
}
}
private static void updateChecksum(byte[] encoded) {
int bodyLength = encoded.length - Integer.BYTES;
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, bodyLength);
ByteBuffer.wrap(encoded, bodyLength, Integer.BYTES).putInt((int) checksum.getValue());
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationBoundaryTest {
@Test
public void freezesHistoricalOwnershipAndDeduplicatesCoordinates() {
ArrayList<GenerationBoundary.ChunkCoordinate> source = new ArrayList<>(List.of(
new GenerationBoundary.ChunkCoordinate(2, -3),
new GenerationBoundary.ChunkCoordinate(2, -3)
));
GenerationBoundary boundary = GenerationBoundary.freeze("activation-4-frontier", source);
source.clear();
source.add(new GenerationBoundary.ChunkCoordinate(9, 9));
assertEquals(1, boundary.historicalChunkCount());
assertTrue(boundary.isHistoricalChunk(2, -3));
assertFalse(boundary.isHistoricalChunk(9, 9));
long[] exported = boundary.packedHistoricalChunks();
exported[0] = GenerationBoundary.packChunk(9, 9);
assertTrue(boundary.isHistoricalChunk(2, -3));
}
@Test
public void mapsNegativeBlocksWithFloorDivision() {
GenerationBoundary boundary = GenerationBoundary.freeze("negative-frontier", List.of(
new GenerationBoundary.ChunkCoordinate(-1, -1)
));
assertTrue(boundary.isHistoricalBlock(-1, -1));
assertTrue(boundary.isHistoricalBlock(-16, -16));
assertFalse(boundary.isHistoricalBlock(-17, -16));
assertEquals(1D, boundary.distanceToHistoricalChunks(-17, -16, 64), 0D);
assertEquals(1D, boundary.distanceToHistoricalChunks(0, -1, 64), 0D);
}
@Test
public void measuresIrregularAndEnclosedFrontiersInBlockSpace() {
GenerationBoundary boundary = GenerationBoundary.freeze("enclosed-frontier", List.of(
new GenerationBoundary.ChunkCoordinate(-1, -1),
new GenerationBoundary.ChunkCoordinate(0, -1),
new GenerationBoundary.ChunkCoordinate(1, -1),
new GenerationBoundary.ChunkCoordinate(-1, 0),
new GenerationBoundary.ChunkCoordinate(1, 0),
new GenerationBoundary.ChunkCoordinate(-1, 1),
new GenerationBoundary.ChunkCoordinate(0, 1),
new GenerationBoundary.ChunkCoordinate(1, 1)
));
assertFalse(boundary.isHistoricalChunk(0, 0));
assertEquals(8D, boundary.distanceToHistoricalChunks(8, 8, 64), 0D);
assertEquals(4D, boundary.distanceToHistoricalChunks(100, 8, 4), 0D);
GenerationBoundary corner = GenerationBoundary.freeze("corner", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
assertEquals(Math.sqrt(2D), corner.distanceToHistoricalChunks(16, 16, 64), 0D);
}
@Test
public void packedCoordinatesRoundTripAcrossIntegerRange() {
long[] packed = {
GenerationBoundary.packChunk(Integer.MIN_VALUE, Integer.MAX_VALUE),
GenerationBoundary.packChunk(-1, 0),
GenerationBoundary.packChunk(0, -1),
GenerationBoundary.packChunk(Integer.MAX_VALUE, Integer.MIN_VALUE)
};
GenerationBoundary boundary = GenerationBoundary.freezePacked("packed-frontier", packed);
assertArrayEquals(packed, boundary.packedHistoricalChunks());
assertEquals(new GenerationBoundary.ChunkCoordinate(Integer.MIN_VALUE, Integer.MAX_VALUE),
GenerationBoundary.unpackChunk(packed[0]));
assertEquals(new GenerationBoundary.ChunkCoordinate(Integer.MAX_VALUE, Integer.MIN_VALUE),
GenerationBoundary.unpackChunk(packed[3]));
}
@Test
public void samplesEveryExposedEdgeBlockWithoutDuplicatingCorners() {
GenerationBoundary isolated = GenerationBoundary.freeze("isolated", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0),
new GenerationBoundary.ChunkCoordinate(-1, -1)
));
assertEquals(120, isolated.exposedBlockColumns().size());
assertTrue(isolated.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(-16, -16)));
assertTrue(isolated.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(-1, -1)));
assertTrue(isolated.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(0, 0)));
assertTrue(isolated.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(15, 15)));
assertFalse(isolated.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(7, 7)));
GenerationBoundary adjacent = GenerationBoundary.freeze("adjacent", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0),
new GenerationBoundary.ChunkCoordinate(1, 0)
));
assertEquals(92, adjacent.exposedBlockColumns().size());
assertFalse(adjacent.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(15, 8)));
assertFalse(adjacent.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(16, 8)));
assertTrue(adjacent.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(0, 8)));
assertTrue(adjacent.exposedBlockColumns().contains(new GenerationBoundary.BlockColumn(31, 8)));
}
@Test
public void fragmentedExplorationStreamsBeyondTheFormerMonolithicSnapshotLimit() {
int chunkCount = 70_000;
ArrayList<GenerationBoundary.ChunkCoordinate> chunks = new ArrayList<>(chunkCount);
for (int index = 0; index < chunkCount; index++) {
chunks.add(new GenerationBoundary.ChunkCoordinate(index * 2, 0));
}
GenerationBoundary boundary = GenerationBoundary.freeze("fragmented", chunks);
assertEquals(4_200_000L, boundary.exposedBlockColumnCount());
}
@Test
public void rejectsInvalidBoundaryInputs() {
assertThrows(NullPointerException.class, () -> GenerationBoundary.freeze("boundary", null));
ArrayList<GenerationBoundary.ChunkCoordinate> withNull = new ArrayList<>();
withNull.add(new GenerationBoundary.ChunkCoordinate(0, 0));
withNull.add(null);
assertThrows(NullPointerException.class, () -> GenerationBoundary.freeze("boundary", withNull));
assertThrows(IllegalArgumentException.class, () -> GenerationBoundary.freeze(" ", List.of()));
GenerationBoundary boundary = GenerationBoundary.freeze("boundary", List.of());
assertThrows(IllegalArgumentException.class,
() -> boundary.distanceToHistoricalChunks(0, 0, 0));
}
}
@@ -0,0 +1,288 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
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 art.arcane.iris.engine.object.IrisRange;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GenerationEpochContractFactoryTest {
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void derivesTheAuthoredDimensionTypeSemantics() {
IrisDimension dimension = dimension(IrisEnvironment.NORMAL);
IrisDimensionType dimensionType = dimension.getDimensionType();
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
dimension,
"worlds/overworld",
"iris:overworld"
);
assertEquals("worlds/overworld", contract.dimensionKey());
assertEquals("iris:overworld", contract.dimensionTypeKey());
assertEquals("NORMAL", contract.environment());
assertEquals("OVERWORLD", contract.generationMode());
assertEquals(127, contract.internalFluidHeight());
assertEquals(-64, contract.minHeight());
assertEquals(384, contract.height());
assertEquals(256, contract.logicalHeight());
assertEquals(1D, contract.coordinateScale(), 0D);
assertEquals(false, contract.upperTerrainEnabled());
assertEquals("none", contract.upperDimensionKey());
assertEquals(0, contract.upperDimensionGap());
assertEquals("0".repeat(64), contract.upperTerrainPackFingerprint());
assertEquals(
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
contract.dimensionTypeFingerprintSchema()
);
assertEquals(
GenerationEpochContractFactory.fingerprintDimensionType(dimensionType),
contract.dimensionTypeFingerprint()
);
}
@Test
public void capturesGenerationModeAndInternalFluidHeight() {
IrisDimension dimension = dimension(IrisEnvironment.NORMAL)
.setMode(new IrisDimensionMode().setType(IrisDimensionModeType.ISLANDS))
.setFluidHeight(100);
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
);
assertEquals("ISLANDS", contract.generationMode());
assertEquals(164, contract.internalFluidHeight());
}
@Test
public void upperTerrainMustBeLocalAndPackIdentityDoesNotChangeItsLayout() throws Exception {
File pack = temporary.newFolder("upper-pack");
IrisData data = mock(IrisData.class);
@SuppressWarnings("unchecked")
ResourceLoader<IrisDimension> loader = mock(ResourceLoader.class);
IrisDimension dimension = dimension(IrisEnvironment.NORMAL).setUpperDimension("ceiling");
IrisDimension ceiling = dimension(IrisEnvironment.NORMAL);
dimension.setLoader(data);
ceiling.setLoader(data);
ceiling.setLoadKey("ceiling");
when(data.getDataFolder()).thenReturn(pack);
when(data.getDimensionLoader()).thenReturn(loader);
when(loader.load("ceiling", false)).thenReturn(ceiling);
GenerationEpoch.DimensionContract first = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
);
Files.writeString(
pack.toPath().resolve("upper-content.json"),
"{}",
StandardCharsets.UTF_8
);
GenerationEpoch.DimensionContract changedPack = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
);
assertEquals(true, first.upperTerrainEnabled());
assertEquals("ceiling", first.upperDimensionKey());
assertEquals(32, first.upperDimensionGap());
assertNotEquals(first.upperTerrainPackFingerprint(), changedPack.upperTerrainPackFingerprint());
assertNotEquals(first, changedPack);
assertEquals(true, first.hasSameLayout(changedPack));
dimension.setUpperDimensionGap(33);
assertEquals(false, first.hasSameLayout(GenerationEpochContractFactory.create(
dimension, "overworld", "iris:overworld")));
when(loader.load("ceiling", false)).thenReturn(null);
assertThrows(
IllegalStateException.class,
() -> GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
)
);
}
@Test
public void resolvesBaseEnvironmentCoordinateScale() {
IrisDimension dimension = dimension(IrisEnvironment.NETHER);
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
dimension,
"underworld",
"iris:underworld"
);
assertEquals("NETHER", contract.environment());
assertEquals(8D, contract.coordinateScale(), 0D);
}
@Test
public void explicitCoordinateScaleOverridesTheBaseEnvironment() {
IrisDimension dimension = dimension(IrisEnvironment.NETHER);
dimension.setDimensionOptions(new IrisDimensionTypeOptions().coordinateScale(3.5D));
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
dimension,
"underworld",
"iris:underworld"
);
assertEquals(3.5D, contract.coordinateScale(), 0D);
}
@Test
public void serializerChangesDoNotAlterTheDimensionContract() {
IrisDimension dimension = dimension(IrisEnvironment.NORMAL);
IrisDimensionType type = dimension.getDimensionType();
String firstJson = type.toJson(new TaggedFixer("first"));
String secondJson = type.toJson(new TaggedFixer("second"));
GenerationEpoch.DimensionContract first = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
);
GenerationEpoch.DimensionContract second = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld"
);
assertNotEquals(firstJson, secondJson);
assertEquals(first, second);
}
@Test
public void authoredDimensionTypeChangesAlterTheFingerprint() {
IrisDimension normal = dimension(IrisEnvironment.NORMAL);
IrisDimension fullbright = dimension(IrisEnvironment.NORMAL);
fullbright.setFullbright(true);
IrisDimension nether = dimension(IrisEnvironment.NETHER);
assertNotEquals(
GenerationEpochContractFactory.create(
normal,
"overworld",
"iris:overworld"
).dimensionTypeFingerprint(),
GenerationEpochContractFactory.create(
fullbright,
"overworld",
"iris:overworld"
).dimensionTypeFingerprint()
);
assertNotEquals(
GenerationEpochContractFactory.fingerprintDimensionType(normal.getDimensionType()),
GenerationEpochContractFactory.fingerprintDimensionType(nether.getDimensionType())
);
}
@Test
public void recordsAndValidatesTheFingerprintSchema() {
IrisDimension dimension = dimension(IrisEnvironment.NORMAL);
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld",
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE
);
assertEquals(
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
contract.dimensionTypeFingerprintSchema()
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationEpochContractFactory.create(
dimension,
"overworld",
"iris:overworld",
2
)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationEpochContractFactory.fingerprintDimensionType(
dimension.getDimensionType(),
2
)
);
}
@Test
public void refusesInvalidKeys() {
IrisDimension dimension = dimension(IrisEnvironment.NORMAL);
assertThrows(
IllegalArgumentException.class,
() -> GenerationEpochContractFactory.create(
dimension,
"Overworld",
"iris:overworld"
)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationEpochContractFactory.create(
dimension,
"overworld",
"overworld"
)
);
}
private static IrisDimension dimension(IrisEnvironment environment) {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
dimension.setEnvironment(environment);
dimension.setDimensionHeight(new IrisRange(-64, 320));
dimension.setLogicalHeight(256);
return dimension;
}
private record TaggedFixer(String tag) implements IDataFixer {
@Override
public JSONObject resolve(Dimension dimension, IrisDimensionTypeOptions options) {
double coordinateScale = options.coordinateScale() == -1D
? dimension == Dimension.NETHER ? 8D : 1D
: options.coordinateScale();
return new JSONObject()
.put("coordinate_scale", coordinateScale)
.put("serializer", tag);
}
@Override
public void fixDimension(Dimension dimension, JSONObject json) {
}
}
}
@@ -0,0 +1,320 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonObject;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
public class GenerationEpochTest {
private static final String PACK_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String PACK_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@Test
public void derivesStableIdentityFromEveryGenerationInput() {
GenerationEpoch.DimensionContract contract = contract("iris:type_a", 1D);
GenerationRegistryContract registry = registry(PACK_A);
GenerationEpoch first = epoch(PACK_A, 1, 42L, 1, 1, 1, contract, registry);
GenerationEpoch same = epoch(PACK_A, 1, 42L, 1, 1, 1, contract, registry);
assertEquals(first, same);
assertEquals(first.epochId(), same.epochId());
assertNotEquals(first.epochId(), epoch(PACK_B, 1, 42L, 1, 1, 1, contract, registry).epochId());
assertNotEquals(first.epochId(), epoch(PACK_A, 2, 42L, 1, 1, 1, contract, registry).epochId());
assertNotEquals(first.epochId(), epoch(PACK_A, 1, 43L, 1, 1, 1, contract, registry).epochId());
assertNotEquals(first.epochId(), epoch(PACK_A, 1, 42L, 2, 1, 1, contract, registry).epochId());
assertNotEquals(first.epochId(), epoch(PACK_A, 1, 42L, 1, 2, 1, contract, registry).epochId());
assertNotEquals(first.epochId(), epoch(PACK_A, 1, 42L, 1, 1, 2, contract, registry).epochId());
GenerationEpoch changedKernelImplementation = GenerationEpoch.create(new GenerationEpoch.Spec(
PACK_A,
1,
42L,
1,
1,
1,
"d".repeat(64),
contract,
registry
));
assertNotEquals(first.epochId(), changedKernelImplementation.epochId());
assertNotEquals(
first.epochId(),
epoch(PACK_A, 1, 42L, 1, 1, 1, contract("iris:type_b", 1D), registry).epochId()
);
assertNotEquals(
first.epochId(),
epoch(PACK_A, 1, 42L, 1, 1, 1, contract("iris:type_a", 8D), registry).epochId()
);
assertNotEquals(
first.epochId(),
epoch(PACK_A, 1, 42L, 1, 1, 1, contract, registry(PACK_B)).epochId()
);
assertNotEquals(first.epochId(), epoch(
PACK_A,
1,
42L,
1,
1,
1,
new GenerationEpoch.DimensionContract(
"overworld",
"iris:type_a",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
PACK_B
),
registry
).epochId());
}
@Test
public void preservesExactImmutableInputs() {
GenerationRegistryContract registry = registry(PACK_A);
GenerationEpoch epoch = epoch(PACK_A, 3, Long.MIN_VALUE, 7, 5, 9, contract("iris:type_a", 1D), registry);
assertEquals(PACK_A, epoch.packFingerprint());
assertEquals(3, epoch.packFingerprintVersion());
assertEquals(Long.MIN_VALUE, epoch.worldSeed());
assertEquals(7, epoch.seedDerivationVersion());
assertEquals(5, epoch.generatorAbi());
assertEquals(9, epoch.rngVersion());
assertEquals(GenerationKernelV1.IMPLEMENTATION_FINGERPRINT, epoch.kernelImplementationFingerprint());
assertEquals(registry, epoch.registryContract());
}
@Test
public void roundTripsStrictDeterministicJson() {
GenerationEpoch epoch = epoch(
PACK_A,
1,
Long.MAX_VALUE,
1,
3,
7,
contract("iris:type_a", 1D),
registry(PACK_A)
);
JsonObject json = epoch.toJson();
assertEquals(epoch, GenerationEpoch.fromJson(json.deepCopy()));
assertEquals(json.toString(), epoch.toJson().toString());
JsonObject unknownField = json.deepCopy();
unknownField.addProperty("unexpected", true);
assertThrows(IllegalArgumentException.class, () -> GenerationEpoch.fromJson(unknownField));
JsonObject missingSeed = json.deepCopy();
missingSeed.remove("worldSeed");
assertThrows(IllegalArgumentException.class, () -> GenerationEpoch.fromJson(missingSeed));
JsonObject missingKernelFingerprint = json.deepCopy();
missingKernelFingerprint.remove("kernelImplementationFingerprint");
assertThrows(IllegalArgumentException.class, () -> GenerationEpoch.fromJson(missingKernelFingerprint));
}
@Test
public void rejectsMalformedGenerationInputs() {
GenerationEpoch.DimensionContract contract = contract("iris:type_a", 1D);
GenerationRegistryContract registry = registry(PACK_A);
assertThrows(
IllegalArgumentException.class,
() -> epoch(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
1,
42L,
1,
1,
1,
contract,
registry
)
);
assertThrows(
IllegalArgumentException.class,
() -> epoch(PACK_A, 0, 42L, 1, 1, 1, contract, registry)
);
assertThrows(
IllegalArgumentException.class,
() -> epoch(PACK_A, 1, 42L, 0, 1, 1, contract, registry)
);
assertThrows(
IllegalArgumentException.class,
() -> epoch(PACK_A, 1, 42L, 1, 0, 1, contract, registry)
);
assertThrows(
IllegalArgumentException.class,
() -> epoch(PACK_A, 1, 42L, 1, 1, 0, contract, registry)
);
GenerationEpoch valid = epoch(PACK_A, 1, 42L, 1, 1, 1, contract, registry);
JsonObject malformedKernelFingerprint = valid.toJson();
malformedKernelFingerprint.addProperty("kernelImplementationFingerprint", "not-a-sha256");
assertThrows(IllegalArgumentException.class, () -> GenerationEpoch.fromJson(malformedKernelFingerprint));
assertThrows(
NullPointerException.class,
() -> epoch(PACK_A, 1, 42L, 1, 1, 1, contract, null)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationEpoch.DimensionContract(
"overworld",
"iris:type_a",
"NORMAL",
"OVERWORLD",
127,
-64,
0,
0,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
PACK_A
)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationEpoch.DimensionContract(
"overworld",
"iris:type_a",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
385,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
PACK_A
)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationEpoch.DimensionContract(
"overworld",
"iris:type_a",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
Double.NaN,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
PACK_A
)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationEpoch.DimensionContract(
"overworld",
"iris:type_a",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
99,
PACK_A
)
);
}
@Test
public void rejectsForgedContentAddress() {
GenerationEpoch epoch = epoch(
PACK_A,
1,
42L,
1,
1,
1,
contract("iris:type_a", 1D),
registry(PACK_A)
);
JsonObject json = epoch.toJson();
json.addProperty("epochId", PACK_B);
assertThrows(IllegalArgumentException.class, () -> GenerationEpoch.fromJson(json));
}
private static GenerationEpoch epoch(
String packFingerprint,
int packFingerprintVersion,
long worldSeed,
int seedDerivationVersion,
int generatorAbi,
int rngVersion,
GenerationEpoch.DimensionContract contract,
GenerationRegistryContract registryContract
) {
return GenerationEpoch.create(new GenerationEpoch.Spec(
packFingerprint,
packFingerprintVersion,
worldSeed,
seedDerivationVersion,
generatorAbi,
rngVersion,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract,
registryContract
));
}
private static GenerationEpoch.DimensionContract contract(String typeKey, double coordinateScale) {
return new GenerationEpoch.DimensionContract(
"overworld",
typeKey,
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
coordinateScale,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.DIMENSION_TYPE_FINGERPRINT_SCHEMA_VERSION_ONE,
PACK_A
);
}
private static GenerationRegistryContract registry(String definitionSha256) {
GenerationRegistryContract.PhysicalResourceKey key = new GenerationRegistryContract.PhysicalResourceKey(
"minecraft:worldgen/biome",
"minecraft:plains"
);
return GenerationRegistryContract.fromDefinitions(Map.of(key, definitionSha256));
}
}
@@ -0,0 +1,283 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
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.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class GenerationFindCatalogTest {
@Test
public void parsesRemovedNamesFromRetainedPackWithoutBuildingRuntime() throws Exception {
Fixture fixture = new Fixture();
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertSame(fixture.meadow, fixture.biomeHandler().parse("meadow", false));
assertSame(fixture.oldRegion, fixture.regionHandler().parse("old-region", false));
assertEquals("old-tree", fixture.objectHandler().parse("old-tree", false));
assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "/OLD-TREE.iob"));
assertFalse(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "unplaced"));
assertEquals("old meadow", GenerationFindCatalog.biome(fixture.engine, "meadow").getName());
data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile()), times(1));
data.verifyNoMoreInteractions();
verify(fixture.oldData).close();
verify(fixture.history, never()).packRoot(2L);
verify(fixture.engine, never()).getActiveGenerationRuntimeBinding();
}
}
@Test
public void activeDefinitionsWinHistoricalKeyCollisions() throws Exception {
Fixture fixture = new Fixture();
IrisBiome activeMeadow = biome("meadow", "new meadow");
IrisRegion activeRegion = region("old-region");
when(fixture.engine.getAllBiomes()).thenReturn(new KList<>(activeMeadow));
when(fixture.activeDimension.getAllRegions(fixture.engine)).thenReturn(new KList<>(activeRegion));
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertSame(activeMeadow, fixture.biomeHandler().parse("MEADOW", false));
assertSame(activeRegion, fixture.regionHandler().parse("OLD-REGION", false));
assertEquals(1, GenerationFindCatalog.biomes(fixture.engine).size());
assertEquals(1, GenerationFindCatalog.regions(fixture.engine).size());
}
}
@Test
public void unrelatedAuthoringAndUnreachableFilesAreNotFallbackCandidates() throws Exception {
Fixture fixture = new Fixture();
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertThrows(DirectorParsingException.class, () -> fixture.biomeHandler().parse("unused", false));
assertThrows(DirectorParsingException.class, () -> fixture.regionHandler().parse("unused", false));
assertThrows(DirectorParsingException.class, () -> fixture.objectHandler().parse("unplaced", false));
verify(fixture.biomeLoader, never()).load("unused");
data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile()));
data.verifyNoMoreInteractions();
}
}
@Test
public void missingRetainedPackFailsInsteadOfSearchingAuthoringPacks() throws Exception {
Fixture fixture = new Fixture();
IOException missingPack = new IOException("missing immutable pack");
when(fixture.history.packRoot(1L)).thenThrow(missingPack);
UncheckedIOException failure = assertThrows(UncheckedIOException.class,
() -> GenerationFindCatalog.biomes(fixture.engine));
assertSame(missingPack, failure.getCause());
}
@Test
public void retainsEditableStructureKeysFromEveryOwningPlacementScope() throws Exception {
Fixture fixture = new Fixture();
fixture.oldDimension.setStructures(new KList<>(structure("old-house")));
fixture.oldRegion.setStructures(new KList<>(structure("old-ruin")));
fixture.meadow.setStructures(new KList<>(structure("old-tower")));
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "OLD-HOUSE"));
assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "old-ruin"));
assertTrue(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "old-tower"));
assertFalse(GenerationFindCatalog.hasRetainedStructurePlacement(fixture.engine, "unused"));
assertEquals(3, GenerationFindCatalog.retainedStructureKeys(fixture.engine).size());
}
}
@Test
public void parsesRemovedObjectsFromTheSeparateRetainedUpperDimension() throws Exception {
Fixture fixture = new Fixture();
fixture.oldDimension.setUpperDimension("ceiling");
IrisDimension upper = new IrisDimension().setRegions(new KList<>("sky-region"));
IrisRegion skyRegion = region("sky-region").setLandBiomes(new KList<>("sky-biome"));
IrisBiome skyBiome = biome("sky-biome", "ceiling biome");
skyRegion.setObjects(new KList<>(object("upper-region-object")));
skyBiome.setObjects(new KList<>(object("upper-biome-object")));
when(fixture.dimensionLoader.load("ceiling", false)).thenReturn(upper);
when(fixture.regionLoader.load("sky-region")).thenReturn(skyRegion);
when(fixture.biomeLoader.load("sky-biome")).thenReturn(skyBiome);
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertEquals("upper-biome-object", fixture.objectHandler().parse("upper-biome-object", false));
assertEquals("upper-region-object", fixture.objectHandler().parse("upper-region-object", false));
assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "upper-biome-object"));
assertTrue(GenerationFindCatalog.hasObjectPlacement(fixture.engine, "upper-region-object"));
assertNull(GenerationFindCatalog.biome(fixture.engine, "sky-biome"));
assertNull(GenerationFindCatalog.region(fixture.engine, "sky-region"));
data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile()));
data.verifyNoMoreInteractions();
verify(fixture.oldData).close();
}
}
@Test
public void missingRetainedUpperDimensionDoesNotFallBackToAnotherPack() throws Exception {
Fixture fixture = new Fixture();
fixture.oldDimension.setUpperDimension("missing-upper");
try (MockedStatic<IrisData> data = mockStatic(IrisData.class)) {
data.when(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile())).thenReturn(fixture.oldData);
assertThrows(IllegalStateException.class, () -> GenerationFindCatalog.objectKeys(fixture.engine));
data.verify(() -> IrisData.openDatapackCompiler(fixture.oldPack.toFile()));
data.verifyNoMoreInteractions();
verify(fixture.oldData).close();
}
}
private static IrisStructurePlacement structure(String key) {
return new IrisStructurePlacement().setStructures(new KList<>(key));
}
private static IrisObjectPlacement object(String key) {
return new IrisObjectPlacement().setPlace(new KList<>(key));
}
private static IrisBiome biome(String key, String name) {
IrisBiome biome = new IrisBiome().setName(name);
biome.setLoadKey(key);
return biome;
}
private static IrisRegion region(String key) {
IrisRegion region = new IrisRegion();
region.setLoadKey(key);
return region;
}
private static final class Fixture {
private final IrisEngine engine = mock(IrisEngine.class);
private final GenerationHistory history = mock(GenerationHistory.class);
private final IrisDimension activeDimension = mock(IrisDimension.class);
private final IrisData oldData = mock(IrisData.class);
private final ResourceLoader<IrisDimension> dimensionLoader;
private final ResourceLoader<IrisRegion> regionLoader;
private final ResourceLoader<IrisBiome> biomeLoader;
private final IrisDimension oldDimension = new IrisDimension().setRegions(new KList<>("old-region"));
private final Path oldPack = Path.of("retained-test-pack").toAbsolutePath();
private final IrisBiome meadow = biome("meadow", "old meadow");
private final IrisRegion oldRegion = region("old-region");
@SuppressWarnings("unchecked")
private Fixture() throws IOException {
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationActivation active = mock(GenerationActivation.class);
GenerationActivation old = mock(GenerationActivation.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(history.manifest()).thenReturn(manifest);
when(manifest.activeActivation()).thenReturn(active);
when(active.activationId()).thenReturn(2L);
when(active.parentActivationId()).thenReturn(1L);
when(manifest.activation(1L)).thenReturn(Optional.of(old));
when(old.epochId()).thenReturn("old");
when(old.parentActivationId()).thenReturn(null);
when(manifest.epoch("old")).thenReturn(Optional.of(epoch));
when(epoch.dimensionContract()).thenReturn(contract);
when(epoch.packFingerprint()).thenReturn("old-pack");
when(contract.dimensionKey()).thenReturn("world");
when(history.packRoot(1L)).thenReturn(oldPack);
IrisData activeData = mock(IrisData.class);
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(engine.getData()).thenReturn(activeData);
when(activeData.getObjectLoader()).thenReturn(objectLoader);
when(objectLoader.getPossibleKeys()).thenReturn(new String[]{"new-tree"});
when(engine.getDimension()).thenReturn(activeDimension);
when(engine.getAllBiomes()).thenReturn(new KList<>(biome("plateau", "new plateau")));
when(activeDimension.getAllRegions(engine)).thenReturn(new KList<>(region("new-region")));
dimensionLoader = mock(ResourceLoader.class);
regionLoader = mock(ResourceLoader.class);
biomeLoader = mock(ResourceLoader.class);
oldRegion.setLandBiomes(new KList<>("meadow"));
meadow.setObjects(new KList<>(new IrisObjectPlacement().setPlace(new KList<>("old-tree"))));
when(oldData.getDimensionLoader()).thenReturn(dimensionLoader);
when(oldData.getRegionLoader()).thenReturn(regionLoader);
when(oldData.getBiomeLoader()).thenReturn(biomeLoader);
when(dimensionLoader.load("world", false)).thenReturn(oldDimension);
when(regionLoader.load("old-region")).thenReturn(oldRegion);
when(biomeLoader.load("meadow")).thenReturn(meadow);
}
private ReachableBiomeHandler biomeHandler() {
return new ReachableBiomeHandler() {
@Override
public Engine engine() {
return engine;
}
@Override
public IrisData data() {
throw new AssertionError("Find must not use the authoring context");
}
};
}
private ReachableRegionHandler regionHandler() {
return new ReachableRegionHandler() {
@Override
public Engine engine() {
return engine;
}
@Override
public IrisData data() {
throw new AssertionError("Find must not use the authoring context");
}
};
}
private LocatableObjectHandler objectHandler() {
return new LocatableObjectHandler() {
@Override
public Engine engine() {
return engine;
}
@Override
public IrisData data() {
throw new AssertionError("Find must not use the authoring context");
}
};
}
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public final class GenerationHistoryPathsTest {
@Test
public void derivesTheCanonicalDimensionHistoryLayout() {
Path dimensionRoot = Path.of("build", "world", "..", "world");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot);
Path expectedRoot = dimensionRoot.toAbsolutePath().normalize();
String epochId = "a".repeat(64);
assertEquals(expectedRoot, paths.dimensionRoot());
assertEquals(expectedRoot.resolve("iris"), paths.irisRoot());
assertEquals(expectedRoot.resolve("iris/generation"), paths.generationRoot());
assertEquals(expectedRoot.resolve("iris/generation/manifest.json"), paths.manifest());
assertEquals(expectedRoot.resolve("iris/generation/epochs"), paths.epochsRoot());
assertEquals(expectedRoot.resolve("iris/generation/activations"), paths.activationsRoot());
assertEquals(expectedRoot.resolve("iris/generation/ownership"), paths.ownershipRoot());
assertEquals(expectedRoot.resolve("iris/generation/epochs").resolve(epochId), paths.epochRoot(epochId));
assertEquals(expectedRoot.resolve("iris/generation/epochs").resolve(epochId).resolve("pack"),
paths.packRoot(epochId));
assertEquals(expectedRoot.resolve("iris/generation/activations/2"), paths.activationRoot(2L));
assertEquals(expectedRoot.resolve("iris/generation/activations/2/mantle-hydrology"),
paths.activationMantleRoot(2L));
assertEquals(expectedRoot.resolve("iris/pack"), paths.legacyPackRoot());
assertEquals(expectedRoot.resolve("mantle-hydrology"), paths.legacyMantleRoot());
assertEquals(expectedRoot.resolve("region"), paths.regionRoot());
assertThrows(IllegalArgumentException.class, () -> paths.epochRoot("../escape"));
assertThrows(IllegalArgumentException.class, () -> paths.activationRoot(0L));
}
}
@@ -0,0 +1,341 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationHistoryStoreTest {
private static final String PACK_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String PACK_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
private static final Clock CLOCK = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneOffset.UTC);
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void persistsActiveAndPendingHistoryAcrossReopen() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation");
GenerationEpoch epochA = epoch(PACK_A);
GenerationEpoch epochB = epoch(PACK_B);
GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, epochA, CLOCK);
GenerationActivation pending = store.preparePendingActivation(epochB, 512);
GenerationHistoryStore reopenedPending = GenerationHistoryStore.open(directory, CLOCK);
assertEquals(epochA, reopenedPending.activeEpoch());
assertEquals(epochB, reopenedPending.pendingEpoch().orElseThrow());
assertEquals(2L, pending.activationId());
assertEquals(Long.valueOf(1L), pending.parentActivationId());
reopenedPending.completePendingTransition(pending.activationId(), digest('d'), digest('e'));
reopenedPending.activatePending(pending.activationId());
GenerationHistoryStore reopenedActive = GenerationHistoryStore.open(directory, CLOCK);
assertEquals(epochB, reopenedActive.activeEpoch());
assertEquals(2L, reopenedActive.activeActivation().activationId());
assertFalse(reopenedActive.pendingActivation().isPresent());
assertEquals(2, reopenedActive.manifest().epochs().size());
assertEquals(2, reopenedActive.manifest().activations().size());
}
@Test
public void repeatedPreparationDoesNotRewriteOrRenumberPendingActivation() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation");
GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK);
GenerationEpoch epochB = epoch(PACK_B);
GenerationActivation first = store.preparePendingActivation(epochB, 512);
byte[] firstManifest = Files.readAllBytes(directory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME));
GenerationActivation repeated = store.preparePendingActivation(epochB, 512);
byte[] repeatedManifest = Files.readAllBytes(directory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME));
assertEquals(first, repeated);
assertEquals(2L, repeated.activationId());
assertEquals(2, store.manifest().activations().size());
assertArrayEquals(firstManifest, repeatedManifest);
}
@Test
public void manifestMutationsLeaveOnlyTheCanonicalFile() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation");
GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK);
GenerationActivation pending = store.preparePendingActivation(epoch(PACK_B), 512);
store.completePendingTransition(pending.activationId(), digest('d'), digest('e'));
store.activatePending(pending.activationId());
try (Stream<Path> entries = Files.list(directory)) {
List<String> names = entries.map(path -> path.getFileName().toString()).toList();
assertEquals(List.of(GenerationHistoryStore.MANIFEST_FILE_NAME), names);
}
GenerationHistoryStore reopened = GenerationHistoryStore.open(directory, CLOCK);
assertEquals(store.manifest(), reopened.manifest());
}
@Test
public void failsClosedWhenManifestIsMissing() throws Exception {
Path directory = temporaryFolder.newFolder("generation").toPath();
assertThrows(NoSuchFileException.class, () -> GenerationHistoryStore.open(directory, CLOCK));
}
@Test
public void failsClosedOnMalformedOrUnknownSchema() throws Exception {
Path malformedDirectory = temporaryFolder.newFolder("malformed").toPath();
Path malformedManifest = malformedDirectory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME);
Files.writeString(malformedManifest, "{not-json", StandardCharsets.UTF_8);
assertThrows(IOException.class, () -> GenerationHistoryStore.open(malformedDirectory, CLOCK));
Path unknownDirectory = temporaryFolder.getRoot().toPath().resolve("unknown-schema");
GenerationHistoryStore.initialize(unknownDirectory, epoch(PACK_A), CLOCK);
JsonObject unknown = readJson(unknownDirectory);
unknown.addProperty("schemaVersion", GenerationManifest.CURRENT_SCHEMA_VERSION + 1);
writeJson(unknownDirectory, unknown);
assertThrows(IOException.class, () -> GenerationHistoryStore.open(unknownDirectory, CLOCK));
}
@Test
public void failsClosedOnMissingEpochReference() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("missing-epoch");
GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK);
JsonObject json = readJson(directory);
json.getAsJsonArray("activations")
.get(0)
.getAsJsonObject()
.addProperty("epochId", PACK_B);
writeJson(directory, json);
assertThrows(IOException.class, () -> GenerationHistoryStore.open(directory, CLOCK));
}
@Test
public void failsClosedOnForgedEpochIdentityAndUnknownFields() throws Exception {
Path forgedDirectory = temporaryFolder.getRoot().toPath().resolve("forged");
GenerationHistoryStore.initialize(forgedDirectory, epoch(PACK_A), CLOCK);
JsonObject forged = readJson(forgedDirectory);
forged.getAsJsonArray("epochs")
.get(0)
.getAsJsonObject()
.addProperty("epochId", PACK_B);
writeJson(forgedDirectory, forged);
assertThrows(IOException.class, () -> GenerationHistoryStore.open(forgedDirectory, CLOCK));
Path unknownFieldDirectory = temporaryFolder.getRoot().toPath().resolve("unknown-field");
GenerationHistoryStore.initialize(unknownFieldDirectory, epoch(PACK_A), CLOCK);
JsonObject unknownField = readJson(unknownFieldDirectory);
unknownField.addProperty("unexpected", true);
writeJson(unknownFieldDirectory, unknownField);
assertThrows(IOException.class, () -> GenerationHistoryStore.open(unknownFieldDirectory, CLOCK));
}
@Test
public void rejectsInitializationOverExistingManifest() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation");
GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK);
assertThrows(
FileAlreadyExistsException.class,
() -> GenerationHistoryStore.initialize(directory, epoch(PACK_B), CLOCK)
);
GenerationHistoryStore reopened = GenerationHistoryStore.open(directory, CLOCK);
assertEquals(PACK_A, reopened.activeEpoch().packFingerprint());
}
@Test
public void failedActivationDoesNotChangePersistedState() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation");
GenerationHistoryStore store = GenerationHistoryStore.initialize(directory, epoch(PACK_A), CLOCK);
store.preparePendingActivation(epoch(PACK_B), 512);
assertThrows(IllegalStateException.class, () -> store.activatePending(99L));
GenerationHistoryStore reopened = GenerationHistoryStore.open(directory, CLOCK);
assertEquals(PACK_A, reopened.activeEpoch().packFingerprint());
assertTrue(reopened.pendingActivation().isPresent());
}
@Test
public void rejectsSymbolicLinkStorageRootAndParent() throws Exception {
Path probe = temporaryFolder.newFolder("history-symlink-probe").toPath();
Path probeTarget = temporaryFolder.newFolder("history-symlink-probe-target").toPath();
try {
Files.createSymbolicLink(probe.resolve("link"), probeTarget);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
Path rootContainer = temporaryFolder.newFolder("history-linked-root-container").toPath();
Path linkedRoot = rootContainer.resolve("generation");
Files.createSymbolicLink(linkedRoot, temporaryFolder.newFolder("history-linked-root-target").toPath());
assertThrows(
IOException.class,
() -> GenerationHistoryStore.initialize(linkedRoot, epoch(PACK_A), CLOCK)
);
Path parentContainer = temporaryFolder.newFolder("history-linked-parent-container").toPath();
Path linkedParent = parentContainer.resolve("iris");
Files.createSymbolicLink(linkedParent, temporaryFolder.newFolder("history-linked-parent-target").toPath());
assertThrows(
IOException.class,
() -> GenerationHistoryStore.initialize(linkedParent.resolve("generation"), epoch(PACK_A), CLOCK)
);
}
@Test
public void concurrentInitializationCannotClobberTheInitialManifest() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("generation-race");
CyclicBarrier barrier = new CyclicBarrier(2);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> first = executor.submit(
() -> initializeAfterBarrier(directory, epoch(PACK_A), barrier)
);
Future<Boolean> second = executor.submit(
() -> initializeAfterBarrier(directory, epoch(PACK_B), barrier)
);
assertTrue(first.get() ^ second.get());
String fingerprint = GenerationHistoryStore.open(directory, CLOCK).activeEpoch().packFingerprint();
assertTrue(fingerprint.equals(PACK_A) || fingerprint.equals(PACK_B));
} finally {
executor.shutdownNow();
}
}
@Test
public void postRenameSyncFailureReconcilesTheLiveManifest() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("sync-reconcile");
AtomicInteger syncCount = new AtomicInteger();
GenerationHistoryStore.DirectorySync directorySync = ignored -> {
if (syncCount.incrementAndGet() > 1) {
throw new IOException("Injected directory sync failure");
}
};
GenerationHistoryStore store = GenerationHistoryStore.initialize(
directory,
epoch(PACK_A),
CLOCK,
directorySync
);
assertThrows(IOException.class, () -> store.preparePendingActivation(epoch(PACK_B), 512));
assertEquals(PACK_B, store.pendingEpoch().orElseThrow().packFingerprint());
assertEquals(store.manifest(), GenerationHistoryStore.open(directory, CLOCK).manifest());
}
@Test
public void failedPostRenameReconciliationPoisonsTheLiveStore() throws Exception {
Path directory = temporaryFolder.getRoot().toPath().resolve("sync-poison");
AtomicInteger syncCount = new AtomicInteger();
GenerationHistoryStore.DirectorySync directorySync = syncedDirectory -> {
if (syncCount.incrementAndGet() > 1) {
Files.writeString(syncedDirectory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME), "{");
throw new IOException("Injected unrecoverable directory sync failure");
}
};
GenerationHistoryStore store = GenerationHistoryStore.initialize(
directory,
epoch(PACK_A),
CLOCK,
directorySync
);
assertThrows(IOException.class, () -> store.preparePendingActivation(epoch(PACK_B), 512));
assertThrows(IllegalStateException.class, store::activeEpoch);
}
private static boolean initializeAfterBarrier(
Path directory,
GenerationEpoch epoch,
CyclicBarrier barrier
) throws Exception {
barrier.await();
try {
GenerationHistoryStore.initialize(directory, epoch, CLOCK);
return true;
} catch (IOException expected) {
return false;
}
}
private static GenerationEpoch epoch(String packFingerprint) {
GenerationEpoch.DimensionContract contract = new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
return GenerationEpoch.create(new GenerationEpoch.Spec(
packFingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
42L,
1,
1,
1,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract,
GenerationRegistryContract.empty()
));
}
private static String digest(char value) {
return String.valueOf(value).repeat(64);
}
private static JsonObject readJson(Path directory) throws IOException {
String content = Files.readString(
directory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME),
StandardCharsets.UTF_8
);
return JsonParser.parseString(content).getAsJsonObject();
}
private static void writeJson(Path directory, JsonObject json) throws IOException {
Files.writeString(
directory.resolve(GenerationHistoryStore.MANIFEST_FILE_NAME),
json.toString(),
StandardCharsets.UTF_8
);
}
}
@@ -0,0 +1,789 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class GenerationHistoryTest {
private static final int SECTOR_BYTES = 4_096;
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void upperContentUpdatesPreserveLayoutAndDistinctActivationsAcrossRestarts() throws Exception {
Path world = temporaryFolder.newFolder("upper-update-world").toPath();
Path packA = createPack("upper-update-a", "alpha");
Path packB = createPack("upper-update-b", "beta");
GenerationEpoch.DimensionContract contractA = upperContract(fingerprint(packA));
GenerationEpoch.DimensionContract contractB = upperContract(fingerprint(packB));
GenerationHistory history = GenerationHistory.create(
world, packA, fingerprint(packA), 42L, contractA, GenerationRegistryContract.empty());
String epochA = history.activeEpoch().epochId();
history.stageUpdate(packB, fingerprint(packB), contractB, GenerationRegistryContract.empty(), 32);
history.promotePending(List.of());
GenerationHistory restarted = GenerationHistory.open(world);
assertEquals(contractB, restarted.activeEpoch().dimensionContract());
restarted.stageUpdate(packA, fingerprint(packA), contractA, GenerationRegistryContract.empty(), 32);
restarted.promotePending(List.of());
GenerationHistory returned = GenerationHistory.open(world);
assertEquals(3L, returned.activeActivation().activationId());
assertEquals(epochA, returned.activeEpoch().epochId());
assertFalse(returned.paths().activationMantleRoot(1L).equals(returned.paths().activationMantleRoot(3L)));
}
@Test
public void createPublishesThePackBeforeTheCanonicalManifest() throws Exception {
Path world = temporaryFolder.newFolder("create-world").toPath();
Path pack = createPack("create-pack", "alpha");
String fingerprint = fingerprint(pack);
GenerationHistory history = createHistory(world, pack);
assertTrue(Files.isRegularFile(history.paths().manifest()));
assertTrue(Files.isRegularFile(history.activePackRoot().resolve("dimensions/main.json")));
assertEquals(1L, history.activeActivation().activationId());
assertEquals(1L, history.resolveActivation(40, -70).activationId());
assertEquals(fingerprint, history.resolveEpoch(40, -70).packFingerprint());
}
@Test
public void failedPackPublicationNeverCreatesAManifest() throws Exception {
Path world = temporaryFolder.newFolder("failed-create-world").toPath();
Path pack = createPack("failed-create-pack", "alpha");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
assertThrows(
IOException.class,
() -> GenerationHistory.create(
world,
pack,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertFalse(Files.exists(paths.manifest()));
}
@Test
public void openFailsClosedWhenAnyReferencedPackIsMissingOrChanged() throws Exception {
Path missingWorld = temporaryFolder.newFolder("missing-world").toPath();
Path missingPack = createPack("missing-pack", "alpha");
GenerationHistory missingHistory = createHistory(missingWorld, missingPack);
Files.delete(missingHistory.activePackRoot().resolve("dimensions/main.json"));
assertThrows(IOException.class, () -> GenerationHistory.open(missingWorld));
Path changedWorld = temporaryFolder.newFolder("changed-world").toPath();
Path changedPack = createPack("changed-pack", "alpha");
GenerationHistory changedHistory = createHistory(changedWorld, changedPack);
Files.writeString(changedHistory.activePackRoot().resolve("dimensions/main.json"), "changed");
assertThrows(IOException.class, () -> GenerationHistory.open(changedWorld));
}
@Test
public void openRejectsUnsupportedGeneratorAndRngVersions() throws Exception {
Path abiWorld = temporaryFolder.newFolder("abi-world").toPath();
Path abiPack = createPack("abi-pack", "alpha");
initializeRawHistory(abiWorld, abiPack, 2, GenerationKernelRegistry.standard().current().rngVersion());
IOException abiFailure = assertThrows(IOException.class, () -> GenerationHistory.open(abiWorld));
assertTrue(abiFailure.getMessage().contains("generation ABI"));
Path rngWorld = temporaryFolder.newFolder("rng-world").toPath();
Path rngPack = createPack("rng-pack", "alpha");
initializeRawHistory(rngWorld, rngPack, GenerationKernelRegistry.standard().current().generatorAbi(), 2);
IOException rngFailure = assertThrows(IOException.class, () -> GenerationHistory.open(rngWorld));
assertTrue(rngFailure.getMessage().contains("RNG/seed derivation version"));
}
@Test
public void promotionFreezesAllocatedChunksBeforePublishingTheNewActivation() throws Exception {
Path world = temporaryFolder.newFolder("promotion-world").toPath();
Path packA = createPack("promotion-pack-a", "alpha");
Path packB = createPack("promotion-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{3, 4}, {31, 31}});
GenerationActivation pending = stage(history, packB);
GenerationActivation active = history.promotePending(signaturesForChunks(new int[][]{{3, 4}, {31, 31}}));
assertEquals(2L, pending.activationId());
assertEquals(pending.activationId(), active.activationId());
assertEquals(pending.epochId(), active.epochId());
assertTrue(active.transition().isComplete());
assertEquals(1L, history.resolveActivation(3, 4).activationId());
assertEquals(1L, history.resolveActivation(31, 31).activationId());
assertEquals(2L, history.resolveActivation(32, 31).activationId());
GenerationBoundary boundary = history.boundary(2L);
assertTrue(boundary.isHistoricalChunk(3, 4));
assertTrue(boundary.isHistoricalChunk(31, 31));
assertFalse(boundary.isHistoricalChunk(32, 31));
assertEquals(history.paths().packRoot(history.activeEpoch().epochId()), history.activePackRoot());
assertTrue(Files.isRegularFile(history.packRoot(1L).resolve("dimensions/main.json")));
assertEquals(256, history.transitionPlan(2L).widthBlocks());
assertEquals(boundary.exposedBlockColumns().size(), history.terrainSignatures(2L).size());
GenerationHistory reopened = GenerationHistory.open(world);
assertEquals(1L, reopened.resolveActivation(3, 4).activationId());
assertEquals(2L, reopened.resolveActivation(32, 31).activationId());
assertEquals(2, reopened.explicitChunkCount());
assertEquals(256, reopened.transitionPlan(2L).widthBlocks());
}
@Test
public void promotionCapturesSignaturesFromTheDurableBoundary() throws Exception {
Path world = temporaryFolder.newFolder("capture-boundary-world").toPath();
Path packA = createPack("capture-boundary-pack-a", "alpha");
Path packB = createPack("capture-boundary-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{3, 4}});
stage(history, packB);
GenerationActivation active = history.promotePending(boundary -> {
assertTrue(boundary.isHistoricalChunk(3, 4));
assertFalse(boundary.isHistoricalChunk(4, 4));
return GenerationHistoryTest::signature;
});
assertEquals(2L, active.activationId());
assertEquals(history.boundary(2L).identity(), active.transition().boundaryIdentity());
}
@Test
public void promotionIsIdempotentForEmptyAndRecoveredWorlds() throws Exception {
Path emptyWorld = temporaryFolder.newFolder("empty-world").toPath();
Path emptyPackA = createPack("empty-pack-a", "alpha");
Path emptyPackB = createPack("empty-pack-b", "beta");
GenerationHistory emptyHistory = createHistory(emptyWorld, emptyPackA);
stage(emptyHistory, emptyPackB);
assertEquals(2L, emptyHistory.promotePending(List.of()).activationId());
assertEquals(2L, emptyHistory.promotePending(List.of()).activationId());
assertEquals(2L, emptyHistory.resolveActivation(0, 0).activationId());
assertEquals(0, emptyHistory.explicitChunkCount());
Path recoveredWorld = temporaryFolder.newFolder("recovered-world").toPath();
Path recoveredPackA = createPack("recovered-pack-a", "alpha");
Path recoveredPackB = createPack("recovered-pack-b", "beta");
GenerationHistory recovered = createHistory(recoveredWorld, recoveredPackA);
stage(recovered, recoveredPackB);
GenerationHistory reopened = GenerationHistory.open(recoveredWorld);
assertEquals(2L, reopened.promotePending(List.of()).activationId());
assertEquals(2L, reopened.resolveActivation(8, 9).activationId());
}
@Test
public void stageUpdateIsIdempotentAndRejectsIncompatibleCandidates() throws Exception {
Path world = temporaryFolder.newFolder("stage-world").toPath();
Path packA = createPack("stage-pack-a", "alpha");
Path packB = createPack("stage-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
GenerationActivation first = stage(history, packB);
GenerationActivation repeated = stage(history, packB);
assertEquals(first, repeated);
assertEquals(2, history.manifest().activations().size());
GenerationEpoch.DimensionContract incompatible = new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
512,
512,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
assertThrows(
IllegalArgumentException.class,
() -> history.stageUpdate(
packB,
fingerprint(packB),
incompatible,
GenerationRegistryContract.empty(),
256
)
);
}
@Test
public void legacyAdoptionIsDurableExplicitAndIdempotent() throws Exception {
Path world = temporaryFolder.newFolder("legacy-world").toPath();
Path legacy = Files.createDirectories(world.resolve("iris/pack/dimensions"));
Files.writeString(legacy.resolve("main.json"), "legacy");
Path legacyRoot = world.resolve("iris/pack");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("r.0.0.lz4b"), "mantle");
String fingerprint = fingerprint(legacyRoot);
GenerationHistory adopted = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertFalse(Files.exists(legacyRoot));
assertFalse(Files.exists(legacyMantle));
assertTrue(Files.isDirectory(adopted.activePackRoot()));
Path activationMantle = adopted.paths().activationMantleRoot(1L);
assertEquals("mantle", Files.readString(activationMantle.resolve("r.0.0.lz4b")));
assertEquals(1L, adopted.activeActivation().activationId());
GenerationHistory repeated = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertEquals(adopted.activeEpoch(), repeated.activeEpoch());
assertEquals("mantle", Files.readString(repeated.paths().activationMantleRoot(1L)
.resolve("r.0.0.lz4b")));
}
@Test
public void legacyMantleAdoptionFailsClosedOnDualRoots() throws Exception {
Path world = temporaryFolder.newFolder("legacy-dual-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("legacy.bin"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Files.writeString(activationMantle.resolve("activation.bin"), "activation");
IOException failure = assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(failure.getMessage().contains("mantle roots conflict"));
assertEquals("legacy", Files.readString(legacyMantle.resolve("legacy.bin")));
assertEquals("activation", Files.readString(activationMantle.resolve("activation.bin")));
}
@Test
public void legacyMantleAdoptionRejectsRootAliases() throws Exception {
Path world = temporaryFolder.newFolder("legacy-aliased-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Path alias = paths.legacyMantleRoot();
try {
Files.createSymbolicLink(alias, activationMantle);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isSymbolicLink(alias));
assertTrue(Files.isDirectory(activationMantle));
}
@Test
public void legacyAdoptionNeverDeletesMismatchedOrUnsafeInput() throws Exception {
Path mismatchWorld = temporaryFolder.newFolder("legacy-mismatch-world").toPath();
Path mismatchRoot = Files.createDirectories(mismatchWorld.resolve("iris/pack/dimensions")).getParent();
Files.writeString(mismatchRoot.resolve("dimensions/main.json"), "legacy");
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
mismatchWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(mismatchRoot));
assertFalse(Files.exists(GenerationHistoryPaths.forDimension(mismatchWorld).manifest()));
Path linkedWorld = temporaryFolder.newFolder("legacy-linked-world").toPath();
Files.createDirectories(linkedWorld.resolve("iris"));
Path outside = temporaryFolder.newFolder("legacy-outside").toPath();
Path link = linkedWorld.resolve("iris/pack");
try {
Files.createSymbolicLink(link, outside);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
linkedWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(outside));
assertTrue(Files.isSymbolicLink(link));
}
@Test
public void openFailsClosedOnDanglingOwnershipAndPartialHistory() throws Exception {
Path danglingWorld = temporaryFolder.newFolder("dangling-world").toPath();
Path pack = createPack("dangling-pack", "alpha");
GenerationHistory history = createHistory(danglingWorld, pack);
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
ownership.assign(1, 2, 99L);
ownership.persist();
assertThrows(IOException.class, () -> GenerationHistory.open(danglingWorld));
Path absentWorld = temporaryFolder.newFolder("absent-world").toPath();
Optional<GenerationHistory> absent = GenerationHistory.openIfPresent(absentWorld);
assertTrue(absent.isEmpty());
Path partialWorld = temporaryFolder.newFolder("partial-world").toPath();
Files.createDirectories(GenerationHistoryPaths.forDimension(partialWorld).generationRoot());
assertThrows(NoSuchFileException.class, () -> GenerationHistory.openIfPresent(partialWorld));
}
@Test
public void stagingTheActiveEpochIsANoOp() throws Exception {
Path world = temporaryFolder.newFolder("same-epoch-world").toPath();
Path pack = createPack("same-epoch-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
GenerationActivation activation = stage(history, pack);
assertEquals(history.activeActivation(), activation);
assertTrue(history.pendingActivation().isEmpty());
assertEquals(1, history.manifest().activations().size());
}
@Test
public void returningToAnEarlierEpochCreatesANewActivation() throws Exception {
Path world = temporaryFolder.newFolder("return-world").toPath();
Path packA = createPack("return-pack-a", "alpha");
Path packB = createPack("return-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
String epochA = history.activeEpoch().epochId();
stage(history, packB);
history.promotePending(List.of());
GenerationActivation activationA2 = stage(history, packA);
history.promotePending(List.of());
assertEquals(3L, activationA2.activationId());
assertEquals(epochA, activationA2.epochId());
assertEquals(2, history.manifest().epochs().size());
assertEquals(3, history.manifest().activations().size());
}
@Test
public void retainedAbiRegistryReopensEveryHistoricalEpoch() throws Exception {
Path world = temporaryFolder.newFolder("retained-abi-world").toPath();
Path packA = createPack("retained-abi-pack-a", "alpha");
Path packB = createPack("retained-abi-pack-b", "beta");
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = new GenerationKernelRegistry(
versionTwo,
List.of(
new GenerationKernelRegistry.Kernel(
1,
"1".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
),
new GenerationKernelRegistry.Kernel(
2,
"2".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
)
)
);
GenerationHistory history = GenerationHistory.create(
world,
packA,
fingerprint(packA),
42L,
contract(),
GenerationRegistryContract.empty(),
versionOne,
kernels
);
history.stageUpdate(
packB,
fingerprint(packB),
contract(),
GenerationRegistryContract.empty(),
256,
versionTwo
);
history.promotePending(List.of());
assertThrows(IOException.class, () -> GenerationHistory.open(world));
GenerationHistory reopened = GenerationHistory.open(world, kernels);
assertEquals(2, reopened.activeEpoch().generatorAbi());
assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId());
}
@Test
public void openRejectsWorldSeedMismatch() throws Exception {
Path world = temporaryFolder.newFolder("seed-world").toPath();
Path pack = createPack("seed-pack", "alpha");
createHistory(world, pack);
assertThrows(IOException.class, () -> GenerationHistory.open(world, 43L));
assertEquals(42L, GenerationHistory.open(world, 42L).activeEpoch().worldSeed());
}
@Test
public void openFailsClosedWhenAnOwnershipShardDisappears() throws Exception {
Path world = temporaryFolder.newFolder("missing-ownership-world").toPath();
Path packA = createPack("missing-ownership-pack-a", "alpha");
Path packB = createPack("missing-ownership-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverResumesAfterPartialOutgoingOwnershipPublication() throws Exception {
Path world = temporaryFolder.newFolder("partial-cutover-world").toPath();
Path packA = createPack("partial-cutover-pack-a", "alpha");
Path packB = createPack("partial-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
int[][] chunks = {{1, 2}, {3, 4}};
writeRegion(region, chunks);
stage(history, packB);
ChunkGenerationOwnership partial = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(partial.assign(1, 2, 1L));
partial.persist();
GenerationHistory recovered = GenerationHistory.open(world);
assertEquals(1, recovered.explicitChunkCount());
assertEquals(2L, recovered.promotePending(signaturesForChunks(chunks)).activationId());
assertEquals(2, GenerationHistory.open(world).explicitChunkCount());
}
@Test
public void pendingCutoverStillRejectsMissingOlderOwnership() throws Exception {
Path world = temporaryFolder.newFolder("pending-missing-old-world").toPath();
Path packA = createPack("pending-missing-old-pack-a", "alpha");
Path packB = createPack("pending-missing-old-pack-b", "beta");
Path packC = createPack("pending-missing-old-pack-c", "gamma");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
stage(history, packC);
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverRejectsOwnershipForUngeneratedChunks() throws Exception {
Path world = temporaryFolder.newFolder("pending-forged-world").toPath();
Path packA = createPack("pending-forged-pack-a", "alpha");
Path packB = createPack("pending-forged-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
ChunkGenerationOwnership forged = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(forged.assign(9, 9, 1L));
forged.persist();
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void sealedGenerationClaimSurvivesCrashBeforeChunkAllocationAndJoinsCutoverOwnership() throws Exception {
Path world = temporaryFolder.newFolder("semantic-claim-cutover-world").toPath();
Path packA = createPack("semantic-claim-cutover-pack-a", "alpha");
Path packB = createPack("semantic-claim-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
GenerationHistory.GenerationStage generation = history.openStage(9, 9);
ChunkGenerationSemantics claim = ChunkGenerationSemantics.builder(9, 9, 1L)
.addSurfaceBiome("iris:forest")
.seal()
.build();
assertTrue(history.claimGeneratedSemantics(generation, claim));
assertFalse(history.claimGeneratedSemantics(generation, claim));
generation.close();
assertThrows(IllegalStateException.class, () -> history.claimGeneratedSemantics(generation, claim));
assertFalse(Files.exists(world.resolve("region")));
stage(history, packB);
Path recoveredWorld = temporaryFolder.newFolder("restored-semantic-claim-world").toPath();
try (Stream<Path> entries = Files.walk(world)) {
for (Path source : entries.toList()) {
Path target = recoveredWorld.resolve(world.relativize(source));
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
Files.copy(source, target);
}
}
}
GenerationHistory recovered = GenerationHistory.open(recoveredWorld);
recovered.promotePending(signaturesForChunks(new int[][]{{9, 9}}));
assertEquals(1L, recovered.resolveActivation(9, 9).activationId());
assertEquals(2L, recovered.resolveActivation(10, 9).activationId());
assertEquals(1, recovered.explicitChunkCount());
GenerationHistory reopened = GenerationHistory.open(recoveredWorld);
assertEquals(Optional.of(claim), reopened.semantics(9, 9));
assertEquals(1L, reopened.resolveActivation(9, 9).activationId());
}
@Test
public void openFailsClosedOnSemanticActivationMismatch() throws Exception {
Path world = temporaryFolder.newFolder("semantic-mismatch-world").toPath();
Path packA = createPack("semantic-mismatch-pack-a", "alpha");
Path packB = createPack("semantic-mismatch-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
history.promotePending(List.of());
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.recordAndPersist(ChunkGenerationSemantics.builder(7, 8, 1L)
.addSurfaceBiome("iris:old")
.seal()
.build());
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void promotionIsRejectedAfterGenerationAdmissionOpens() throws Exception {
Path world = temporaryFolder.newFolder("admission-world").toPath();
Path packA = createPack("admission-pack-a", "alpha");
Path packB = createPack("admission-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
GenerationHistory.GenerationStage generationStage = history.openStage(2, 3);
assertEquals(1L, generationStage.activation().activationId());
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> history.promotePending(List.of())
);
assertTrue(failure.getMessage().contains("before generation admission opens"));
generationStage.close();
assertThrows(IllegalStateException.class, () -> history.promotePending(List.of()));
}
@Test
public void openFailsClosedOnDeletedOrCorruptTransitionSnapshots() throws Exception {
Path deletedWorld = temporaryFolder.newFolder("deleted-snapshot-world").toPath();
Path deletedPackA = createPack("deleted-snapshot-pack-a", "alpha");
Path deletedPackB = createPack("deleted-snapshot-pack-b", "beta");
GenerationHistory deletedHistory = createHistory(deletedWorld, deletedPackA);
stage(deletedHistory, deletedPackB);
deletedHistory.promotePending(List.of());
Files.delete(new TerrainBoundarySignatureStore(deletedWorld).snapshotPath(2L));
assertThrows(IOException.class, () -> GenerationHistory.open(deletedWorld));
Path corruptWorld = temporaryFolder.newFolder("corrupt-snapshot-world").toPath();
Path corruptPackA = createPack("corrupt-snapshot-pack-a", "alpha");
Path corruptPackB = createPack("corrupt-snapshot-pack-b", "beta");
GenerationHistory corruptHistory = createHistory(corruptWorld, corruptPackA);
stage(corruptHistory, corruptPackB);
corruptHistory.promotePending(List.of());
Files.write(new GenerationBoundaryStore(corruptWorld).snapshotPath(2L), new byte[]{1, 2, 3});
assertThrows(IOException.class, () -> GenerationHistory.open(corruptWorld));
}
private void initializeRawHistory(
Path world,
Path pack,
int generatorAbi,
int rngVersion
) throws IOException {
String fingerprint = fingerprint(pack);
GenerationEpoch epoch = GenerationEpoch.create(new GenerationEpoch.Spec(
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
42L,
GenerationEpoch.CURRENT_SEED_DERIVATION_VERSION,
generatorAbi,
rngVersion,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract(),
GenerationRegistryContract.empty()
));
GenerationPackRepository repository = new GenerationPackRepository(world);
repository.publish(
epoch.epochId(),
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
pack
);
GenerationSemanticIndex.initialize(world);
GenerationHistoryStore.initialize(repository.generationRoot(), epoch);
}
private Path createPack(String name, String content) throws IOException {
Path pack = temporaryFolder.newFolder(name).toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), content);
return pack;
}
private static String fingerprint(Path pack) throws IOException {
return GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
}
private static GenerationEpoch.DimensionContract upperContract(String packFingerprint) {
GenerationEpoch.DimensionContract base = contract();
return new GenerationEpoch.DimensionContract(
base.dimensionKey(), base.dimensionTypeKey(), base.environment(), base.generationMode(),
base.internalFluidHeight(), base.minHeight(), base.height(), base.logicalHeight(),
base.coordinateScale(), true, "ceiling", 32, packFingerprint,
base.dimensionTypeFingerprintSchema(), base.dimensionTypeFingerprint());
}
private static GenerationEpoch.DimensionContract contract() {
return new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
}
private static GenerationHistory createHistory(Path world, Path pack) throws IOException {
return GenerationHistory.create(
world,
pack,
fingerprint(pack),
42L,
contract(),
GenerationRegistryContract.empty()
);
}
private static GenerationActivation stage(GenerationHistory history, Path pack) throws IOException {
return history.stageUpdate(
pack,
fingerprint(pack),
contract(),
GenerationRegistryContract.empty(),
256
);
}
private static List<TerrainBoundarySignature> signaturesForChunks(int[][] chunks) {
ArrayList<GenerationBoundary.ChunkCoordinate> coordinates = new ArrayList<>(chunks.length);
for (int[] chunk : chunks) {
coordinates.add(new GenerationBoundary.ChunkCoordinate(chunk[0], chunk[1]));
}
GenerationBoundary boundary = GenerationBoundary.freeze("test-boundary", coordinates);
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>(boundary.exposedBlockColumns().size());
for (GenerationBoundary.BlockColumn column : boundary.exposedBlockColumns()) {
signatures.add(signature(column.blockX(), column.blockZ()));
}
return List.copyOf(signatures);
}
private static TerrainBoundarySignature signature(int blockX, int blockZ) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, 64, 63, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 64, 2),
new TerrainBoundarySignature.BiomeEncoding(
List.of("iris:test"),
new short[]{0, 0}
)
)
);
}
private static void writeRegion(Path file, int[][] chunks) throws IOException {
try (RandomAccessFile output = new RandomAccessFile(file.toFile(), "rw")) {
output.setLength((long) (2 + chunks.length) * SECTOR_BYTES);
for (int index = 0; index < chunks.length; index++) {
int chunkIndex = MCAFile.getChunkIndex(chunks[index][0], chunks[index][1]);
output.seek((long) chunkIndex * Integer.BYTES);
output.writeInt((2 + index) << Byte.SIZE | 1);
}
}
}
}
@@ -0,0 +1,42 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class GenerationKernelCatalogTest {
@Test
public void appendsNewRegistrationsWithoutChangingAnExistingSeal() throws Exception {
GenerationKernelCatalog.Catalog catalog = parse("current\t2\t3\t4\nkernel\t1\nkernel\t2\n");
assertEquals(new GenerationKernelRegistry.Version(2, 3, 4), catalog.current());
assertEquals(List.of(1, 2), catalog.abis());
}
@Test
public void rejectsMissingCurrentDuplicateAndExecutableCatalogEntries() {
assertThrows(IOException.class, () -> parse("current\t2\t1\t1\nkernel\t1\n"));
assertThrows(IOException.class, () -> parse("current\t1\t1\t1\nkernel\t1\nkernel\t1\n"));
assertThrows(IOException.class, () -> parse("current\t1\t1\t1\nkernel\t1\tfactory.Override\n"));
assertThrows(IOException.class, () -> parse("current\t1\t1\t1\nkernel\t01\n"));
}
@Test
public void installedCatalogUsesOnlyItsSealedFactoryAndAlgorithms() throws Exception {
GenerationKernelRegistry registry = GenerationKernelCatalog.load();
GenerationKernelSourceSeal.Descriptor seal = GenerationKernelSourceSeal.load(1);
assertEquals("art.arcane.iris.engine.history.GenerationKernelV1", seal.factoryClass());
assertEquals(seal.fingerprint(), registry.select(new GenerationKernelRegistry.Version(1, 1, 1))
.implementationFingerprint());
assertThrows(IOException.class, () -> registry.select(new GenerationKernelRegistry.Version(1, 2, 1)));
}
private static GenerationKernelCatalog.Catalog parse(String entries) throws IOException {
return GenerationKernelCatalog.parse(("iris-generation-kernel-catalog-v1\n" + entries)
.getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.SeedManager;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
public class GenerationKernelRegistryTest {
@Test
public void selectsAnExactImplementationFingerprint() throws Exception {
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(7, 3, 2);
String fingerprint = "a".repeat(64);
GenerationKernelRegistry registry = registry(version, fingerprint);
GenerationKernelRegistry.RuntimeKernel runtime = registry.select(version);
assertEquals(fingerprint, runtime.implementationFingerprint());
assertEquals(fingerprint, registry.requireSupported(version, fingerprint).implementationFingerprint());
}
@Test
public void algorithmTupleSelectsItsOwnSeedFactory() throws Exception {
SeedManager first = mock(SeedManager.class);
SeedManager second = mock(SeedManager.class);
GenerationKernelRegistry.Version original = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version changed = new GenerationKernelRegistry.Version(1, 2, 3);
GenerationKernelRegistry registry = new GenerationKernelRegistry(changed, Set.of(
new GenerationKernelRegistry.Kernel(1, "a".repeat(64), Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1), seedFactory(first),
new GenerationKernelRegistry.AlgorithmVersion(2, 3), seedFactory(second)))));
assertSame(first, registry.select(original).createSeedManager(991L));
assertSame(second, registry.select(changed).createSeedManager(991L));
}
@Test
public void missingSeedFactoryFailsClosed() throws Exception {
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(7, 3, 2);
assertThrows(UnsupportedOperationException.class,
() -> registry(version, "a".repeat(64)).select(version).createSeedManager(991L));
}
@Test
public void rejectsAnEpochWhoseImplementationChangedWithoutAVersionChange() {
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(7, 3, 2);
GenerationKernelRegistry registry = registry(version, "a".repeat(64));
assertThrows(IOException.class, () -> registry.requireSupported(version, "b".repeat(64)));
}
@Test
public void rejectsMalformedImplementationFingerprints() {
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(7, 3, 2);
assertThrows(IllegalArgumentException.class, () -> registry(version, "not-a-sha256"));
}
private static GenerationKernelRegistry registry(
GenerationKernelRegistry.Version version,
String fingerprint
) {
return new GenerationKernelRegistry(
version,
Set.of(new GenerationKernelRegistry.Kernel(
version.generatorAbi(),
fingerprint,
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(
version.rngVersion(),
version.seedDerivationVersion()
),
(engine, transitionPlan) -> mock(IrisComplex.class)
)
))
);
}
private static GenerationKernelRegistry.RuntimeFactory seedFactory(SeedManager seedManager) {
return new GenerationKernelRegistry.RuntimeFactory() {
@Override
public IrisComplex create(IrisEngine engine, TransitionGenerationPlan transitionPlan) {
return mock(IrisComplex.class);
}
@Override
public SeedManager createSeedManager(long worldSeed) {
assertEquals(991L, worldSeed);
return seedManager;
}
};
}
}
@@ -0,0 +1,56 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
public class GenerationKernelSourceSealTest {
@Test
public void fingerprintUsesTheCompleteManifest() throws Exception {
byte[] initial = seal("a".repeat(64));
assertEquals(GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial),
GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial));
assertNotEquals(GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", initial),
GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", seal("b".repeat(64))));
}
@Test
public void rejectsMismatchedAbiFactoryAndMalformedHashes() {
assertThrows(IOException.class,
() -> GenerationKernelSourceSeal.fingerprint(2, "example.KernelV1", seal("a".repeat(64))));
assertThrows(IOException.class,
() -> GenerationKernelSourceSeal.fingerprint(1, "example.OtherKernel", seal("a".repeat(64))));
assertThrows(IOException.class,
() -> GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", seal("not-a-hash")));
assertThrows(IOException.class,
() -> GenerationKernelSourceSeal.fingerprint(1, "example.KernelV1", new byte[0]));
}
@Test
public void rejectsDuplicateAndUnsortedEntries() {
String source = new String(seal("a".repeat(64)), StandardCharsets.UTF_8);
String duplicate = source + "dependency\tmath\t" + "a".repeat(64) + "\n";
assertThrows(IOException.class, () -> GenerationKernelSourceSeal.fingerprint(1,
"example.KernelV1", duplicate.getBytes(StandardCharsets.UTF_8)));
String unordered = source + "scope\tother\n";
assertThrows(IOException.class, () -> GenerationKernelSourceSeal.fingerprint(1,
"example.KernelV1", unordered.getBytes(StandardCharsets.UTF_8)));
}
@Test
public void standardKernelUsesThePackagedSourceSeal() {
assertEquals(GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class),
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT);
}
private static byte[] seal(String fingerprint) {
return ("iris-generation-kernel-seal-v1\nabi\t1\nfactory\texample.KernelV1\n"
+ "algorithm\t1\t1\nscope\tgeneration\nsource\tgeneration/Noise.java\t" + fingerprint
+ "\ndependency\tmath\t" + "c".repeat(64) + "\n").getBytes(StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,136 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationManifestTest {
private static final String PACK_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String PACK_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@Test
public void keepsEpochsSeparateFromMonotonicActivations() {
GenerationEpoch epochA = epoch(PACK_A, contract("iris:type_a"));
GenerationEpoch epochB = epoch(PACK_B, contract("iris:type_a"));
GenerationManifest initial = GenerationManifest.initial(epochA, 10L);
GenerationManifest pendingB = initial.preparePending(epochB, 20L, 256);
GenerationActivation activationB = pendingB.pendingActivation().orElseThrow();
GenerationManifest activeB = complete(pendingB, activationB.activationId()).activatePending(
activationB.activationId()
);
GenerationManifest pendingA = activeB.preparePending(epochA, 30L, 256);
GenerationActivation activationA2 = pendingA.pendingActivation().orElseThrow();
assertEquals(3L, activationA2.activationId());
assertEquals(Long.valueOf(2L), activationA2.parentActivationId());
assertEquals(epochA.epochId(), activationA2.epochId());
assertEquals(2, pendingA.epochs().size());
assertEquals(List.of(1L, 2L, 3L), pendingA.activations().stream()
.map(GenerationActivation::activationId)
.toList());
assertEquals(epochB, pendingA.activeEpoch());
}
@Test
public void repeatedPendingPreparationIsIdempotent() {
GenerationEpoch epochA = epoch(PACK_A, contract("iris:type_a"));
GenerationEpoch epochB = epoch(PACK_B, contract("iris:type_a"));
GenerationManifest pending = GenerationManifest.initial(epochA, 10L).preparePending(epochB, 20L, 256);
GenerationManifest repeated = pending.preparePending(epochB, 99L, 256);
assertSame(pending, repeated);
assertEquals(2, repeated.activations().size());
assertEquals(20L, repeated.pendingActivation().orElseThrow().createdAtEpochMillis());
}
@Test
public void refusesCompetingPendingActivation() {
GenerationEpoch epochA = epoch(PACK_A, contract("iris:type_a"));
GenerationEpoch epochB = epoch(PACK_B, contract("iris:type_a"));
GenerationEpoch epochC = epoch(
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
contract("iris:type_a")
);
GenerationManifest pending = GenerationManifest.initial(epochA, 10L).preparePending(epochB, 20L, 256);
assertThrows(IllegalStateException.class, () -> pending.preparePending(epochC, 30L, 256));
}
@Test
public void refusesImmutableDimensionContractChange() {
GenerationEpoch epochA = epoch(PACK_A, contract("iris:type_a"));
GenerationEpoch changedType = epoch(PACK_B, contract("iris:type_b"));
GenerationManifest manifest = GenerationManifest.initial(epochA, 10L);
assertThrows(IllegalArgumentException.class, () -> manifest.preparePending(changedType, 20L, 256));
assertFalse(manifest.pendingActivation().isPresent());
assertEquals(epochA, manifest.activeEpoch());
}
@Test
public void activatesOnlyTheExpectedPendingRecord() {
GenerationEpoch epochA = epoch(PACK_A, contract("iris:type_a"));
GenerationEpoch epochB = epoch(PACK_B, contract("iris:type_a"));
GenerationManifest pending = GenerationManifest.initial(epochA, 10L).preparePending(epochB, 20L, 256);
assertThrows(IllegalStateException.class, () -> pending.activatePending(3L));
assertThrows(IllegalArgumentException.class, () -> pending.activatePending(2L));
GenerationManifest active = complete(pending, 2L).activatePending(2L);
assertEquals(2L, active.activeActivation().activationId());
assertEquals(epochB, active.activeEpoch());
assertTrue(active.pendingActivation().isEmpty());
}
private static GenerationEpoch epoch(
String packFingerprint,
GenerationEpoch.DimensionContract contract
) {
return GenerationEpoch.create(new GenerationEpoch.Spec(
packFingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
42L,
GenerationEpoch.CURRENT_SEED_DERIVATION_VERSION,
1,
1,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract,
GenerationRegistryContract.empty()
));
}
private static GenerationEpoch.DimensionContract contract(String typeKey) {
return new GenerationEpoch.DimensionContract(
"overworld",
typeKey,
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
}
private static GenerationManifest complete(GenerationManifest manifest, long activationId) {
return manifest.completePendingTransition(
activationId,
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
);
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.engine.history;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
public class GenerationPackFingerprintTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void versionOneMatchesItsGoldenContentAddress() throws Exception {
Path pack = temporaryFolder.newFolder("golden-pack").toPath();
Files.createDirectories(pack.resolve("objects"));
Files.writeString(pack.resolve("objects/tree.json"), "oak");
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), "{}");
Files.createDirectories(pack.resolve(".git"));
Files.writeString(pack.resolve(".git/config"), "ignored");
Files.writeString(pack.resolve("workspace.code-workspace"), "ignored");
String fingerprint = GenerationPackFingerprint.compute(
pack,
GenerationPackFingerprint.CURRENT_VERSION
);
assertEquals("ad7d39c2b95bd3001248778911ce6bbd6b4cbdcece28145838c2b923a0bf73b3", fingerprint);
}
@Test
public void contentChangesProduceDifferentAddresses() throws Exception {
Path pack = temporaryFolder.newFolder("changed-pack").toPath();
Files.writeString(pack.resolve("dimension.json"), "first");
String first = GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
Files.writeString(pack.resolve("dimension.json"), "second");
String second = GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
assertNotEquals(first, second);
}
@Test
public void unsupportedVersionsAndSymbolicLinksFailClosed() throws Exception {
Path pack = temporaryFolder.newFolder("unsafe-pack").toPath();
assertThrows(IOException.class, () -> GenerationPackFingerprint.compute(pack, 2));
Path target = temporaryFolder.newFile("outside.json").toPath();
try {
Files.createSymbolicLink(pack.resolve("linked.json"), target);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
assertThrows(
IOException.class,
() -> GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION)
);
}
}
@@ -0,0 +1,233 @@
package art.arcane.iris.engine.history;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationPackRepositoryTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void publishesAnImmutableFilteredPack() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("world").toPath();
Path source = createPack("source", "{}");
Files.createDirectories(source.resolve(".git"));
Files.writeString(source.resolve(".git/config"), "local");
Files.writeString(source.resolve("pack.code-workspace"), "{}");
String fingerprint = fingerprint(source);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('a');
Path published = repository.publish(epochId, fingerprint, GenerationPackFingerprint.CURRENT_VERSION, source);
assertTrue(Files.isRegularFile(published.resolve("dimensions/main.json")));
assertFalse(Files.exists(published.resolve(".git")));
assertFalse(Files.exists(published.resolve("pack.code-workspace")));
assertEquals(published, repository.publish(
epochId,
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
source
));
}
@Test
public void rejectsMutationOfAnExistingEpoch() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("mutation-world").toPath();
Path source = createPack("first", "{}");
String fingerprint = fingerprint(source);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('b');
Path published = repository.publish(epochId, fingerprint, GenerationPackFingerprint.CURRENT_VERSION, source);
Files.writeString(published.resolve("dimensions/main.json"), "{\"changed\":true}");
assertThrows(IOException.class, () -> repository.requireExactPack(
epochId,
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION
));
assertThrows(IOException.class, () -> repository.publish(
epochId,
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
source
));
}
@Test
public void rejectsNestedSymbolicLinks() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("linked-world").toPath();
Path source = createPack("linked", "{}");
Path outside = temporaryFolder.newFile("outside.json").toPath();
try {
Files.createSymbolicLink(source.resolve("dimensions/link.json"), outside);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
assertThrows(IOException.class, () -> repository.publish(
digest('c'),
digest('d'),
GenerationPackFingerprint.CURRENT_VERSION,
source));
}
@Test
public void rejectsInvalidEpochIdentity() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("invalid-world").toPath();
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
assertThrows(IllegalArgumentException.class, () -> repository.packRoot("not-a-digest"));
assertThrows(IllegalArgumentException.class, () -> repository.packRoot(digest('A')));
}
@Test
public void rejectsSymbolicLinksAtEveryStorageComponent() throws Exception {
Path probe = temporaryFolder.newFolder("symlink-probe").toPath();
Path probeTarget = temporaryFolder.newFolder("symlink-probe-target").toPath();
try {
Files.createSymbolicLink(probe.resolve("link"), probeTarget);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
for (String component : new String[]{"dimension", "iris", "generation", "epochs", "epoch", "pack"}) {
assertStorageLinkRejected(component);
}
}
@Test
public void concurrentRepositoryInstancesCannotClobberAnEpoch() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("race-world").toPath();
Path firstSource = createPack("race-first", "{\"winner\":1}");
Path secondSource = createPack("race-second", "{\"winner\":2}");
String firstFingerprint = fingerprint(firstSource);
String secondFingerprint = fingerprint(secondSource);
String epochId = digest('e');
GenerationPackRepository firstRepository = new GenerationPackRepository(dimensionRoot);
GenerationPackRepository secondRepository = new GenerationPackRepository(dimensionRoot);
CyclicBarrier barrier = new CyclicBarrier(2);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> first = executor.submit(() -> publishAfterBarrier(
firstRepository,
epochId,
firstFingerprint,
firstSource,
barrier
));
Future<Boolean> second = executor.submit(() -> publishAfterBarrier(
secondRepository,
epochId,
secondFingerprint,
secondSource,
barrier
));
assertTrue(first.get() ^ second.get());
String content = Files.readString(firstRepository.packRoot(epochId).resolve("dimensions/main.json"));
assertTrue(content.equals("{\"winner\":1}") || content.equals("{\"winner\":2}"));
} finally {
executor.shutdownNow();
}
}
private void assertStorageLinkRejected(String component) throws Exception {
Path container = temporaryFolder.newFolder("linked-storage-" + component).toPath();
Path dimensionRoot = container.resolve("dimension");
Path external = temporaryFolder.newFolder("linked-storage-target-" + component).toPath();
String epochId = digest('f');
if (component.equals("dimension")) {
Files.createSymbolicLink(dimensionRoot, external);
} else {
Files.createDirectory(dimensionRoot);
Path iris = dimensionRoot.resolve("iris");
if (component.equals("iris")) {
Files.createSymbolicLink(iris, external);
} else {
Files.createDirectory(iris);
Path generation = iris.resolve("generation");
if (component.equals("generation")) {
Files.createSymbolicLink(generation, external);
} else {
Files.createDirectory(generation);
Path epochs = generation.resolve("epochs");
if (component.equals("epochs")) {
Files.createSymbolicLink(epochs, external);
} else {
Files.createDirectory(epochs);
Path epoch = epochs.resolve(epochId);
if (component.equals("epoch")) {
Files.createSymbolicLink(epoch, external);
} else {
Files.createDirectory(epoch);
Files.createSymbolicLink(epoch.resolve("pack"), external);
}
}
}
}
}
Path source = createPack("linked-storage-source-" + component, "{}");
String fingerprint = fingerprint(source);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
assertThrows(IOException.class, () -> repository.publish(
epochId,
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
source
));
}
private static boolean publishAfterBarrier(
GenerationPackRepository repository,
String epochId,
String fingerprint,
Path source,
CyclicBarrier barrier
) throws Exception {
barrier.await();
try {
repository.publish(
epochId,
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
source
);
return true;
} catch (IOException expected) {
return false;
}
}
private Path createPack(String name, String dimensionJson) throws IOException {
Path source = temporaryFolder.newFolder(name).toPath();
Files.createDirectories(source.resolve("dimensions"));
Files.writeString(source.resolve("dimensions/main.json"), dimensionJson);
return source;
}
private static String digest(char value) {
return String.valueOf(value).repeat(64);
}
private static String fingerprint(Path source) throws IOException {
return GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
}
}
@@ -0,0 +1,810 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.KeyStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisBiomeCustomSpawn;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.iris.engine.object.IrisDimensionTypeOptions;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
public class GenerationRegistryContractFactoryTest {
private static final String HASH_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String HASH_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void capturesGeneratedAndReferencedPhysicalDefinitions() throws Exception {
File pack = temporary.newFolder("pack");
Files.writeString(
pack.toPath().resolve("content.json"),
"{\"block\":\"minecraft:stone[axis=y]\",\"type\":\"minecraft:zombie\"}",
StandardCharsets.UTF_8
);
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(pack);
ContentGate contentGate = mock(ContentGate.class);
when(data.getContentGate()).thenReturn(contentGate);
IrisBiomeCustom custom = spy(new IrisBiomeCustom().setId("mist"));
IDataFixer fixer = DataVersion.getLatest().get();
doReturn("{\"effects\":{},\"temperature\":0.5}")
.when(custom).generateJson(fixer, contentGate);
IrisNativeStructure nativeStructure = new IrisNativeStructure().setStructure("minecraft:village_plains");
IrisStructurePlacement placement = new IrisStructurePlacement()
.setNativeStructures(new KList<>(nativeStructure));
IrisBiome biome = new IrisBiome()
.setDerivative("minecraft:plains")
.setVanillaDerivative("minecraft:forest")
.setBiomeScatter(new KList<>("minecraft:desert", "overworld:mist"))
.setBiomeSkyScatter(new KList<>("minecraft:taiga"))
.setCustomDerivitives(new KList<>(custom));
biome.setLoadKey("surface");
IrisDimension dimension = spy(new IrisDimension()
.setStructures(new KList<>(placement)));
dimension.setLoadKey("overworld");
stubDimensions(data, dimension);
doReturn(new KList<>(biome)).when(dimension).getReachableBiomes(any());
doReturn(new KList<>()).when(dimension).getAllRegions(any());
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.blockTypeKeys()).thenReturn(new KList<>("minecraft:stone", "minecraft:dirt"));
when(registries.entityKeys()).thenReturn(new KList<>("minecraft:zombie", "minecraft:pig"));
FakeGenerationRegistry generationRegistry = new FakeGenerationRegistry("test-runtime");
String packFingerprint = GenerationPackFingerprint.compute(
pack.toPath(),
GenerationPackFingerprint.CURRENT_VERSION
);
GenerationRegistryContract contract = GenerationRegistryContractFactory.create(
data,
dimension,
packFingerprint,
fixer,
registries,
generationRegistry
);
Set<GenerationRegistryContract.PhysicalResourceKey> keys = contract.definitions().keySet();
assertTrue(keys.contains(key(GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY, "test:overworld_type")));
String customFingerprint = GenerationRegistryContractFactory.fingerprintCustomBiomeIdentity(
GenerationRegistryContractFactory.fingerprintCustomBiomeAuthoredDefinition(custom),
"overworld",
"mist"
);
assertTrue(keys.contains(key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"test:biomes/" + customFingerprint
)));
assertFalse(keys.contains(key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"test:overworld/mist"
)));
assertFalse(keys.contains(key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"overworld:mist"
)));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.BIOME_REGISTRY, "minecraft:plains")));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.BIOME_REGISTRY, "minecraft:forest")));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.BIOME_REGISTRY, "minecraft:desert")));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.BIOME_REGISTRY, "minecraft:taiga")));
assertTrue(keys.contains(key(
GenerationRegistryContractFactory.STRUCTURE_REGISTRY,
"minecraft:village_plains"
)));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.BLOCK_REGISTRY, "minecraft:stone")));
assertTrue(keys.contains(key(GenerationRegistryContractFactory.ENTITY_TYPE_REGISTRY, "minecraft:zombie")));
GenerationRegistryContract adopted = GenerationRegistryContractFactory.create(
data,
dimension,
packFingerprint,
fixer,
registries,
generationRegistry,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
);
assertTrue(adopted.definitions().containsKey(key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"test:overworld/mist"
)));
}
@Test
public void duplicateLogicalBiomeIdsKeepExactDefinitionsAndChooseStableLegacyAlias() throws Exception {
File pack = temporary.newFolder("duplicate-biomes");
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(pack);
ContentGate contentGate = mock(ContentGate.class);
when(contentGate.entity("example:missing")).thenReturn(KeyStatus.MISSING);
when(data.getContentGate()).thenReturn(contentGate);
IrisBiomeCustom plateau = new IrisBiomeCustom().setId("savanna_plateau").setGrassColor("#FFCB31")
.setSpawns(new KList<>(new IrisBiomeCustomSpawn().setType("example:missing")));
IrisBiomeCustom vanilla = new IrisBiomeCustom().setId("savanna_plateau").setGrassColor("#BFB755");
IrisBiome first = new IrisBiome().setCustomDerivitives(new KList<>(plateau));
IrisBiome second = new IrisBiome().setCustomDerivitives(new KList<>(vanilla));
IrisDimension dimension = spy(new IrisDimension());
dimension.setLoadKey("overworld");
stubDimensions(data, dimension);
doReturn(new KList<>(first, second)).when(dimension).getAllBiomes(any());
doReturn(new KList<>(first)).when(dimension).getReachableBiomes(any());
doReturn(new KList<>()).when(dimension).getAllRegions(any());
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.blockTypeKeys()).thenReturn(List.of());
when(registries.entityKeys()).thenReturn(List.of());
FakeGenerationRegistry registry = new FakeGenerationRegistry("duplicate-runtime");
String fingerprint = GenerationPackFingerprint.compute(
pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION);
GenerationRegistryContract contract = GenerationRegistryContractFactory.create(
data, dimension, fingerprint, DataVersion.getLatest().get(), registries, registry,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES);
String plateauKey = GenerationRegistryContractFactory.customBiomeResourceKey(
"overworld", plateau, contentGate, registry);
String vanillaKey = GenerationRegistryContractFactory.customBiomeResourceKey(
"overworld", vanilla, contentGate, registry);
assertNotEquals(plateauKey, vanillaKey);
assertTrue(contract.definitions().containsKey(key(GenerationRegistryContractFactory.BIOME_REGISTRY, plateauKey)));
assertTrue(contract.definitions().containsKey(key(GenerationRegistryContractFactory.BIOME_REGISTRY, vanillaKey)));
String selected = plateauKey.compareTo(vanillaKey) < 0 ? plateauKey : vanillaKey;
assertEquals(contract.generatedSources().get(key(GenerationRegistryContractFactory.BIOME_REGISTRY, selected)).sourceJson(),
contract.generatedSources().get(key(GenerationRegistryContractFactory.BIOME_REGISTRY,
"test:overworld/savanna_plateau")).sourceJson());
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver =
new GenerationRegistryContractFactory.CustomBiomeResourceResolver(contract, registry::customBiomeResourceKey);
assertEquals(plateauKey, resolver.resolve("overworld", plateau));
assertEquals(vanillaKey, resolver.resolve("overworld", vanilla));
assertNotEquals(plateauKey, GenerationRegistryContractFactory.customBiomeResourceKey("overworld", plateau, registry));
assertThrows(IOException.class,
() -> resolver.resolve("overworld", new IrisBiomeCustom().setId("savanna_plateau").setGrassColor("#FF0000")));
doReturn(new KList<>(second, first)).when(dimension).getAllBiomes(any());
GenerationRegistryContract reversed = GenerationRegistryContractFactory.create(
data, dimension, fingerprint, DataVersion.getLatest().get(), registries, registry,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES);
assertEquals(contract, reversed);
}
@Test
public void customBiomeIdentityUsesAuthoredDefinitionAndLogicalIdentity() {
IrisBiomeCustom firstBiome = new IrisBiomeCustom().setId("mist").setTemperature(0.5D);
IrisBiomeCustom changedBiome = new IrisBiomeCustom().setId("mist").setTemperature(0.7D);
String firstDefinition = GenerationRegistryContractFactory
.fingerprintCustomBiomeAuthoredDefinition(firstBiome);
String changedDefinition = GenerationRegistryContractFactory
.fingerprintCustomBiomeAuthoredDefinition(changedBiome);
String first = GenerationRegistryContractFactory.fingerprintCustomBiomeIdentity(
firstDefinition,
"Overworld",
"Mist"
);
String normalized = GenerationRegistryContractFactory.fingerprintCustomBiomeIdentity(
firstDefinition,
"overworld",
"mist"
);
String changedBiomeIdentity = GenerationRegistryContractFactory.fingerprintCustomBiomeIdentity(
changedDefinition,
"overworld",
"mist"
);
assertEquals(first, normalized);
assertNotEquals(first, changedBiomeIdentity);
assertEquals("iris:biomes/" + first,
PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(first));
}
@Test
public void contentGateEffectsParticipateInCustomBiomeIdentity() {
IrisBiomeCustom custom = new IrisBiomeCustom()
.setId("mist")
.setSpawns(new KList<>(new IrisBiomeCustomSpawn().setType("example:animal")));
ContentGate present = mock(ContentGate.class);
ContentGate missing = mock(ContentGate.class);
when(present.entity("example:animal")).thenReturn(KeyStatus.PRESENT);
when(missing.entity("example:animal")).thenReturn(KeyStatus.MISSING);
String withSpawn = GenerationRegistryContractFactory.customBiomeResourceKey(
"overworld",
custom,
present
);
String withoutSpawn = GenerationRegistryContractFactory.customBiomeResourceKey(
"overworld",
custom,
missing
);
assertNotEquals(withSpawn, withoutSpawn);
}
@Test
public void unrelatedPackChangesReuseCustomBiomePhysicalIdentity() throws Exception {
File firstPack = temporary.newFolder("identity-pack-a");
File secondPack = temporary.newFolder("identity-pack-b");
Files.writeString(firstPack.toPath().resolve("unrelated.json"), "{\"value\":1}", StandardCharsets.UTF_8);
Files.writeString(secondPack.toPath().resolve("unrelated.json"), "{\"value\":2}", StandardCharsets.UTF_8);
ContentGate contentGate = mock(ContentGate.class);
IrisData firstData = mock(IrisData.class);
when(firstData.getDataFolder()).thenReturn(firstPack);
when(firstData.getContentGate()).thenReturn(contentGate);
IrisData secondData = mock(IrisData.class);
when(secondData.getDataFolder()).thenReturn(secondPack);
when(secondData.getContentGate()).thenReturn(contentGate);
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
IDataFixer fixer = mock(IDataFixer.class);
IrisBiomeCustom custom = spy(new IrisBiomeCustom().setId("mist").setTemperature(0.5D));
doReturn("{\"temperature\":0.5,\"effects\":{}}")
.when(custom).generateJson(fixer, contentGate);
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime");
GenerationRegistryContractFactory.CustomBiomeDefinition first =
GenerationRegistryContractFactory.customBiomeDefinition(
firstData,
dimension,
custom,
fixer,
registry
);
GenerationRegistryContractFactory.CustomBiomeDefinition second =
GenerationRegistryContractFactory.customBiomeDefinition(
secondData,
dimension,
custom,
fixer,
registry
);
assertNotEquals(
GenerationPackFingerprint.compute(firstPack.toPath(), GenerationPackFingerprint.CURRENT_VERSION),
GenerationPackFingerprint.compute(secondPack.toPath(), GenerationPackFingerprint.CURRENT_VERSION)
);
assertEquals(first.physicalKey(), second.physicalKey());
assertEquals(first.identityFingerprint(), second.identityFingerprint());
}
@Test
public void serializerChangesDoNotAlterCustomBiomePhysicalIdentityOrContract() throws Exception {
File pack = temporary.newFolder("serializer-stable-pack");
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(pack);
ContentGate contentGate = mock(ContentGate.class);
when(data.getContentGate()).thenReturn(contentGate);
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
IrisBiomeCustom custom = spy(new IrisBiomeCustom().setId("mist"));
IDataFixer firstFixer = mock(IDataFixer.class);
IDataFixer secondFixer = mock(IDataFixer.class);
doReturn(
"{\"temperature\":0.5,\"effects\":{}}"
).when(custom).generateJson(firstFixer, contentGate);
doReturn(
"{\"temperature\":0.5,\"effects\":{},\"new_schema_default\":true}"
).when(custom).generateJson(secondFixer, contentGate);
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime-a") {
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
return Definition.exactJson("{\"effects\":{},\"temperature\":0.5}");
}
};
GenerationRegistryContractFactory.CustomBiomeDefinition first =
GenerationRegistryContractFactory.customBiomeDefinition(
data,
dimension,
custom,
firstFixer,
registry
);
GenerationRegistryContractFactory.CustomBiomeDefinition second =
GenerationRegistryContractFactory.customBiomeDefinition(
data,
dimension,
custom,
secondFixer,
registry
);
assertEquals(first.physicalKey(), second.physicalKey());
assertEquals(first.identityFingerprint(), second.identityFingerprint());
assertEquals(first.definition(), second.definition());
assertEquals(first.contentFingerprint(), second.contentFingerprint());
assertEquals(first.sourceJson(), second.sourceJson());
assertEquals(
GenerationRegistryContractFactory.fingerprintDefinition(
first.physicalKey(),
first.definition(),
"runtime-a"
),
GenerationRegistryContractFactory.fingerprintDefinition(
second.physicalKey(),
second.definition(),
"runtime-b"
)
);
}
@Test
public void customBiomeDefinitionFingerprintCanonicalizesCurrentEmission() {
String first = GenerationRegistryContractFactory.fingerprintCustomBiomeDefinition(
"{\"temperature\":0.50,\"effects\":{\"sky_color\":1}}"
);
String reordered = GenerationRegistryContractFactory.fingerprintCustomBiomeDefinition(
"{\"effects\":{\"sky_color\":1.0},\"temperature\":0.5}"
);
String changed = GenerationRegistryContractFactory.fingerprintCustomBiomeDefinition(
"{\"effects\":{\"sky_color\":2},\"temperature\":0.5}"
);
assertEquals(first, reordered);
assertNotEquals(first, changed);
}
@Test
public void canonicalJsonIgnoresObjectOrderAndNumericFormatting() {
GenerationRegistryContract.PhysicalResourceKey key = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"minecraft:plains"
);
String first = GenerationRegistryContractFactory.fingerprintDefinition(
key,
PlatformGenerationRegistry.Definition.exactJson("{\"b\":2.0,\"a\":1}"),
"runtime-a"
);
String second = GenerationRegistryContractFactory.fingerprintDefinition(
key,
PlatformGenerationRegistry.Definition.exactJson("{\"a\":1.00,\"b\":2}"),
"runtime-b"
);
assertEquals(first, second);
}
@Test
public void resourceIdentitySurvivesCompatibleRuntimeUpdates() {
GenerationRegistryContract.PhysicalResourceKey key = key(
GenerationRegistryContractFactory.BLOCK_REGISTRY,
"minecraft:stone"
);
PlatformGenerationRegistry.Definition definition = PlatformGenerationRegistry.Definition.resourceIdentity(
key.registryKey(),
key.resourceKey()
);
String first = GenerationRegistryContractFactory.fingerprintDefinition(key, definition, "runtime-a");
String second = GenerationRegistryContractFactory.fingerprintDefinition(key, definition, "runtime-b");
assertEquals(first, second);
}
@Test
public void capturesAndValidatesTheRetainedContractUnion() throws Exception {
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime");
GenerationRegistryContract.PhysicalResourceKey plains = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"minecraft:plains"
);
GenerationRegistryContract.PhysicalResourceKey village = key(
GenerationRegistryContractFactory.STRUCTURE_REGISTRY,
"minecraft:village_plains"
);
GenerationRegistryContract first = contract(registry, plains);
GenerationRegistryContract second = contract(registry, village);
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(first, second),
registry
);
assertEquals(Set.of(plains, village), available.definitions().keySet());
first.requireDefinitionsAvailableIn(available);
second.requireDefinitionsAvailableIn(available);
}
@Test
public void rejectsConflictingRetainedPhysicalDefinitions() {
GenerationRegistryContract.PhysicalResourceKey plains = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"minecraft:plains"
);
GenerationRegistryContract first = GenerationRegistryContract.fromDefinitions(Map.of(plains, HASH_A));
GenerationRegistryContract second = GenerationRegistryContract.fromDefinitions(Map.of(plains, HASH_B));
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(first, second),
new FakeGenerationRegistry("runtime")
)
);
}
@Test
public void acceptsChangedExternalRuntimeDetailsButRejectsMissingKeys() throws Exception {
FakeGenerationRegistry original = new FakeGenerationRegistry("runtime");
GenerationRegistryContract.PhysicalResourceKey plains = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"minecraft:plains"
);
GenerationRegistryContract required = contract(original, plains);
FakeGenerationRegistry changed = new FakeGenerationRegistry("different-runtime");
changed.put(
plains,
PlatformGenerationRegistry.Definition.resourceIdentity(
plains.registryKey(),
plains.resourceKey()
)
);
FakeGenerationRegistry missing = new FakeGenerationRegistry("runtime");
missing.remove(plains);
assertEquals(
required,
GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), changed)
);
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), missing)
);
}
@Test
public void validatesGeneratedDefinitionsByRecordedCanonicalContent() throws Exception {
FakeGenerationRegistry registry = new FakeGenerationRegistry("runtime-a");
GenerationRegistryContract.PhysicalResourceKey generated = key(
GenerationRegistryContractFactory.BIOME_REGISTRY,
"iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
IrisBiomeCustom customBiome = new IrisBiomeCustom().setId("mist").setTemperature(0.5D);
String semantic = GenerationRegistryContractFactory.customBiomeEffectiveSemanticJson(customBiome, null);
String semanticFingerprint = GenerationRegistryContractFactory
.fingerprintCustomBiomeAuthoredDefinition(semantic);
IDataFixer firstFixer = new TaggedBiomeFixer("first");
String source = customBiome.generateJson(
firstFixer,
new ContentGate(null, Map.of(), null)
);
PlatformGenerationRegistry.Definition definition = PlatformGenerationRegistry.Definition.exactJson(source);
registry.put(generated, definition);
GenerationRegistryContract.GeneratedSource generatedSource = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.CUSTOM_BIOME_EFFECTIVE_SOURCE_SCHEMA,
semanticFingerprint,
semantic,
registry.generatedDefinitionRendererIdentity(),
GenerationRegistryContractFactory.fingerprintDefinition(
generated,
definition,
"generated"
),
source
);
GenerationRegistryContract required = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(generated, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(
generated,
generatedSource
)),
Map.of(generated, generatedSource)
);
GenerationRegistryContract available = GenerationRegistryContractFactory.captureRequiredDefinitions(
Set.of(required),
registry
);
required.requireDefinitionsAvailableIn(available);
assertEquals(
source,
GenerationRegistryContractFactory.requireGeneratedSource(
required,
generated,
firstFixer,
registry
)
);
registry.put(generated, PlatformGenerationRegistry.Definition.exactJson(
"{\"effects\":{},\"temperature\":0.7}"
));
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), registry)
);
registry.remove(generated);
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.captureRequiredDefinitions(Set.of(required), registry)
);
FakeGenerationRegistry changedRenderer = new FakeGenerationRegistry("runtime-b") {
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v2";
}
};
IDataFixer secondFixer = new TaggedBiomeFixer("second");
String rerendered = GenerationRegistryContractFactory.requireGeneratedSource(
required,
generated,
secondFixer,
changedRenderer
);
assertTrue(rerendered.contains("second"));
assertNotEquals(source, rerendered);
changedRenderer.put(generated, PlatformGenerationRegistry.Definition.exactJson(rerendered));
GenerationRegistryContract availableAfterUpgrade = GenerationRegistryContractFactory
.captureRequiredDefinitions(Set.of(required), secondFixer, changedRenderer);
required.requireDefinitionsAvailableIn(availableAfterUpgrade);
}
@Test
public void rerendersDimensionTypeSemanticsForANewRenderer() throws Exception {
assertDimensionRerender(new IrisDimension().getDimensionType());
}
@Test
public void dimensionRerenderPreservesNullableAndNondefaultOptions() throws Exception {
IrisDimensionType original = new IrisDimension().getDimensionType();
IrisDimensionTypeOptions options = new IrisDimensionTypeOptions()
.coordinateScale(8.0)
.ambientLight(0.375F)
.fixedTime(null)
.cloudHeight(null)
.monsterSpawnBlockLightLimit(7)
.ultrawarm(IrisDimensionTypeOptions.TriState.TRUE)
.natural(IrisDimensionTypeOptions.TriState.FALSE)
.ceiling(IrisDimensionTypeOptions.TriState.TRUE);
assertDimensionRerender(new IrisDimensionType(original.base(), options, 128, 256, -64));
options.fixedTime(6000L).cloudHeight(-256);
assertDimensionRerender(new IrisDimensionType(original.base(), options, 128, 256, -64));
}
private void assertDimensionRerender(IrisDimensionType dimensionType) throws Exception {
GenerationRegistryContract.PhysicalResourceKey key = key(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
"iris:overworld_type"
);
IDataFixer firstFixer = new TaggedBiomeFixer("first");
String semantic = GenerationEpochContractFactory.dimensionTypeSemanticJson(
dimensionType
);
String source = dimensionType.toJson(firstFixer);
PlatformGenerationRegistry.Definition definition = PlatformGenerationRegistry.Definition.exactJson(source);
GenerationRegistryContract.GeneratedSource generatedSource = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_EFFECTIVE_SOURCE_SCHEMA,
GenerationEpochContractFactory.fingerprintDimensionType(dimensionType),
semantic,
"test-renderer-v1",
GenerationRegistryContractFactory.fingerprintDefinition(key, definition, "generated"),
source
);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, generatedSource)),
Map.of(key, generatedSource)
);
FakeGenerationRegistry upgraded = new FakeGenerationRegistry("runtime-b") {
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v2";
}
};
String rerendered = GenerationRegistryContractFactory.requireGeneratedSource(
contract,
key,
new TaggedBiomeFixer("second"),
upgraded
);
assertTrue(rerendered.contains("second"));
assertNotEquals(source, rerendered);
JsonObject expected = JsonParser.parseString(source).getAsJsonObject();
expected.addProperty("serializer", "second");
assertEquals(expected, JsonParser.parseString(rerendered));
}
@Test
public void rejectsAChangedPackBeforeCapture() throws Exception {
File pack = temporary.newFolder("changed-pack");
String original = GenerationPackFingerprint.compute(
pack.toPath(),
GenerationPackFingerprint.CURRENT_VERSION
);
Files.writeString(pack.toPath().resolve("dimension.json"), "{}", StandardCharsets.UTF_8);
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(pack);
assertThrows(
IOException.class,
() -> GenerationRegistryContractFactory.create(
data,
new IrisDimension(),
original,
DataVersion.getLatest().get(),
mock(PlatformRegistries.class),
new FakeGenerationRegistry("runtime")
)
);
}
@Test
public void rejectsAnUnloadableAdditionalDimensionBeforeCapturingItsRegistry() throws Exception {
File pack = temporary.newFolder("missing-secondary-dimension");
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(pack);
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
ResourceLoader<IrisDimension> loader = stubDimensions(data, dimension);
when(loader.getPossibleKeys()).thenReturn(new String[]{"overworld", "upper"});
String fingerprint = GenerationPackFingerprint.compute(pack.toPath(), GenerationPackFingerprint.CURRENT_VERSION);
IOException exception = assertThrows(IOException.class, () -> GenerationRegistryContractFactory.create(
data, dimension, fingerprint, DataVersion.getLatest().get(),
mock(PlatformRegistries.class), new FakeGenerationRegistry("runtime")));
assertTrue(exception.getMessage().contains("Unable to load Iris dimension 'upper'"));
}
@SuppressWarnings("unchecked")
private static ResourceLoader<IrisDimension> stubDimensions(IrisData data, IrisDimension dimension) {
ResourceLoader<IrisDimension> loader = mock(ResourceLoader.class);
String dimensionKey = dimension.getLoadKey();
when(data.getDimensionLoader()).thenReturn(loader);
when(loader.getPossibleKeys()).thenReturn(new String[]{dimensionKey});
return loader;
}
private static GenerationRegistryContract contract(
FakeGenerationRegistry registry,
GenerationRegistryContract.PhysicalResourceKey... keys
) {
Map<GenerationRegistryContract.PhysicalResourceKey, String> definitions = new LinkedHashMap<>();
for (GenerationRegistryContract.PhysicalResourceKey key : keys) {
PlatformGenerationRegistry.Definition definition = registry.definition(
key.registryKey(),
key.resourceKey()
);
definitions.put(
key,
GenerationRegistryContractFactory.fingerprintDefinition(
key,
definition,
registry.runtimeIdentity()
)
);
}
return GenerationRegistryContract.fromDefinitions(definitions);
}
private static GenerationRegistryContract.PhysicalResourceKey key(String registry, String resource) {
return new GenerationRegistryContract.PhysicalResourceKey(registry, resource);
}
private record TaggedBiomeFixer(String tag) implements IDataFixer {
@Override
public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) {
return json.put("renderer", tag);
}
@Override
public JSONObject resolve(Dimension dimension, IrisDimensionTypeOptions options) {
return new JSONObject().put("serializer", tag).put("options",
new JSONObject(new GsonBuilder().serializeNulls().create().toJson(options)));
}
@Override
public void fixDimension(Dimension dimension, JSONObject json) {
}
}
private static class FakeGenerationRegistry implements PlatformGenerationRegistry {
private final String runtimeIdentity;
private final Map<GenerationRegistryContract.PhysicalResourceKey, Definition> definitions =
new LinkedHashMap<>();
private FakeGenerationRegistry(String runtimeIdentity) {
this.runtimeIdentity = runtimeIdentity;
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return "test-renderer-v1";
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return "test:biomes/" + identitySha256;
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return List.of("test:" + dimensionKey + "/" + customBiomeId);
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "test:" + dimensionKey + "_type";
}
@Override
public Definition definition(String registryKey, String resourceKey) {
GenerationRegistryContract.PhysicalResourceKey key = key(registryKey, resourceKey);
Definition definition = definitions.get(key);
if (definition == null && !definitions.containsKey(key)) {
definition = Definition.resourceIdentity(registryKey, resourceKey);
definitions.put(key, definition);
}
return definition;
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
return Definition.exactJson(sourceJson);
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
Definition definition = definitions.get(key(registryKey, resourceKey));
if (definition == null || definition.encoding() != Encoding.JSON) {
throw new IllegalStateException("Missing generated definition " + resourceKey);
}
return definition;
}
private void put(GenerationRegistryContract.PhysicalResourceKey key, Definition definition) {
definitions.put(key, definition);
}
private void remove(GenerationRegistryContract.PhysicalResourceKey key) {
definitions.put(key, null);
}
}
}
@@ -0,0 +1,229 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
public class GenerationRegistryContractTest {
private static final String DEFINITION_A =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String DEFINITION_B =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@Test
public void canonicalizesPhysicalResourceKeysAndRetainsCurrentFingerprint() {
GenerationRegistryContract.PhysicalResourceKey plains = key(
"minecraft:worldgen/biome",
"minecraft:plains"
);
GenerationRegistryContract.PhysicalResourceKey desert = key(
"minecraft:worldgen/biome",
"minecraft:desert"
);
LinkedHashMap<GenerationRegistryContract.PhysicalResourceKey, String> reverseOrder = new LinkedHashMap<>();
reverseOrder.put(plains, DEFINITION_A);
reverseOrder.put(desert, DEFINITION_B);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitions(
GenerationRegistryContract.FINGERPRINT_SCHEMA_VERSION_ONE,
reverseOrder
);
assertEquals(
"b9165819aa1cdf4e7024a85ca1b63801c3cfaaefb1898de7fd83b0adf366b16e",
contract.fingerprint()
);
assertEquals(desert, contract.definitions().firstKey());
assertEquals(plains, contract.definitions().lastKey());
assertThrows(
UnsupportedOperationException.class,
() -> contract.definitions().put(key("minecraft:block", "minecraft:stone"), DEFINITION_A)
);
}
@Test
public void derivesStableFingerprintIndependentOfMapIterationOrder() {
GenerationRegistryContract.PhysicalResourceKey plains = key(
"minecraft:worldgen/biome",
"minecraft:plains"
);
GenerationRegistryContract.PhysicalResourceKey desert = key(
"minecraft:worldgen/biome",
"minecraft:desert"
);
LinkedHashMap<GenerationRegistryContract.PhysicalResourceKey, String> firstOrder = new LinkedHashMap<>();
firstOrder.put(plains, DEFINITION_A);
firstOrder.put(desert, DEFINITION_B);
LinkedHashMap<GenerationRegistryContract.PhysicalResourceKey, String> secondOrder = new LinkedHashMap<>();
secondOrder.put(desert, DEFINITION_B);
secondOrder.put(plains, DEFINITION_A);
GenerationRegistryContract first = GenerationRegistryContract.fromDefinitions(firstOrder);
GenerationRegistryContract second = GenerationRegistryContract.fromDefinitions(secondOrder);
assertEquals(first, second);
assertEquals(first.fingerprint(), second.fingerprint());
}
@Test
public void definitionOrPhysicalKeyMutationChangesFingerprint() {
GenerationRegistryContract.PhysicalResourceKey plains = key(
"minecraft:worldgen/biome",
"minecraft:plains"
);
GenerationRegistryContract.PhysicalResourceKey forest = key(
"minecraft:worldgen/biome",
"minecraft:forest"
);
GenerationRegistryContract original = GenerationRegistryContract.fromDefinitions(Map.of(plains, DEFINITION_A));
GenerationRegistryContract changedDefinition = GenerationRegistryContract.fromDefinitions(
Map.of(plains, DEFINITION_B)
);
GenerationRegistryContract changedKey = GenerationRegistryContract.fromDefinitions(Map.of(forest, DEFINITION_A));
assertNotEquals(original.fingerprint(), changedDefinition.fingerprint());
assertNotEquals(original.fingerprint(), changedKey.fingerprint());
}
@Test
public void roundTripsStrictDeterministicJson() {
GenerationRegistryContract.PhysicalResourceKey generatedBiome = key(
"minecraft:worldgen/biome",
"iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(Map.of(
key("minecraft:worldgen/biome", "minecraft:plains"),
DEFINITION_A,
key("minecraft:dimension_type", "minecraft:overworld"),
DEFINITION_B,
generatedBiome,
DEFINITION_A
), Map.of(generatedBiome, generatedSource("{\"temperature\":0.5}")));
JsonObject json = contract.toJson();
assertEquals(contract, GenerationRegistryContract.fromJson(json.deepCopy()));
assertEquals(json.toString(), contract.toJson().toString());
JsonObject unknownField = json.deepCopy();
unknownField.addProperty("unexpected", true);
assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(unknownField));
JsonObject missingDefinitions = json.deepCopy();
missingDefinitions.remove("definitions");
assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(missingDefinitions));
JsonObject missingGeneratedSources = json.deepCopy();
missingGeneratedSources.remove("generatedSources");
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromJson(missingGeneratedSources)
);
}
@Test
public void generatedSourcesAreDefinitionBoundedAndFingerprintProtected() {
GenerationRegistryContract.PhysicalResourceKey generatedBiome = key(
"minecraft:worldgen/biome",
"iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(generatedBiome, DEFINITION_A),
Map.of(generatedBiome, generatedSource("{\"temperature\":0.5}"))
);
JsonObject changed = contract.toJson();
changed.getAsJsonArray("generatedSources")
.get(0)
.getAsJsonObject()
.addProperty("sourceJson", "{\"temperature\":0.7}");
assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(changed));
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(),
Map.of(generatedBiome, generatedSource("{}"))
)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(generatedBiome, DEFINITION_A),
Map.of(generatedBiome, generatedSource("[]"))
)
);
String oversized = "{\"value\":\"" + "a".repeat(1024 * 1024) + "\"}";
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(generatedBiome, DEFINITION_A),
Map.of(generatedBiome, generatedSource(oversized))
)
);
}
@Test
public void refusesUnsortedPersistedDefinitionsAndForgedFingerprint() {
LinkedHashMap<GenerationRegistryContract.PhysicalResourceKey, String> definitions = new LinkedHashMap<>();
definitions.put(key("minecraft:worldgen/biome", "minecraft:desert"), DEFINITION_B);
definitions.put(key("minecraft:worldgen/biome", "minecraft:plains"), DEFINITION_A);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitions(definitions);
JsonObject unsorted = contract.toJson();
JsonArray definitionArray = unsorted.getAsJsonArray("definitions");
JsonElement first = definitionArray.get(0).deepCopy();
JsonElement second = definitionArray.get(1).deepCopy();
definitionArray.set(0, second);
definitionArray.set(1, first);
assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(unsorted));
JsonObject forged = contract.toJson();
forged.addProperty("fingerprint", DEFINITION_A);
assertThrows(IllegalArgumentException.class, () -> GenerationRegistryContract.fromJson(forged));
}
@Test
public void refusesNonCanonicalKeysHashesAndUnknownSchemas() {
assertThrows(
IllegalArgumentException.class,
() -> key("Minecraft:worldgen/biome", "minecraft:plains")
);
assertThrows(
IllegalArgumentException.class,
() -> key("minecraft:worldgen/biome", "plains")
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromDefinitions(Map.of(
key("minecraft:worldgen/biome", "minecraft:plains"),
"not-a-sha256"
))
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationRegistryContract.fromDefinitions(2, Map.of())
);
}
private static GenerationRegistryContract.PhysicalResourceKey key(String registryKey, String resourceKey) {
return new GenerationRegistryContract.PhysicalResourceKey(registryKey, resourceKey);
}
private static GenerationRegistryContract.GeneratedSource generatedSource(String sourceJson) {
return new GenerationRegistryContract.GeneratedSource(
"test-source-v1",
DEFINITION_A,
"{\"authored\":true}",
"test-renderer-v1",
DEFINITION_B,
sourceJson
);
}
}
@@ -0,0 +1,730 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.zip.CRC32;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class GenerationSemanticIndexTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void roundTripPreservesAllGeneratedTruthAndQueriesWithoutDiskAccess() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("dimension").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics semantics = completeSemantics(-33, -1, 7L);
assertTrue(index.recordAndPersist(semantics));
assertFalse(index.recordAndPersist(semantics));
Path expectedDirectory = dimensionRoot.resolve("iris/generation/semantics");
assertEquals(expectedDirectory, index.storageDirectory());
Path shard = onlyShard(expectedDirectory);
assertTrue(shard.getFileName().toString().matches("r\\.-2\\.-1\\.[0-9a-f]{64}\\.isem"));
GenerationSemanticIndex loaded = GenerationSemanticIndex.load(dimensionRoot);
assertEquals(Optional.of(semantics), loaded.get(-33, -1));
assertEquals(1, loaded.recordCount());
Files.delete(shard);
assertEquals(-33, requiredMatch(loaded, GenerationSemanticIndex.SemanticKind.SURFACE_BIOME, "iris:forest").chunk().chunkX());
assertEquals(-33, requiredMatch(loaded, GenerationSemanticIndex.SemanticKind.CAVE_BIOME, "iris:limestone_caves").chunk().chunkX());
assertEquals(-33, requiredMatch(loaded, GenerationSemanticIndex.SemanticKind.REGION, "temperate").chunk().chunkX());
assertEquals(-33, requiredMatch(loaded, GenerationSemanticIndex.SemanticKind.RIVER_PROFILE, "rivers/default").chunk().chunkX());
assertEquals(-33, requiredMatch(loaded, GenerationSemanticIndex.SemanticKind.OBJECT, "iris:oak").chunk().chunkX());
GenerationSemanticIndex.Match structure = requiredMatch(
loaded,
GenerationSemanticIndex.SemanticKind.STRUCTURE,
"minecraft:village_plains"
);
assertEquals(new ChunkGenerationSemantics.BlockPosition(-520, 72, -7), structure.exactPosition().orElseThrow());
}
@Test
public void stagedFactsMergeMonotonicallyAndSealDurably() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("staged").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics noiseStage = ChunkGenerationSemantics.builder(4, 5, 3L)
.addSurfaceBiome("iris:forest")
.build();
ChunkGenerationSemantics biomeStage = ChunkGenerationSemantics.builder(4, 5, 3L)
.addRegion("temperate")
.addRiverProfile("rivers/new")
.build();
ChunkGenerationSemantics fullStage = ChunkGenerationSemantics.builder(4, 5, 3L)
.addObject("iris:oak")
.addStructure("iris:tower", 72, 90, 88)
.seal()
.build();
assertTrue(index.recordAndPersist(noiseStage));
assertFalse(index.recordAndPersist(noiseStage));
assertTrue(index.recordAndPersist(biomeStage));
assertTrue(index.recordAndPersist(fullStage));
assertFalse(index.recordAndPersist(noiseStage));
ChunkGenerationSemantics expected = ChunkGenerationSemantics.builder(4, 5, 3L)
.addSurfaceBiome("iris:forest")
.addRegion("temperate")
.addRiverProfile("rivers/new")
.addObject("iris:oak")
.addStructure("iris:tower", 72, 90, 88)
.seal()
.build();
assertEquals(Optional.of(expected), index.get(4, 5));
assertEquals(1, index.recordCount());
assertEquals(Optional.of(expected), GenerationSemanticIndex.load(dimensionRoot).get(4, 5));
}
@Test
public void conflictingActivationsAndFactsAfterSealAreRejectedWithoutDiskChanges() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("sealed").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics sealed = ChunkGenerationSemantics.builder(4, 5, 3L)
.addSurfaceBiome("iris:forest")
.seal()
.build();
assertTrue(index.recordAndPersist(sealed));
Path shard = onlyShard(index.storageDirectory());
byte[] persisted = Files.readAllBytes(shard);
ChunkGenerationSemantics additionalFact = ChunkGenerationSemantics.builder(4, 5, 3L)
.addSurfaceBiome("iris:desert")
.build();
ChunkGenerationSemantics differentActivation = ChunkGenerationSemantics.builder(4, 5, 4L)
.addSurfaceBiome("iris:forest")
.build();
assertThrows(IllegalStateException.class, () -> index.recordAndPersist(additionalFact));
assertThrows(IllegalStateException.class, () -> index.recordAndPersist(differentActivation));
assertArrayEquals(persisted, Files.readAllBytes(shard));
assertEquals(Optional.of(sealed), index.get(4, 5));
assertEquals(Optional.of(sealed), GenerationSemanticIndex.load(dimensionRoot).get(4, 5));
}
@Test
public void nearestChunkUsesDistanceStableTiesRadiusAndActivationFilters() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("nearest-chunks").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
index.recordAndPersist(ChunkGenerationSemantics.builder(-1, 0, 1L).addRiverProfile("rivers/new").build());
index.recordAndPersist(ChunkGenerationSemantics.builder(1, 0, 2L).addRiverProfile("rivers/new").build());
ChunkGenerationSemantics.BlockPosition origin = new ChunkGenerationSemantics.BlockPosition(0, 80, 0);
GenerationSemanticIndex.Match tied = index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.RIVER_PROFILE,
"rivers/new",
origin,
2
)).orElseThrow();
assertEquals(new GenerationSemanticIndex.ChunkReference(-1, 0, 1L), tied.chunk());
GenerationSemanticIndex.Match filtered = index.findNearest(GenerationSemanticIndex.Query.forActivation(
GenerationSemanticIndex.SemanticKind.RIVER_PROFILE,
"rivers/new",
origin,
2,
2L
)).orElseThrow();
assertEquals(new GenerationSemanticIndex.ChunkReference(1, 0, 2L), filtered.chunk());
assertTrue(index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.RIVER_PROFILE,
"rivers/new",
origin,
0
)).isEmpty());
assertTrue(index.findNearest(GenerationSemanticIndex.Query.forActivation(
GenerationSemanticIndex.SemanticKind.RIVER_PROFILE,
"rivers/new",
origin,
2,
99L
)).isEmpty());
}
@Test
public void nearestStructureUsesExactHorizontalDistanceAndStablePositionTies() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("nearest-structures").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
index.recordAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L)
.addStructure("iris:tower", -10, 90, 0)
.addStructure("iris:tower", 10, 70, 0)
.build());
index.recordAndPersist(ChunkGenerationSemantics.builder(2, 0, 2L)
.addStructure("iris:tower", 32, 64, 0)
.build());
ChunkGenerationSemantics.BlockPosition origin = new ChunkGenerationSemantics.BlockPosition(0, 0, 0);
GenerationSemanticIndex.Match tied = index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.STRUCTURE,
"iris:tower",
origin,
4
)).orElseThrow();
assertEquals(new ChunkGenerationSemantics.BlockPosition(-10, 90, 0), tied.exactPosition().orElseThrow());
GenerationSemanticIndex.Match filtered = index.findNearest(GenerationSemanticIndex.Query.forActivation(
GenerationSemanticIndex.SemanticKind.STRUCTURE,
"iris:tower",
origin,
4,
2L
)).orElseThrow();
assertEquals(new ChunkGenerationSemantics.BlockPosition(32, 64, 0), filtered.exactPosition().orElseThrow());
}
@Test
public void shardBytesAreDeterministicAcrossRecordInsertionOrder() throws Exception {
Path firstRoot = temporaryFolder.newFolder("deterministic-first").toPath();
Path secondRoot = temporaryFolder.newFolder("deterministic-second").toPath();
List<ChunkGenerationSemantics> semantics = List.of(
completeSemantics(0, 0, 1L),
completeSemantics(4, 7, 2L),
completeSemantics(31, 31, 3L)
);
GenerationSemanticIndex first = GenerationSemanticIndex.load(firstRoot);
GenerationSemanticIndex second = GenerationSemanticIndex.load(secondRoot);
for (ChunkGenerationSemantics record : semantics) {
first.recordAndPersist(record);
}
for (int index = semantics.size() - 1; index >= 0; index--) {
second.recordAndPersist(semantics.get(index));
}
assertArrayEquals(
Files.readAllBytes(onlyShard(first.storageDirectory())),
Files.readAllBytes(onlyShard(second.storageDirectory()))
);
assertArrayEquals(
Files.readAllBytes(first.storageDirectory().resolve("index.isix")),
Files.readAllBytes(second.storageDirectory().resolve("index.isix"))
);
}
@Test
public void failedPublicationLeavesMemoryAndPersistedStateUnchanged() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("publication-failure").toPath();
GenerationSemanticIndex initial = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics baseline = ChunkGenerationSemantics.builder(2, 3, 8L)
.addSurfaceBiome("iris:forest")
.build();
initial.recordAndPersist(baseline);
byte[] persisted = Files.readAllBytes(onlyShard(initial.storageDirectory()));
GenerationSemanticIndex failing = GenerationSemanticIndex.load(
dimensionRoot,
(directory, regionX, regionZ, encoded) -> {
throw new IOException("simulated publication failure");
}
);
ChunkGenerationSemantics update = ChunkGenerationSemantics.builder(2, 3, 8L)
.addRegion("temperate")
.build();
IOException error = assertThrows(IOException.class, () -> failing.recordAndPersist(update));
assertEquals("simulated publication failure", error.getMessage());
assertEquals(Optional.of(baseline), failing.get(2, 3));
assertArrayEquals(persisted, Files.readAllBytes(onlyShard(initial.storageDirectory())));
assertEquals(Optional.of(baseline), GenerationSemanticIndex.load(dimensionRoot).get(2, 3));
}
@Test
public void failedCatalogPublicationLeavesAnUnreferencedShardAndReopensPreviousTruth() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("catalog-publication-failure").toPath();
GenerationSemanticIndex initial = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics baseline = ChunkGenerationSemantics.builder(2, 3, 8L)
.addSurfaceBiome("iris:forest")
.build();
initial.recordAndPersist(baseline);
GenerationSemanticIndex failing = GenerationSemanticIndex.loadWithCatalogPublisher(
dimensionRoot,
(directory, regionKeys) -> {
throw new IOException("simulated catalog failure");
}
);
ChunkGenerationSemantics update = ChunkGenerationSemantics.builder(32, 3, 9L)
.addRegion("temperate")
.build();
IOException error = assertThrows(IOException.class, () -> failing.recordAndPersist(update));
assertEquals("simulated catalog failure", error.getMessage());
assertEquals(Optional.of(baseline), failing.get(2, 3));
assertTrue(failing.get(32, 3).isEmpty());
assertEquals(Optional.of(baseline), GenerationSemanticIndex.load(dimensionRoot).get(2, 3));
assertTrue(GenerationSemanticIndex.load(dimensionRoot).get(32, 3).isEmpty());
assertEquals(2, shardCount(initial.storageDirectory()));
}
@Test
public void snapshotIsImmutableAndUsesStableChunkOrder() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("snapshot").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics last = completeSemantics(7, -3, 1L);
ChunkGenerationSemantics first = completeSemantics(-2, 9, 2L);
index.recordAndPersist(last);
index.recordAndPersist(first);
List<ChunkGenerationSemantics> snapshot = index.recordsSnapshot();
assertEquals(List.of(first, last), snapshot);
assertThrows(UnsupportedOperationException.class, () -> snapshot.clear());
}
@Test
public void checksumCorruptionAndTruncationFailClosed() throws Exception {
Path checksumRoot = persistedRoot("corrupt-checksum");
Path checksumShard = onlyShard(checksumRoot.resolve("iris/generation/semantics"));
byte[] corrupted = Files.readAllBytes(checksumShard);
corrupted[20] ^= 0x10;
replaceReferencedShard(checksumRoot, checksumShard, corrupted);
IOException checksumError = assertThrows(IOException.class, () -> GenerationSemanticIndex.load(checksumRoot));
assertTrue(checksumError.getMessage().contains("checksum mismatch"));
Path truncatedRoot = persistedRoot("corrupt-truncated");
Path truncatedShard = onlyShard(truncatedRoot.resolve("iris/generation/semantics"));
byte[] complete = Files.readAllBytes(truncatedShard);
replaceReferencedShard(truncatedRoot, truncatedShard, Arrays.copyOf(complete, complete.length - 5));
IOException truncatedError = assertThrows(IOException.class, () -> GenerationSemanticIndex.load(truncatedRoot));
assertTrue(truncatedError.getMessage().contains("Invalid generation semantic shard"));
}
@Test
public void catalogDetectsMissingShardsAndFailsClosedWhenItIsMissingOrCorrupt() throws Exception {
Path missingShardRoot = persistedRoot("missing-shard");
Files.delete(onlyShard(missingShardRoot.resolve("iris/generation/semantics")));
IOException missingShard = assertThrows(
IOException.class,
() -> GenerationSemanticIndex.load(missingShardRoot)
);
assertTrue(missingShard.getMessage().contains("catalog references a missing shard"));
Path missingPointerRoot = persistedRoot("missing-pointer");
Files.delete(missingPointerRoot.resolve("iris/generation/semantics/r.0.0.isix"));
IOException missingPointer = assertThrows(
IOException.class,
() -> GenerationSemanticIndex.load(missingPointerRoot)
);
assertTrue(missingPointer.getMessage().contains("catalog references a missing shard pointer"));
Path missingCatalogRoot = persistedRoot("missing-catalog");
Files.delete(missingCatalogRoot.resolve("iris/generation/semantics/index.isix"));
IOException missingCatalog = assertThrows(
IOException.class,
() -> GenerationSemanticIndex.load(missingCatalogRoot)
);
assertTrue(missingCatalog.getMessage().contains("shard catalog is missing"));
Path corruptCatalogRoot = persistedRoot("corrupt-catalog");
Path catalog = corruptCatalogRoot.resolve("iris/generation/semantics/index.isix");
byte[] corrupt = Files.readAllBytes(catalog);
corrupt[8] ^= 0x01;
Files.write(catalog, corrupt);
IOException corruptCatalog = assertThrows(
IOException.class,
() -> GenerationSemanticIndex.load(corruptCatalogRoot)
);
assertTrue(corruptCatalog.getMessage().contains("semantic catalog"));
assertTrue(corruptCatalog.getMessage().contains("checksum mismatch"));
Path corruptPointerRoot = persistedRoot("corrupt-pointer");
Path pointer = corruptPointerRoot.resolve("iris/generation/semantics/r.0.0.isix");
byte[] corruptPointer = Files.readAllBytes(pointer);
corruptPointer[16] ^= 0x01;
Files.write(pointer, corruptPointer);
IOException pointerError = assertThrows(
IOException.class,
() -> GenerationSemanticIndex.load(corruptPointerRoot)
);
assertTrue(pointerError.getMessage().contains("semantic shard pointer"));
assertTrue(pointerError.getMessage().contains("checksum mismatch"));
}
@Test
public void unknownSchemaFailsClosedEvenWithAValidChecksum() throws Exception {
Path dimensionRoot = persistedRoot("unknown-schema");
Path shard = onlyShard(dimensionRoot.resolve("iris/generation/semantics"));
byte[] encoded = Files.readAllBytes(shard);
encoded[4] = 0;
encoded[5] = 6;
rewriteChecksum(encoded);
replaceReferencedShard(dimensionRoot, shard, encoded);
IOException error = assertThrows(IOException.class, () -> GenerationSemanticIndex.load(dimensionRoot));
assertTrue(error.getMessage().contains("unsupported format version 6"));
}
@Test
public void sealedClaimJournalReplaysIntoKeyedAndRiverPostings() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("journal-postings").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
ChunkGenerationSemantics claim = ChunkGenerationSemantics.builder(64, -32, 7L)
.addRegion("iris:temperate")
.addRiverFeature(
"iris:rivers/default",
HydrologyFeatureType.RIFFLE,
17L,
1031,
71,
-503
)
.seal()
.build();
assertTrue(index.claimAndPersist(claim));
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(dimensionRoot);
GenerationSemanticIndex.Match region = loaded.findNearest(
GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:temperate",
new ChunkGenerationSemantics.BlockPosition(1024, 0, -512),
2
)
).orElseThrow();
assertEquals(new GenerationSemanticIndex.ChunkReference(64, -32, 7L), region.chunk());
GenerationSemanticIndex.RiverMatch river = loaded.findNearestRiver(
GenerationSemanticIndex.RiverQuery.acrossActivations(
java.util.Set.of(HydrologyFeatureType.RIFFLE),
"iris:rivers/default",
new ChunkGenerationSemantics.BlockPosition(1024, 0, -512),
2
),
ChunkGenerationSemantics::sealed
).orElseThrow();
assertEquals(new ChunkGenerationSemantics.BlockPosition(1031, 71, -503), river.occurrence().position());
assertTrue(sealedClaims(loaded, 7L).contains(ChunkGenerationOwnership.packChunk(64, -32)));
}
@Test
public void journalReplayTruncatesOnlyATornTerminalEntry() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("journal-torn-tail").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
index.claimAndPersist(sealedRegion(0, 0, 1L, "iris:first"));
index.claimAndPersist(sealedRegion(1, 0, 1L, "iris:second"));
Path journal = onlyJournal(index.storageDirectory());
byte[] bytes = Files.readAllBytes(journal);
Files.write(journal, Arrays.copyOf(bytes, bytes.length - 3));
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(dimensionRoot);
assertEquals(Optional.of(sealedRegion(0, 0, 1L, "iris:first")), loaded.get(0, 0));
assertTrue(loaded.get(1, 0).isEmpty());
assertTrue(Files.size(journal) < bytes.length);
}
@Test
public void journalReplayRejectsInteriorCorruption() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("journal-corrupt-interior").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
index.claimAndPersist(sealedRegion(0, 0, 1L, "iris:first"));
index.claimAndPersist(sealedRegion(1, 0, 1L, "iris:second"));
Path journal = onlyJournal(index.storageDirectory());
byte[] bytes = Files.readAllBytes(journal);
bytes[16] ^= 0x20;
Files.write(journal, bytes);
IOException error = assertThrows(IOException.class, () -> GenerationSemanticIndex.loadRequired(dimensionRoot));
assertTrue(error.getMessage().contains("interior entry"));
}
@Test
public void duplicateJournalEntriesReplayIdempotently() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("journal-idempotent").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
ChunkGenerationSemantics claim = sealedRegion(0, 0, 1L, "iris:temperate");
index.claimAndPersist(claim);
Path journal = onlyJournal(index.storageDirectory());
byte[] entry = Files.readAllBytes(journal);
Files.write(journal, entry, StandardOpenOption.APPEND);
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(dimensionRoot);
assertEquals(1, loaded.recordCount());
assertEquals(Optional.of(claim), loaded.get(0, 0));
assertEquals(1, sealedClaims(loaded, 1L).size());
}
@Test
public void boundedCacheEvictsAndReloadsUncompactedClaimsWithoutLosingTruth() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("journal-cache-eviction").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
for (int regionX = 0; regionX < 270; regionX++) {
index.claimAndPersist(sealedRegion(regionX << 5, 0, 1L, "iris:temperate"));
if (regionX < 10) {
index.claimAndPersist(sealedRegion((regionX << 5) + 1, 0, 1L, "iris:temperate"));
}
assertTrue(index.cachedRegionCount() <= 64);
assertTrue(index.cachedSummaryCount() <= 256);
}
assertEquals(Optional.of(sealedRegion(0, 0, 1L, "iris:temperate")), index.get(0, 0));
assertTrue(index.cachedSummaryCount() <= 256);
assertTrue(index.cachedRegionCount() <= 64);
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(dimensionRoot);
assertEquals(280, loaded.recordCount());
assertTrue(loaded.cachedSummaryCount() <= 256);
assertEquals(280, sealedClaims(loaded, 1L).size());
assertEquals(Optional.of(sealedRegion(0, 0, 1L, "iris:temperate")), loaded.get(0, 0));
}
@Test
public void keyedNearestLoadsOnlyRegionsInThatPosting() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("keyed-nearest-lazy").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.initialize(dimensionRoot);
for (int regionX = 0; regionX < 70; regionX++) {
String region = regionX == 0 ? "iris:target" : "iris:unrelated";
index.claimAndPersist(sealedRegion(regionX << 5, 0, 1L, region));
}
index.compactJournals();
GenerationSemanticIndex loaded = GenerationSemanticIndex.loadRequired(dimensionRoot);
long before = loaded.regionDecodeCount();
assertEquals(0L, before);
assertEquals(0, loaded.cachedRegionCount());
GenerationSemanticIndex.Match match = loaded.findNearest(
GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:target",
new ChunkGenerationSemantics.BlockPosition(8, 0, 8),
5000
)
).orElseThrow();
assertEquals(0, match.chunk().chunkX());
assertEquals(before + 1L, loaded.regionDecodeCount());
assertTrue(loaded.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:missing",
new ChunkGenerationSemantics.BlockPosition(8, 0, 8),
5000
)).isEmpty());
assertEquals(before + 1L, loaded.regionDecodeCount());
}
@Test
public void lazilyDecodedUnknownRecordFlagsFailClosedEvenWithAValidChecksum() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("unknown-record-flags").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
index.recordAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).seal().build());
Path shard = onlyShard(index.storageDirectory());
byte[] encoded = Files.readAllBytes(shard);
encoded[firstRecordFlagsOffset(encoded)] = 2;
rewriteChecksum(encoded);
replaceReferencedShard(dimensionRoot, shard, encoded);
GenerationSemanticIndex loaded = GenerationSemanticIndex.load(dimensionRoot);
IllegalStateException error = assertThrows(IllegalStateException.class, () -> loaded.get(0, 0));
assertTrue(error.getCause().getMessage().contains("unsupported semantic record flags 2"));
}
@Test
public void symbolicLinkStorageAncestorsAreRejected() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("symlink-dimension").toPath();
Path external = temporaryFolder.newFolder("symlink-external").toPath();
Files.createDirectories(external.resolve("generation/semantics"));
Files.createSymbolicLink(dimensionRoot.resolve("iris"), external);
assertThrows(IOException.class, () -> GenerationSemanticIndex.load(dimensionRoot));
}
@Test
public void queriesValidateKindsKeysRadiiAndActivationFilters() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("invalid-query").toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
ChunkGenerationSemantics.BlockPosition origin = new ChunkGenerationSemantics.BlockPosition(0, 0, 0);
assertThrows(
IllegalArgumentException.class,
() -> GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
" Iris:Forest",
origin,
1
)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:forest",
origin,
-1
)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationSemanticIndex.Query.forActivation(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:forest",
origin,
1,
0L
)
);
assertTrue(index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
GenerationSemanticIndex.SemanticKind.REGION,
"iris:missing",
origin,
1
)).isEmpty());
}
private GenerationSemanticIndex.Match requiredMatch(
GenerationSemanticIndex index,
GenerationSemanticIndex.SemanticKind kind,
String key
) {
return index.findNearest(GenerationSemanticIndex.Query.acrossActivations(
kind,
key,
new ChunkGenerationSemantics.BlockPosition(-520, 80, -8),
2
)).orElseThrow();
}
private Path persistedRoot(String name) throws Exception {
Path dimensionRoot = temporaryFolder.newFolder(name).toPath();
GenerationSemanticIndex index = GenerationSemanticIndex.load(dimensionRoot);
index.recordAndPersist(completeSemantics(0, 0, 1L));
return dimensionRoot;
}
private static ChunkGenerationSemantics completeSemantics(int chunkX, int chunkZ, long activationId) {
return ChunkGenerationSemantics.builder(chunkX, chunkZ, activationId)
.addSurfaceBiome("iris:forest")
.addSurfaceBiome("iris:wetlands")
.addCaveBiome("iris:limestone_caves")
.addRegion("temperate")
.addRiverProfile("rivers/default")
.addRiverFeature(
"rivers/default",
HydrologyFeatureType.RIFFLE,
5L,
(chunkX << 4) + 7,
70,
(chunkZ << 4) + 6
)
.addObject("iris:oak")
.addStructure(
"minecraft:village_plains",
(chunkX << 4) + 8,
72,
(chunkZ << 4) + 9
)
.seal()
.build();
}
private static ChunkGenerationSemantics sealedRegion(
int chunkX,
int chunkZ,
long activationId,
String regionKey
) {
return ChunkGenerationSemantics.builder(chunkX, chunkZ, activationId)
.addRegion(regionKey)
.seal()
.build();
}
private static Path onlyShard(Path directory) throws Exception {
try (Stream<Path> files = Files.list(directory)) {
List<Path> shards = files.filter(path -> path.getFileName().toString().endsWith(".isem")).toList();
assertEquals(1, shards.size());
return shards.getFirst();
}
}
private static Path onlyJournal(Path directory) throws Exception {
try (Stream<Path> files = Files.list(directory)) {
List<Path> journals = files.filter(path -> path.getFileName().toString().endsWith(".iswal"))
.toList();
assertEquals(1, journals.size());
return journals.getFirst();
}
}
private static long shardCount(Path directory) throws Exception {
try (Stream<Path> files = Files.list(directory)) {
return files.filter(path -> path.getFileName().toString().endsWith(".isem")).count();
}
}
private static void replaceReferencedShard(Path dimensionRoot, Path previousShard, byte[] encoded) throws Exception {
int regionX = ByteBuffer.wrap(encoded, 8, Integer.BYTES).getInt();
int regionZ = ByteBuffer.wrap(encoded, 12, Integer.BYTES).getInt();
byte[] digest = MessageDigest.getInstance("SHA-256").digest(encoded);
String hash = HexFormat.of().formatHex(digest);
Path directory = dimensionRoot.resolve("iris/generation/semantics");
Path replacement = directory.resolve("r." + regionX + "." + regionZ + "." + hash + ".isem");
Files.write(replacement, encoded);
if (!replacement.equals(previousShard)) {
Files.delete(previousShard);
}
Path pointer = directory.resolve("r." + regionX + "." + regionZ + ".isix");
byte[] pointerBytes = Files.readAllBytes(pointer);
System.arraycopy(digest, 0, pointerBytes, 16, digest.length);
rewriteChecksum(pointerBytes);
Files.write(pointer, pointerBytes);
}
private static void rewriteChecksum(byte[] encoded) {
int bodyLength = encoded.length - Integer.BYTES;
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, bodyLength);
ByteBuffer.wrap(encoded, bodyLength, Integer.BYTES).putInt((int) checksum.getValue());
}
private static LongOpenHashSet sealedClaims(
GenerationSemanticIndex index,
long activationId
) throws IOException {
LongOpenHashSet claims = new LongOpenHashSet();
index.forEachSealedClaim(
activationId,
(chunkX, chunkZ) -> claims.add(ChunkGenerationOwnership.packChunk(chunkX, chunkZ))
);
return claims;
}
private static int firstRecordFlagsOffset(byte[] encoded) {
ByteBuffer input = ByteBuffer.wrap(encoded);
int paletteSize = input.getInt(20);
int summaryLength = input.getInt(24);
input.position(28 + summaryLength);
for (int index = 0; index < paletteSize; index++) {
int keyLength = Short.toUnsignedInt(input.getShort());
input.position(input.position() + keyLength);
}
return input.position() + Short.BYTES + Long.BYTES;
}
}
@@ -0,0 +1,253 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.IrisEngine;
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 art.arcane.iris.engine.object.IrisDimension;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
public final class GenerationSemanticQueriesTest {
@Test
public void mergesExactHistoricalStructureStartsWithEligibleActiveCandidates() {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
when(complex.allowsNewGenerationChunk(1, 0)).thenReturn(false);
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
when(history.isActiveUnowned(0, 0)).thenReturn(false);
ChunkGenerationSemantics sealedEmpty = ChunkGenerationSemantics.builder(2, 0, 2L).seal().build();
when(history.semantics(2, 0)).thenReturn(Optional.of(sealedEmpty));
GenerationSemanticIndex.Match recorded = new GenerationSemanticIndex.Match(
GenerationSemanticIndex.SemanticKind.STRUCTURE,
"old-tower",
new GenerationSemanticIndex.ChunkReference(6, 0, 1L),
Optional.of(new ChunkGenerationSemantics.BlockPosition(100, 73, 5)));
when(history.findRecorded(any())).thenReturn(Optional.of(recorded));
IrisStructureLocator.LocateResult nearer = foundStructure(48, 90, 0);
try (MockedStatic<IrisStructureLocator> structures = mockStatic(IrisStructureLocator.class)) {
structures.when(() -> IrisStructureLocator.locate(
eq(engine), eq("old-tower"), eq(0), eq(0), eq(20), any()))
.thenAnswer(invocation -> {
IrisStructureLocator.CandidateFilter filter = invocation.getArgument(5);
assertFalse(filter.accept(0, 0));
assertFalse(filter.accept(1, 0));
assertFalse(filter.accept(2, 0));
assertTrue(filter.accept(3, 0));
return nearer;
});
assertSame(nearer, GenerationSemanticQueries.nearestStructure(engine, "OLD-TOWER", 0, 0, 20));
structures.when(() -> IrisStructureLocator.locate(
eq(engine), eq("old-tower"), eq(0), eq(0), eq(20), any()))
.thenReturn(foundStructure(120, 90, 0));
assertEquals(foundStructure(100, 73, 5),
GenerationSemanticQueries.nearestStructure(engine, "old-tower", 0, 0, 20));
structures.when(() -> IrisStructureLocator.locate(
eq(engine), eq("old-tower"), eq(0), eq(0), eq(20), any()))
.thenReturn(foundStructure(100, 90, 5));
assertEquals(foundStructure(100, 73, 5),
GenerationSemanticQueries.nearestStructure(engine, "old-tower", 0, 0, 20));
structures.when(() -> IrisStructureLocator.locate(
eq(engine), eq("old-tower"), eq(0), eq(0), eq(20), any()))
.thenReturn(new IrisStructureLocator.LocateResult(IrisStructureLocator.LocateStatus.NOT_FOUND, 0, 0, 0));
assertEquals(foundStructure(100, 73, 5),
GenerationSemanticQueries.nearestStructure(engine, "old-tower", 0, 0, 20));
}
}
@Test
public void exhaustedStructureSearchDoesNotClaimARecordedResultIsNearest() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
IrisStructureLocator.LocateResult limit = new IrisStructureLocator.LocateResult(
IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
try (MockedStatic<IrisStructureLocator> structures = mockStatic(IrisStructureLocator.class)) {
structures.when(() -> IrisStructureLocator.locate(
eq(engine), eq("tower"), eq(0), eq(0), eq(20), any())).thenReturn(limit);
assertSame(limit, GenerationSemanticQueries.nearestStructure(engine, "tower", 0, 0, 20));
}
}
private static IrisStructureLocator.LocateResult foundStructure(int x, int y, int z) {
return new IrisStructureLocator.LocateResult(IrisStructureLocator.LocateStatus.FOUND, x, y, z);
}
@Test
public void rejectsTaperedPredictionsAndMergesTheNearestEligibleRiver() {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime runtime = mock(IrisHydrologyRuntime.class);
IrisDimension dimension = mock(IrisDimension.class);
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
GenerationActivation activation = mock(GenerationActivation.class);
HydrologyFeatureRef tapered = feature(1L, 8, 20, 0);
HydrologyFeatureRef eligible = feature(2L, 40, 20, 0);
ChunkGenerationSemantics.RiverFeatureOccurrence recordedOccurrence =
new ChunkGenerationSemantics.RiverFeatureOccurrence(
"iris:river",
HydrologyFeatureType.RIFFLE,
3L,
new ChunkGenerationSemantics.BlockPosition(100, 70, 0)
);
GenerationSemanticIndex.RiverMatch recorded = new GenerationSemanticIndex.RiverMatch(
recordedOccurrence,
new GenerationSemanticIndex.ChunkReference(6, 0, 1L)
);
when(engine.getComplex()).thenReturn(complex);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(complex.getHydrologyRuntime()).thenReturn(runtime);
when(complex.getTransitionGenerationPlan()).thenReturn(transition);
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
when(transition.hydrologyWeightAt(anyInt(), anyInt())).thenAnswer(invocation ->
invocation.<Integer>getArgument(0) == tapered.x() ? 0.5D : 1D);
when(dimension.getMinHeight()).thenReturn(-64);
when(router.history()).thenReturn(history);
when(history.findRecordedRiver(any())).thenReturn(Optional.of(recorded));
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
when(history.activeActivation()).thenReturn(activation);
when(activation.activationId()).thenReturn(2L);
when(runtime.nearestFeature(
any(), nullable(String.class), anyInt(), anyInt(), anyInt(), any(), any()
)).thenAnswer(invocation -> firstEligible(
List.of(tapered, eligible),
invocation.getArgument(5)
));
GenerationSemanticQueries.RiverResult active = GenerationSemanticQueries.nearestRiver(
engine,
HydrologyFeatureQuery.parse("surface"),
0,
0,
200,
ignored -> { }
).orElseThrow();
assertEquals(GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION, active.source());
assertEquals(40, active.x());
assertEquals(-44, active.y());
HydrologyFeatureRef fartherActive = feature(4L, 120, 20, 0);
doAnswer(invocation -> firstEligible(
List.of(tapered, fartherActive),
invocation.getArgument(5)
)).when(runtime).nearestFeature(
any(), nullable(String.class), anyInt(), anyInt(), anyInt(), any(), any()
);
GenerationSemanticQueries.RiverResult historical = GenerationSemanticQueries.nearestRiver(
engine,
HydrologyFeatureQuery.parse("surface"),
0,
0,
200,
ignored -> { }
).orElseThrow();
assertEquals(GenerationSemanticQueries.RiverSource.RECORDED, historical.source());
assertEquals(100, historical.x());
assertEquals(70, historical.y());
}
@Test
public void returnsRecordedRiverWhenTheActivePackHasNoHydrologyRuntime() {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
GenerationHistory history = mock(GenerationHistory.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
ChunkGenerationSemantics.RiverFeatureOccurrence occurrence =
new ChunkGenerationSemantics.RiverFeatureOccurrence(
"iris:legacy_river",
HydrologyFeatureType.WATERFALL,
9L,
new ChunkGenerationSemantics.BlockPosition(-30, 55, 12)
);
when(engine.getComplex()).thenReturn(complex);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.history()).thenReturn(history);
when(history.findRecordedRiver(any())).thenReturn(Optional.of(
new GenerationSemanticIndex.RiverMatch(
occurrence,
new GenerationSemanticIndex.ChunkReference(-2, 0, 1L)
)
));
GenerationSemanticQueries.RiverResult result = GenerationSemanticQueries.nearestRiver(
engine,
HydrologyFeatureQuery.parse("waterfall"),
0,
0,
100,
ignored -> { }
).orElseThrow();
assertEquals(GenerationSemanticQueries.RiverSource.RECORDED, result.source());
assertEquals(HydrologyFeatureType.WATERFALL, result.type());
assertEquals("iris:legacy_river", result.profileKey().orElseThrow());
}
private static Optional<HydrologyFeatureRef> firstEligible(
List<HydrologyFeatureRef> candidates,
Predicate<HydrologyFeatureRef> eligibility
) {
for (HydrologyFeatureRef candidate : candidates) {
if (eligibility.test(candidate)) {
return Optional.of(candidate);
}
}
return Optional.empty();
}
private static HydrologyFeatureRef feature(long id, int x, int y, int z) {
return new HydrologyFeatureRef(
id,
HydrologyFeatureType.RIFFLE,
10L,
11L,
x,
y,
z,
1,
0,
false
);
}
}
@@ -0,0 +1,83 @@
package art.arcane.iris.engine.history;
import com.google.gson.JsonObject;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationTransitionTest {
private static final String BOUNDARY =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String TERRAIN =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@Test
public void completionPreservesTheImmutableTransitionRecipe() {
GenerationTransition pending = GenerationTransition.pending(256);
GenerationTransition completed = pending.complete(BOUNDARY, TERRAIN);
assertEquals(GenerationTransition.CURRENT_ALGORITHM_VERSION, completed.algorithmVersion());
assertEquals(256, completed.widthBlocks());
assertEquals(BOUNDARY, completed.boundaryIdentity());
assertEquals(TERRAIN, completed.terrainSignatureIdentity());
assertFalse(pending.isComplete());
assertTrue(completed.isComplete());
assertSame(completed, completed.complete(BOUNDARY, TERRAIN));
assertThrows(IllegalStateException.class, () -> completed.complete(TERRAIN, BOUNDARY));
}
@Test
public void strictJsonRoundTripRetainsPendingAndCompleteStates() {
GenerationTransition pending = GenerationTransition.pending(512);
GenerationTransition completed = pending.complete(BOUNDARY, TERRAIN);
assertEquals(pending, GenerationTransition.fromJson(pending.toJson()));
assertEquals(completed, GenerationTransition.fromJson(completed.toJson()));
JsonObject unknown = completed.toJson();
unknown.addProperty("unexpected", true);
assertThrows(IllegalArgumentException.class, () -> GenerationTransition.fromJson(unknown));
}
@Test
public void rejectsInvalidWidthsAndPartiallyCompletedSnapshots() {
assertThrows(
IllegalArgumentException.class,
() -> GenerationTransition.pending(GenerationTransition.MINIMUM_WIDTH_BLOCKS - 1)
);
assertThrows(
IllegalArgumentException.class,
() -> GenerationTransition.pending(GenerationTransition.MAXIMUM_WIDTH_BLOCKS + 1)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationTransition(
GenerationTransition.CURRENT_ALGORITHM_VERSION,
256,
BOUNDARY,
null
)
);
assertThrows(
IllegalArgumentException.class,
() -> new GenerationTransition(0, 256, BOUNDARY, TERRAIN)
);
}
@Test
public void activationRoundTripRetainsItsTransition() {
GenerationActivation activation = GenerationActivation.next(2L, BOUNDARY, 1L, 99L, 256)
.completeTransition(BOUNDARY, TERRAIN);
GenerationActivation restored = GenerationActivation.fromJson(activation.toJson());
assertEquals(activation, restored);
assertEquals(256, restored.transition().widthBlocks());
assertEquals(BOUNDARY, restored.transition().boundaryIdentity());
}
}
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.Optional;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisBoundarySignatureSamplerTest {
@Test
public void recordsTheEffectiveCeilingInInternalCoordinatesAndBiomesInWorldCoordinates() throws Exception {
IrisEngine engine = engine();
UpperDimensionContext upper = mock(UpperDimensionContext.class);
when(engine.getUpperContext()).thenReturn(upper);
when(upper.getEffectiveSurfaceY(-17, 8)).thenReturn(200);
TerrainBoundarySignature signature = IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8);
assertEquals(OptionalInt.of(55), signature.upperCeilingDepth());
assertEquals(64, signature.oceanFloorHeight());
assertEquals(-64, signature.sampleY(0));
assertEquals(188, signature.sampleY(63));
verify(upper).getEffectiveSurfaceY(-17, 8);
}
@Test
public void recordsAbsentUpperMassWithAndWithoutAnUpperContext() throws Exception {
IrisEngine engine = engine();
assertTrue(IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8).upperCeilingDepth().isEmpty());
UpperDimensionContext upper = mock(UpperDimensionContext.class);
when(engine.getUpperContext()).thenReturn(upper);
when(upper.getEffectiveSurfaceY(-17, 8)).thenReturn(256);
assertTrue(IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8).upperCeilingDepth().isEmpty());
}
@Test
public void recordsTheOwningStackLayerBiomeAtEachVerticalSample() throws Exception {
IrisEngine engine = engine();
IrisComplex complex = engine.getComplex();
when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt()))
.thenReturn(Optional.empty());
DimensionStackContext stackContext = mock(DimensionStackContext.class);
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
DimensionTerrainContext terrainContext = mock(DimensionTerrainContext.class);
IrisBiome biome = mock(IrisBiome.class);
when(engine.getDimensionStackContext()).thenReturn(stackContext);
when(stackContext.getLayout(-17, 8)).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer);
when(layer.terrainContext()).thenReturn(terrainContext);
when(layer.biome()).thenReturn(biome);
when(biome.getSkyBiomeKey(any(RNG.class), same(engine), anyDouble(), anyDouble(), anyDouble()))
.thenReturn("minecraft:forest");
TerrainBoundarySignature signature = IrisBoundarySignatureSampler.INSTANCE.sample(engine, -17, 8);
assertEquals("minecraft:forest", signature.biomeAtSample(0));
assertEquals("minecraft:forest", signature.biomeAtSample(63));
}
private static IrisEngine engine() {
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
IrisWorld world = mock(IrisWorld.class);
when(world.getRawWorldSeed()).thenReturn(1337L);
when(engine.getWorld()).thenReturn(world);
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(-64);
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 64D));
when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt()))
.thenReturn(Optional.of("minecraft:plains"));
return engine;
}
}
@@ -0,0 +1,157 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.CRC32;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class RegionGenerationOwnershipTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void roundTripPreservesAssignmentsAcrossNegativeRegionCoordinates() throws Exception {
Path directory = temporaryFolder.newFolder("ownership").toPath();
RegionGenerationOwnership ownership = new RegionGenerationOwnership(-1, -1);
assertTrue(ownership.assign(-32, -32, 7L));
assertTrue(ownership.assign(-1, -1, 7L));
assertTrue(ownership.assign(-17, -9, 19L));
assertTrue(ownership.persist(directory));
Path file = directory.resolve(RegionGenerationOwnership.fileName(-1, -1));
assertTrue(Files.size(file) < 512L);
RegionGenerationOwnership loaded = RegionGenerationOwnership.read(file);
assertEquals(3, loaded.assignmentCount());
assertEquals(7L, loaded.resolve(-32, -32, 31L));
assertEquals(7L, loaded.resolve(-1, -1, 31L));
assertEquals(19L, loaded.resolve(-17, -9, 31L));
assertEquals(31L, loaded.resolve(-16, -8, 31L));
assertFalse(loaded.persist(directory));
}
@Test
public void everyPaletteWidthRoundTripsWithoutBitBoundaryLoss() throws Exception {
Path directory = temporaryFolder.newFolder("wide-palette").toPath();
RegionGenerationOwnership ownership = new RegionGenerationOwnership(0, 0);
for (int chunkZ = 0; chunkZ < 32; chunkZ++) {
for (int chunkX = 0; chunkX < 32; chunkX++) {
int index = chunkX + chunkZ * 32;
ownership.assign(chunkX, chunkZ, index + 1L);
}
}
ownership.persist(directory);
Path file = directory.resolve(RegionGenerationOwnership.fileName(0, 0));
RegionGenerationOwnership loaded = RegionGenerationOwnership.read(file);
assertEquals(1_024, loaded.assignmentCount());
assertTrue(Files.size(file) < 10_000L);
for (int chunkZ = 0; chunkZ < 32; chunkZ++) {
for (int chunkX = 0; chunkX < 32; chunkX++) {
int index = chunkX + chunkZ * 32;
assertEquals(index + 1L, loaded.explicitActivation(chunkX, chunkZ));
}
}
}
@Test
public void assignmentsAreIdempotentButCannotChangeOwner() {
RegionGenerationOwnership ownership = new RegionGenerationOwnership(0, 0);
assertTrue(ownership.assign(3, 5, 12L));
assertFalse(ownership.assign(3, 5, 12L));
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> ownership.assign(3, 5, 13L)
);
assertTrue(error.getMessage().contains("already belongs to activation 12"));
assertEquals(12L, ownership.explicitActivation(3, 5));
}
@Test
public void checksumCorruptionFailsClosed() throws Exception {
Path file = persistedSingleAssignment("checksum");
byte[] encoded = Files.readAllBytes(file);
encoded[20] ^= 0x10;
Files.write(file, encoded);
IOException error = assertThrows(IOException.class, () -> RegionGenerationOwnership.read(file));
assertTrue(error.getMessage().contains("checksum mismatch"));
}
@Test
public void truncatedDataFailsClosed() throws Exception {
Path file = persistedSingleAssignment("truncated");
byte[] encoded = Files.readAllBytes(file);
Files.write(file, java.util.Arrays.copyOf(encoded, encoded.length - 3));
IOException error = assertThrows(IOException.class, () -> RegionGenerationOwnership.read(file));
assertTrue(error.getMessage().contains("Invalid generation ownership file"));
}
@Test
public void unsupportedVersionFailsClosedEvenWithAValidChecksum() throws Exception {
Path file = persistedSingleAssignment("version");
byte[] encoded = Files.readAllBytes(file);
encoded[4] = 0;
encoded[5] = 2;
rewriteChecksum(encoded);
Files.write(file, encoded);
IOException error = assertThrows(IOException.class, () -> RegionGenerationOwnership.read(file));
assertTrue(error.getMessage().contains("unsupported format version 2"));
}
@Test
public void regionCoordinatesOutsideTheChunkRangeFailClosed() throws Exception {
Path file = persistedSingleAssignment("coordinate-range");
byte[] encoded = Files.readAllBytes(file);
ByteBuffer.wrap(encoded, 8, Integer.BYTES).putInt(Integer.MAX_VALUE);
rewriteChecksum(encoded);
Files.write(file, encoded);
IOException error = assertThrows(IOException.class, () -> RegionGenerationOwnership.read(file));
assertTrue(error.getMessage().contains("region coordinate is outside"));
}
@Test
public void activationIdsMustBePositive() {
RegionGenerationOwnership ownership = new RegionGenerationOwnership(0, 0);
assertThrows(IllegalArgumentException.class, () -> ownership.assign(0, 0, 0L));
assertThrows(IllegalArgumentException.class, () -> ownership.assign(0, 0, -1L));
assertThrows(IllegalArgumentException.class, () -> ownership.resolve(0, 0, 0L));
}
private Path persistedSingleAssignment(String directoryName) throws Exception {
Path directory = temporaryFolder.newFolder(directoryName).toPath();
RegionGenerationOwnership ownership = new RegionGenerationOwnership(0, 0);
ownership.assign(0, 0, 1L);
ownership.persist(directory);
return directory.resolve(RegionGenerationOwnership.fileName(0, 0));
}
private static void rewriteChecksum(byte[] encoded) {
int bodyLength = encoded.length - Integer.BYTES;
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, bodyLength);
ByteBuffer.wrap(encoded, bodyLength, Integer.BYTES).putInt((int) checksum.getValue());
}
}
@@ -0,0 +1,445 @@
package art.arcane.iris.engine.history;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.CRC32;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class TerrainBoundarySignatureStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void preservesPresentAndAbsentUpperCeilingsAcrossShardReloads() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("upper-ceiling-shards").toPath());
TerrainBoundarySignature.Samples samples = new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 4, 0),
new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0]));
TerrainBoundarySignature present = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(-17, 8, 64, 64, OptionalInt.empty(), OptionalInt.of(80)), samples);
TerrainBoundarySignature absent = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(257, 8, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples);
store.publish(2L, List.of(present, absent));
TerrainBoundarySignatureStore.Snapshot reopened = store.load(2L);
assertEquals(OptionalInt.of(80), reopened.signatureAt(-17, 8).orElseThrow().upperCeilingDepth());
assertTrue(reopened.signatureAt(257, 8).orElseThrow().upperCeilingDepth().isEmpty());
}
@Test
public void preservesEveryCompactFieldAtNegativeCoordinates() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("signature-dimension").toPath();
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignature negative = signature(
-17,
Integer.MIN_VALUE,
93,
-48,
OptionalInt.of(62),
-64,
32,
List.of("iris:forest", "iris:cavern", "iris:forest"),
new short[]{2, 1, 0}
);
TerrainBoundarySignature positive = signature(
Integer.MAX_VALUE,
4,
77,
41,
OptionalInt.empty(),
0,
1,
List.of(),
new short[0]
);
TerrainBoundarySignatureStore.Snapshot published = store.publish(19L, List.of(positive, negative));
TerrainBoundarySignatureStore.Snapshot loaded = store.load(19L);
assertEquals(dimensionRoot.toAbsolutePath().resolve("iris/generation/boundaries"), store.directory());
assertEquals(19L, published.activationId());
assertEquals(published.identity(), loaded.identity());
assertEquals(2, loaded.size());
assertSignatureEquals(negative, loaded.signatureAt(-17, Integer.MIN_VALUE).orElseThrow());
assertSignatureEquals(positive, loaded.signatureAt(Integer.MAX_VALUE, 4).orElseThrow());
assertTrue(loaded.signatureAt(0, 0).isEmpty());
assertThrows(UnsupportedOperationException.class, () -> loaded.signatures().clear());
}
@Test
public void writesDeterministicallyRegardlessOfCollectionOrder() throws Exception {
Path firstRoot = temporaryFolder.newFolder("deterministic-first").toPath();
Path secondRoot = temporaryFolder.newFolder("deterministic-second").toPath();
TerrainBoundarySignatureStore firstStore = new TerrainBoundarySignatureStore(firstRoot);
TerrainBoundarySignatureStore secondStore = new TerrainBoundarySignatureStore(secondRoot);
TerrainBoundarySignature first = signature(
-1,
9,
80,
40,
OptionalInt.empty(),
-32,
16,
List.of("iris:snowy_森林"),
new short[]{0, 0}
);
TerrainBoundarySignature second = signature(
12,
-8,
70,
35,
OptionalInt.of(50),
5,
4,
List.of("iris:desert"),
new short[]{0}
);
TerrainBoundarySignatureStore.Snapshot firstSnapshot = firstStore.publish(8L, List.of(first, second));
TerrainBoundarySignatureStore.Snapshot secondSnapshot = secondStore.publish(8L, List.of(second, first));
assertEquals(firstSnapshot.identity(), secondSnapshot.identity());
assertArrayEquals(
Files.readAllBytes(firstStore.snapshotPath(8L)),
Files.readAllBytes(secondStore.snapshotPath(8L))
);
}
@Test
public void streamedBoundaryMatchesCollectionPublicationAcrossCells() throws Exception {
TerrainBoundarySignatureStore streamed = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("streamed-boundary").toPath()
);
TerrainBoundarySignatureStore collected = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("collected-boundary").toPath()
);
List<GenerationBoundary.ChunkCoordinate> chunks = new ArrayList<>();
for (int index = -40; index < 40; index++) {
chunks.add(new GenerationBoundary.ChunkCoordinate(index * 3, index % 5));
}
GenerationBoundary boundary = GenerationBoundary.freeze("a".repeat(64), chunks);
List<TerrainBoundarySignature> signatures = new ArrayList<>();
boundary.forEachExposedBlockColumn((x, z) -> signatures.add(simpleSignature(x, z, 81)));
TerrainBoundarySignatureStore.Snapshot first = streamed.publish(
2L, boundary, (x, z) -> simpleSignature(x, z, 81)
);
TerrainBoundarySignatureStore.Snapshot second = collected.publish(2L, signatures);
assertEquals(boundary.exposedBlockColumnCount(), first.size());
assertEquals(second.identity(), first.identity());
assertArrayEquals(Files.readAllBytes(collected.snapshotPath(2L)), Files.readAllBytes(streamed.snapshotPath(2L)));
boundary.forEachExposedBlockColumn((x, z) -> assertTrue(first.signatureAt(x, z).isPresent()));
assertTrue(first.cachedShardCount() <= 64);
}
@Test
public void streamedBoundaryDoesNotPublishOnMissingOrMisplacedSamples() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("failed-streamed-boundary").toPath()
);
GenerationBoundary boundary = GenerationBoundary.freeze(
"a".repeat(64), List.of(new GenerationBoundary.ChunkCoordinate(0, 0))
);
assertThrows(IOException.class, () -> store.publish(2L, boundary, (x, z) -> {
throw new IOException("sample failed");
}));
assertFalse(Files.exists(store.snapshotPath(2L)));
assertThrows(IOException.class, () -> store.publish(2L, boundary, (x, z) -> simpleSignature(x + 1, z, 81)));
assertFalse(Files.exists(store.snapshotPath(2L)));
assertEquals(60, store.publish(2L, boundary, (x, z) -> simpleSignature(x, z, 81)).size());
}
@Test
public void canonicalizesEquivalentBiomePalettesBeforeIdentityAndPublication() throws Exception {
Path firstRoot = temporaryFolder.newFolder("palette-first").toPath();
Path secondRoot = temporaryFolder.newFolder("palette-second").toPath();
TerrainBoundarySignature first = signature(
5,
-7,
80,
40,
OptionalInt.empty(),
-64,
32,
List.of("iris:z", "iris:a", "iris:z", "iris:unused"),
new short[]{0, 1, 2}
);
TerrainBoundarySignature equivalent = signature(
5,
-7,
80,
40,
OptionalInt.empty(),
-64,
32,
List.of("iris:a", "iris:z"),
new short[]{1, 0, 1}
);
TerrainBoundarySignatureStore firstStore = new TerrainBoundarySignatureStore(firstRoot);
TerrainBoundarySignatureStore secondStore = new TerrainBoundarySignatureStore(secondRoot);
TerrainBoundarySignatureStore.Snapshot firstSnapshot = firstStore.publish(13L, List.of(first));
TerrainBoundarySignatureStore.Snapshot repeated = firstStore.publish(13L, List.of(equivalent));
TerrainBoundarySignatureStore.Snapshot secondSnapshot = secondStore.publish(13L, List.of(equivalent));
assertEquals(firstSnapshot.identity(), repeated.identity());
assertEquals(firstSnapshot.identity(), secondSnapshot.identity());
assertArrayEquals(
Files.readAllBytes(firstStore.snapshotPath(13L)),
Files.readAllBytes(secondStore.snapshotPath(13L))
);
TerrainBoundarySignature loaded = firstStore.load(13L).signatureAt(5, -7).orElseThrow();
assertEquals(List.of("iris:a", "iris:z"), loaded.samples().biomes().palette());
assertArrayEquals(new short[]{1, 0, 1}, loaded.samples().biomes().paletteIndices());
}
@Test
public void keepsActivationSnapshotImmutableAndRejectsDuplicateCoordinates() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("immutable-signature-dimension").toPath();
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignature original = simpleSignature(-5, 6, 81);
TerrainBoundarySignature changed = simpleSignature(-5, 6, 82);
TerrainBoundarySignatureStore.Snapshot first = store.publish(2L, List.of(original));
TerrainBoundarySignatureStore.Snapshot repeated = store.publish(2L, List.of(original));
assertEquals(first.identity(), repeated.identity());
assertThrows(FileAlreadyExistsException.class, () -> store.publish(2L, List.of(changed)));
assertThrows(IllegalArgumentException.class, () -> store.publish(3L, List.of(original, changed)));
assertEquals(81, store.load(2L).signatureAt(-5, 6).orElseThrow().surfaceHeight());
}
@Test
public void failsClosedForChecksumCorruptionAndTruncation() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("corrupt-signature-dimension").toPath();
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
store.publish(4L, List.of(simpleSignature(1, -2, 70)));
Path corrupted = store.snapshotPath(4L);
byte[] corruptedBytes = Files.readAllBytes(corrupted);
corruptedBytes[8] ^= 0x01;
Files.write(corrupted, corruptedBytes);
IOException checksumFailure = assertThrows(IOException.class, () -> store.load(4L));
assertTrue(checksumFailure.getMessage().contains("checksum"));
store.publish(5L, List.of(simpleSignature(-3, 7, 72)));
Path truncated = store.snapshotPath(5L);
byte[] complete = Files.readAllBytes(truncated);
Files.write(truncated, Arrays.copyOf(complete, complete.length - 2));
assertThrows(IOException.class, () -> store.load(5L));
}
@Test
public void verifiesContentIdentityAndDeclaredBounds() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("verified-signature-dimension").toPath();
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
store.publish(9L, List.of(simpleSignature(-9, -10, 90)));
Path identitySnapshot = store.snapshotPath(9L);
byte[] identityBytes = Files.readAllBytes(identitySnapshot);
identityBytes[20] ^= 0x01;
updateChecksum(identityBytes);
Files.write(identitySnapshot, identityBytes);
IOException identityFailure = assertThrows(IOException.class, () -> store.load(9L));
assertTrue(identityFailure.getMessage().contains("identity mismatch"));
store.publish(10L, List.of());
Path boundedSnapshot = store.snapshotPath(10L);
byte[] boundedBytes = Files.readAllBytes(boundedSnapshot);
ByteBuffer.wrap(boundedBytes).putInt(16, Integer.MAX_VALUE);
updateChecksum(boundedBytes);
Files.write(boundedSnapshot, boundedBytes);
assertThrows(IOException.class, () -> store.load(10L));
}
@Test
public void rejectsInvalidActivationAndMalformedUtf8() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("invalid-signature-dimension").toPath();
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
assertThrows(IllegalArgumentException.class, () -> store.publish(0L, List.of()));
String malformed = new String(new char[]{'i', 'r', 'i', 's', ':', '\uD800'});
TerrainBoundarySignature invalid = signature(
0,
0,
1,
0,
OptionalInt.empty(),
0,
1,
List.of(malformed),
new short[]{0}
);
assertThrows(IOException.class, () -> store.publish(1L, List.of(invalid)));
assertFalse(Files.exists(store.snapshotPath(1L)));
}
@Test
public void rejectsSymbolicLinksAtEveryStorageComponent() throws Exception {
Path probe = temporaryFolder.newFolder("terrain-symlink-probe").toPath();
Path probeTarget = temporaryFolder.newFolder("terrain-symlink-probe-target").toPath();
try {
Files.createSymbolicLink(probe.resolve("link"), probeTarget);
} catch (IOException | UnsupportedOperationException exception) {
Assume.assumeNoException(exception);
}
for (String component : new String[]{"dimension", "iris", "generation", "boundaries"}) {
assertStorageLinkRejected(component);
}
}
@Test
public void concurrentStoreInstancesCannotClobberTerrainSignatures() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("terrain-race").toPath();
TerrainBoundarySignatureStore firstStore = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignatureStore secondStore = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignature firstSignature = simpleSignature(1, 2, 70);
TerrainBoundarySignature secondSignature = simpleSignature(3, 4, 90);
CyclicBarrier barrier = new CyclicBarrier(2);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Boolean> first = executor.submit(
() -> publishAfterBarrier(firstStore, 22L, firstSignature, barrier)
);
Future<Boolean> second = executor.submit(
() -> publishAfterBarrier(secondStore, 22L, secondSignature, barrier)
);
assertTrue(first.get() ^ second.get());
TerrainBoundarySignatureStore.Snapshot loaded = firstStore.load(22L);
assertTrue(loaded.signatureAt(1, 2).isPresent() ^ loaded.signatureAt(3, 4).isPresent());
} finally {
executor.shutdownNow();
}
}
private void assertStorageLinkRejected(String component) throws Exception {
Path container = temporaryFolder.newFolder("terrain-linked-" + component).toPath();
Path dimensionRoot = container.resolve("dimension");
Path external = temporaryFolder.newFolder("terrain-linked-target-" + component).toPath();
if (component.equals("dimension")) {
Files.createSymbolicLink(dimensionRoot, external);
} else {
Files.createDirectory(dimensionRoot);
Path iris = dimensionRoot.resolve("iris");
if (component.equals("iris")) {
Files.createSymbolicLink(iris, external);
} else {
Files.createDirectory(iris);
Path generation = iris.resolve("generation");
if (component.equals("generation")) {
Files.createSymbolicLink(generation, external);
} else {
Files.createDirectory(generation);
Files.createSymbolicLink(generation.resolve("boundaries"), external);
}
}
}
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
assertThrows(IOException.class, () -> store.publish(1L, List.of()));
}
private static boolean publishAfterBarrier(
TerrainBoundarySignatureStore store,
long activationId,
TerrainBoundarySignature signature,
CyclicBarrier barrier
) throws Exception {
barrier.await();
try {
store.publish(activationId, List.of(signature));
return true;
} catch (IOException expected) {
return false;
}
}
private static TerrainBoundarySignature simpleSignature(int blockX, int blockZ, int surfaceHeight) {
return signature(
blockX,
blockZ,
surfaceHeight,
surfaceHeight - 20,
OptionalInt.of(surfaceHeight - 10),
-64,
32,
List.of("iris:plains"),
new short[]{0, 0}
);
}
private static TerrainBoundarySignature signature(
int blockX,
int blockZ,
int surfaceHeight,
int oceanFloorHeight,
OptionalInt fluidHeight,
int minimumY,
int sampleStep,
List<String> palette,
short[] paletteIndices
) {
TerrainBoundarySignature.VerticalLayout layout = new TerrainBoundarySignature.VerticalLayout(
minimumY,
sampleStep,
paletteIndices.length
);
TerrainBoundarySignature.BiomeEncoding biomeEncoding =
new TerrainBoundarySignature.BiomeEncoding(palette, paletteIndices);
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(
blockX,
blockZ,
surfaceHeight,
oceanFloorHeight,
fluidHeight,
OptionalInt.empty()
),
new TerrainBoundarySignature.Samples(layout, biomeEncoding)
);
}
private static void assertSignatureEquals(
TerrainBoundarySignature expected,
TerrainBoundarySignature actual
) {
assertEquals(expected.column(), actual.column());
assertEquals(expected.samples().layout(), actual.samples().layout());
for (int index = 0; index < expected.sampleCount(); index++) {
assertEquals(expected.biomeAtSample(index), actual.biomeAtSample(index));
}
}
private static void updateChecksum(byte[] encoded) {
int bodyLength = encoded.length - Integer.BYTES;
CRC32 checksum = new CRC32();
checksum.update(encoded, 0, bodyLength);
ByteBuffer.wrap(encoded, bodyLength, Integer.BYTES).putInt((int) checksum.getValue());
}
}
@@ -0,0 +1,80 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class TerrainBoundarySignatureTest {
@Test
public void storesPlatformNeutralTerrainSamples() {
ArrayList<String> palette = new ArrayList<>(List.of("iris:forest", "iris:cave"));
short[] biomeIndices = {0, 1, 1};
TerrainBoundarySignature.BiomeEncoding biomeEncoding =
new TerrainBoundarySignature.BiomeEncoding(palette, biomeIndices);
TerrainBoundarySignature signature = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(-1, 32, 91, 48, OptionalInt.of(63), OptionalInt.of(48)),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 32, 3),
biomeEncoding
)
);
palette.set(0, "changed");
biomeIndices[0] = 1;
assertEquals(-1, signature.blockX());
assertEquals(32, signature.blockZ());
assertEquals(91, signature.surfaceHeight());
assertEquals(48, signature.oceanFloorHeight());
assertEquals(63, signature.fluidHeight().orElseThrow());
assertEquals(3, signature.sampleCount());
assertEquals(-64, signature.sampleY(0));
assertEquals(0, signature.sampleY(2));
assertEquals("iris:forest", signature.biomeAtSample(0));
}
@Test
public void returnedCompactSamplesCannotMutateSignature() {
TerrainBoundarySignature.BiomeEncoding biomes = new TerrainBoundarySignature.BiomeEncoding(
List.of("iris:forest"), new short[]{0}
);
short[] exportedBiomes = biomes.paletteIndices();
exportedBiomes[0] = -1;
assertEquals("iris:forest", biomes.biomeAtSample(0));
assertThrows(UnsupportedOperationException.class, () -> biomes.palette().add("iris:other"));
}
@Test
public void supportsColumnsWithoutFluid() {
TerrainBoundarySignature.Column column =
new TerrainBoundarySignature.Column(0, 0, 80, 50, OptionalInt.empty(), OptionalInt.empty());
assertTrue(column.fluidHeight().isEmpty());
assertTrue(column.upperCeilingDepth().isEmpty());
assertThrows(IllegalArgumentException.class, () -> new TerrainBoundarySignature.Column(
0, 0, 80, 50, OptionalInt.empty(), OptionalInt.of(0)));
}
@Test
public void rejectsInvalidSampleEncodings() {
assertThrows(IllegalArgumentException.class,
() -> new TerrainBoundarySignature.VerticalLayout(0, 0, 1));
assertThrows(IllegalArgumentException.class,
() -> new TerrainBoundarySignature.BiomeEncoding(List.of("iris:forest"), new short[]{1}));
TerrainBoundarySignature.VerticalLayout layout =
new TerrainBoundarySignature.VerticalLayout(0, 4, 2);
TerrainBoundarySignature.BiomeEncoding oneBiome =
new TerrainBoundarySignature.BiomeEncoding(List.of("iris:forest"), new short[]{0});
assertThrows(IllegalArgumentException.class,
() -> new TerrainBoundarySignature.Samples(layout, oneBiome));
}
}
@@ -0,0 +1,486 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.OptionalInt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class TransitionGenerationPlanTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void interpolatesPresentAndAbsentUpperDepthUsingTheSameBoundedNeighbours() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("upper-depth-neighbours").toPath());
TerrainBoundarySignature.Samples samples = new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 4, 0),
new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0]));
TerrainBoundarySignature present = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(15, 0, 64, 64, OptionalInt.empty(), OptionalInt.of(100)), samples);
TerrainBoundarySignature absent = new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(15, 2, 64, 64, OptionalInt.empty(), OptionalInt.empty()), samples);
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, store.publish(2L, List.of(present, absent)));
assertEquals(100D, sampler.sample(16, 0).historicalUpperCeilingDepth(), 0D);
assertEquals(0D, sampler.sample(16, 2).historicalUpperCeilingDepth(), 0D);
assertEquals(50D, sampler.sample(17, 1).historicalUpperCeilingDepth(), 0D);
assertEquals(1L, sampler.candidateBuildCount());
}
@Test
public void bindsImmutableActivationEpochWidthAndBoundaryInputs() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("frontier-sha256", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(9L, boundary);
TransitionGenerationPlan.Specification specification = new TransitionGenerationPlan.Specification(
9L,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
GenerationTransition.CURRENT_ALGORITHM_VERSION,
32,
"frontier-sha256",
snapshot.identity()
);
TransitionGenerationPlan plan = new TransitionGenerationPlan(specification, boundary, snapshot);
assertEquals(9L, plan.activationId());
assertEquals(specification.oldEpochId(), plan.oldEpochId());
assertEquals(specification.newEpochId(), plan.newEpochId());
assertEquals(32, plan.widthBlocks());
assertEquals(GenerationTransition.CURRENT_ALGORITHM_VERSION, plan.algorithmVersion());
assertEquals("frontier-sha256", plan.boundaryIdentity());
assertEquals(snapshot.identity(), plan.terrainSignatures().identity());
assertTrue(plan.isHistoricalBlock(15, 15));
assertEquals(0D, plan.newEpochWeightAt(15, 15), 0D);
assertEquals(1D, plan.newEpochWeightAt(47, 15), 0D);
}
@Test
public void computesTransitionForEnclosedNewTerritory() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("ring", List.of(
new GenerationBoundary.ChunkCoordinate(-1, 0),
new GenerationBoundary.ChunkCoordinate(1, 0),
new GenerationBoundary.ChunkCoordinate(0, -1),
new GenerationBoundary.ChunkCoordinate(0, 1)
));
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(2L, boundary);
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(2L, "old", "new", 16, "ring", snapshot.identity()),
boundary,
snapshot
);
assertEquals(8D, plan.distanceToHistoricalChunks(8, 8), 0D);
assertEquals(0.5D, plan.newEpochWeightAt(8, 8), 0D);
}
@Test
public void samplesFrozenTerrainAndSeparatelyTapersHydrology() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("terrain", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(3L, boundary);
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(3L, "old", "new", 32, "terrain", snapshot.identity()),
boundary,
snapshot
);
TransitionGenerationPlan.TerrainSample edge = plan.terrainSampleAt(16, 8);
assertNotNull(edge.nearestSignature());
assertEquals(1D, edge.distanceToHistoricalTerrain(), 0D);
assertEquals(64D, edge.historicalSurfaceHeight(), 0D);
assertEquals(0D, edge.hydrologyWeight(), 0D);
assertTrue(edge.newEpochWeight() > 0D);
assertTrue(edge.newEpochWeight() < 0.001D);
assertEquals(1D, plan.newEpochWeightAt(47, 8), 0D);
assertEquals(0D, plan.hydrologyWeightAt(47, 8), 0D);
assertEquals(0.5D, plan.hydrologyWeightAt(63, 8), 0D);
assertEquals(1D, plan.hydrologyWeightAt(79, 8), 0D);
assertFalse(plan.allowsNewFootprint(31, 8, 48, 8));
assertTrue(plan.allowsNewFootprint(47, 8, 63, 8));
}
@Test
public void eachNewBoundaryColumnStartsAtItsTouchingHistoricalEdgeHeight() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("varying-edge", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
Path dimensionRoot = temporaryFolder.getRoot().toPath().resolve("varying-edge");
Files.createDirectory(dimensionRoot);
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(
7L,
boundary.exposedBlockColumns().stream()
.map(column -> heightSignature(
column.blockX(),
column.blockZ(),
column.blockX() == 15 ? 40 + column.blockZ() * 7 : 180
))
.toList()
);
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(7L, "old", "new", 16, "varying-edge", snapshot.identity()),
boundary,
snapshot
);
for (int blockZ = 1; blockZ < 15; blockZ++) {
assertEquals(
40D + blockZ * 7D,
plan.terrainSampleAt(16, blockZ).historicalSurfaceHeight(),
0D
);
}
}
@Test
public void rejectsFootprintsWhoseInteriorCrossesHistoryOrTheTerrainBand() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("footprints", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(5L, boundary);
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(5L, "old", "new", 16, "footprints", snapshot.identity()),
boundary,
snapshot
);
assertFalse(plan.allowsNewFootprint(-20, -20, 40, 40));
assertFalse(plan.allowsNewFootprint(16, -20, 16, 35));
assertTrue(plan.allowsNewFootprint(31, 8, 31, 8));
}
@Test
public void exposesTheNearestFrozenPhysicalBiomeUntilDiscreteHandoff() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("biomes", List.of(
new GenerationBoundary.ChunkCoordinate(0, 0)
));
Path dimensionRoot = temporaryFolder.getRoot().toPath().resolve("activation-biomes");
Files.createDirectory(dimensionRoot);
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(6L, boundary.exposedBlockColumns().stream()
.map(column -> biomeSignature(column.blockX(), column.blockZ()))
.toList());
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(6L, "old", "new", 16, "biomes", snapshot.identity()),
boundary,
snapshot
);
assertEquals("iris:middle", plan.historicalPhysicalBiomeKeyAt(16, 14, 8).orElseThrow());
assertEquals("iris:high", plan.historicalPhysicalBiomeKeyAt(16, 18, 8).orElseThrow());
assertTrue(plan.historicalPhysicalBiomeKeyAt(31, 14, 8).isEmpty());
}
@Test
public void emptyHistoryUsesTheNewEpochWithoutAStoredTerrainSample() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("empty", List.of());
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(4L, boundary);
TransitionGenerationPlan plan = new TransitionGenerationPlan(
specification(4L, "old", "new", 32, "empty", snapshot.identity()),
boundary,
snapshot
);
TransitionGenerationPlan.TerrainSample sample = plan.terrainSampleAt(-120, 240);
assertFalse(sample.hasHistoricalSignature());
assertEquals(1D, sample.newEpochWeight(), 0D);
assertEquals(1D, sample.hydrologyWeight(), 0D);
assertTrue(plan.allowsNewDiscreteContentAt(-120, 240));
}
@Test
public void boundedChunkCandidatesMatchExhaustiveNearestWeightingAndTies() throws Exception {
List<TerrainBoundarySignature> signatures = new ArrayList<>();
for (int x = -160; x <= 160; x += 11) {
for (int z = -160; z <= 160; z += 13) {
signatures.add(heightSignature(x, z, Math.floorMod(x * 7 + z * 17, 220)));
}
}
signatures.add(heightSignature(-1, 0, 77));
signatures.add(heightSignature(1, 0, 101));
signatures.add(heightSignature(0, -1, 125));
signatures.add(heightSignature(0, 1, 149));
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot("nearest-oracle", signatures);
for (int width : new int[]{32, GenerationTransition.MAXIMUM_WIDTH_BLOCKS}) {
TransitionBoundarySampler sampler = new TransitionBoundarySampler(width, snapshot);
for (GenerationBoundary.ChunkCoordinate chunk : List.of(
new GenerationBoundary.ChunkCoordinate(-1, -1),
new GenerationBoundary.ChunkCoordinate(0, 0),
new GenerationBoundary.ChunkCoordinate(4, 5)
)) {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
assertMatchesExhaustive(signatures, sampler, chunk.chunkX() * 16 + localX,
chunk.chunkZ() * 16 + localZ, width * 2);
}
}
}
}
}
@Test
public void includesSearchRadiusTiesAndExcludesMoreDistantSignatures() throws Exception {
List<TerrainBoundarySignature> signatures = List.of(
heightSignature(-64, 0, 40), heightSignature(64, 0, 60),
heightSignature(0, -64, 80), heightSignature(0, 64, 100),
heightSignature(65, 0, 500)
);
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot("radius-ties", signatures));
assertMatchesExhaustive(signatures, sampler, 0, 0, 64);
assertEquals(70D, sampler.sample(0, 0).historicalSurfaceHeight(), 0D);
assertEquals(-64, sampler.sample(0, 0).nearestSignature().blockX());
assertMatchesExhaustive(signatures, sampler, 0, 1, 64);
}
@Test
public void wideFragmentedBoundaryOnlyLoadsCompetitiveCellsAndKeepsBoundedCandidates() throws Exception {
List<TerrainBoundarySignature> signatures = fragmentedSignatures();
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot("fragmented-nearest", signatures);
List<TerrainBoundarySignature> candidates = snapshot.nearestCandidatesForChunk(0, 0, 32_768);
assertTrue(signatures.size() > 1_024);
assertTrue(candidates.size() <= 1_024);
assertEquals(4, candidates.size());
assertEquals(1L, snapshot.shardLoadCount());
assertTrue(snapshot.catalogProbeCount() <= 4_356L);
TransitionBoundarySampler sampler = new TransitionBoundarySampler(16_384, snapshot);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertMatchesExhaustive(signatures, sampler, x, z, 32_768);
}
}
assertEquals(1L, sampler.candidateBuildCount());
assertEquals(1L, snapshot.shardLoadCount());
assertTrue(snapshot.cachedShardCount() <= 64);
}
@Test
public void farOutsideChunkDoesBoundedCatalogWorkOnceAndDoesNotReadShards() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot("distant-nearest", fragmentedSignatures());
TransitionBoundarySampler sampler = new TransitionBoundarySampler(16_384, snapshot);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertFalse(sampler.sample(1_000_000_000 + x, -1_000_000_000 + z).hasHistoricalSignature());
}
}
assertEquals(1L, sampler.candidateBuildCount());
assertEquals(0L, sampler.shardLoadCount());
assertTrue(sampler.catalogProbeCount() <= 4_356L);
}
@Test
public void footprintIntersectionStopsAtTheFirstWitnessWithoutCollectingSignatures() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot("footprint-stream", fragmentedSignatures());
TransitionBoundarySampler sampler = new TransitionBoundarySampler(16_384, snapshot);
assertTrue(sampler.intersectsTerrainBand(Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE));
assertEquals(1L, sampler.shardLoadCount());
assertEquals(1L, sampler.catalogProbeCount());
TerrainBoundarySignatureStore.Snapshot exact = snapshot("footprint-edge", List.of(heightSignature(0, 0, 70)));
TransitionBoundarySampler edge = new TransitionBoundarySampler(32, exact);
assertFalse(edge.intersectsTerrainBand(32, 0, 32, 0));
assertTrue(edge.intersectsTerrainBand(31, 0, 31, 0));
assertFalse(edge.intersectsTerrainBand(23, 23, 23, 23));
}
@Test
public void rejectsInvalidOrMismatchedImmutableInputs() throws Exception {
GenerationBoundary boundary = GenerationBoundary.freeze("actual", List.of());
TerrainBoundarySignatureStore.Snapshot snapshot = snapshot(1L, boundary);
assertThrows(IllegalArgumentException.class,
() -> specification(0L, "old", "new", 16, "actual", snapshot.identity()));
assertThrows(IllegalArgumentException.class,
() -> specification(1L, "same", "same", 16, "actual", snapshot.identity()));
assertThrows(IllegalArgumentException.class,
() -> specification(1L, "old", "new", 0, "actual", snapshot.identity()));
assertThrows(IllegalArgumentException.class,
() -> new TransitionGenerationPlan(
specification(1L, "old", "new", 16, "different", snapshot.identity()),
boundary,
snapshot
));
assertThrows(IllegalArgumentException.class,
() -> new TransitionGenerationPlan(
specification(2L, "old", "new", 16, "actual", snapshot.identity()),
boundary,
snapshot
));
assertThrows(IllegalArgumentException.class,
() -> new TransitionGenerationPlan(
specification(1L, "old", "new", 16, "actual", "f".repeat(64)),
boundary,
snapshot
));
assertThrows(IllegalArgumentException.class,
() -> new TransitionGenerationPlan(
new TransitionGenerationPlan.Specification(
1L,
"old",
"new",
GenerationTransition.CURRENT_ALGORITHM_VERSION + 1,
16,
"actual",
snapshot.identity()
),
boundary,
snapshot
));
}
private TerrainBoundarySignatureStore.Snapshot snapshot(
long activationId,
GenerationBoundary boundary
) throws IOException {
Path dimensionRoot = temporaryFolder.getRoot().toPath().resolve("activation-" + activationId);
Files.createDirectory(dimensionRoot);
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(dimensionRoot);
return store.publish(activationId, boundary.exposedBlockColumns().stream()
.map(column -> signature(column.blockX(), column.blockZ()))
.toList());
}
private TerrainBoundarySignatureStore.Snapshot snapshot(
String name,
List<TerrainBoundarySignature> signatures
) throws IOException {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(temporaryFolder.newFolder(name).toPath());
store.publish(1L, signatures);
return store.load(1L);
}
private static List<TerrainBoundarySignature> fragmentedSignatures() {
List<TerrainBoundarySignature> signatures = new ArrayList<>();
for (int x = -16; x <= 16; x++) {
for (int z = -16; z <= 16; z++) {
for (int local = 0; local < 4; local++) {
signatures.add(heightSignature(x * 1_024 + local * 4, z * 1_024 + local * 4, 60 + local));
}
}
}
return signatures;
}
private static void assertMatchesExhaustive(
List<TerrainBoundarySignature> signatures,
TransitionBoundarySampler sampler,
int blockX,
int blockZ,
int searchWidth
) {
List<TerrainBoundarySignature> nearest = signatures.stream()
.filter(signature -> distanceSquared(signature, blockX, blockZ) <= (double) searchWidth * searchWidth)
.sorted(Comparator.comparingDouble((TerrainBoundarySignature signature) -> distanceSquared(signature, blockX, blockZ))
.thenComparingInt(TerrainBoundarySignature::blockX)
.thenComparingInt(TerrainBoundarySignature::blockZ))
.limit(4)
.toList();
TransitionGenerationPlan.TerrainSample sampled = sampler.sample(blockX, blockZ);
assertEquals(!nearest.isEmpty(), sampled.hasHistoricalSignature());
if (nearest.isEmpty()) {
return;
}
assertEquals(nearest.getFirst().blockX(), sampled.nearestSignature().blockX());
assertEquals(nearest.getFirst().blockZ(), sampled.nearestSignature().blockZ());
assertEquals(Math.sqrt(distanceSquared(nearest.getFirst(), blockX, blockZ)), sampled.distanceToHistoricalTerrain(), 0D);
double surface = nearest.getFirst().surfaceHeight();
double floor = nearest.getFirst().oceanFloorHeight();
if (distanceSquared(nearest.getFirst(), blockX, blockZ) > 1D) {
surface = 0D;
floor = 0D;
double totalWeight = 0D;
for (TerrainBoundarySignature signature : nearest) {
double weight = 1D / distanceSquared(signature, blockX, blockZ);
surface += signature.surfaceHeight() * weight;
floor += signature.oceanFloorHeight() * weight;
totalWeight += weight;
}
surface /= totalWeight;
floor /= totalWeight;
}
assertEquals(surface, sampled.historicalSurfaceHeight(), 0D);
assertEquals(floor, sampled.historicalOceanFloorHeight(), 0D);
}
private static double distanceSquared(TerrainBoundarySignature signature, int blockX, int blockZ) {
long x = (long) signature.blockX() - blockX;
long z = (long) signature.blockZ() - blockZ;
return (double) x * x + (double) z * z;
}
private static TransitionGenerationPlan.Specification specification(
long activationId,
String oldEpochId,
String newEpochId,
int widthBlocks,
String boundaryIdentity,
String terrainSignatureIdentity
) {
return new TransitionGenerationPlan.Specification(
activationId,
oldEpochId,
newEpochId,
GenerationTransition.CURRENT_ALGORITHM_VERSION,
widthBlocks,
boundaryIdentity,
terrainSignatureIdentity
);
}
private static TerrainBoundarySignature signature(int blockX, int blockZ) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, 64, 63, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(0, 1, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("iris:test"), new short[]{0})
)
);
}
private static TerrainBoundarySignature biomeSignature(int blockX, int blockZ) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, 64, 63, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(0, 10, 3),
new TerrainBoundarySignature.BiomeEncoding(
List.of("iris:low", "iris:middle", "iris:high"),
new short[]{0, 1, 2})
)
);
}
private static TerrainBoundarySignature heightSignature(int blockX, int blockZ, int height) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, height, height - 1, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(0, 1, 1),
new TerrainBoundarySignature.BiomeEncoding(List.of("iris:test"), new short[]{0})
)
);
}
}
@@ -0,0 +1,212 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class WorldChunkInventoryTest {
private static final int SECTOR_BYTES = 4_096;
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void scanReadsAllocationTablesAcrossNegativeRegionBoundaries() throws Exception {
Path world = temporaryFolder.newFolder("world").toPath();
Path regionDirectory = Files.createDirectories(world.resolve("region"));
writeRegion(regionDirectory.resolve("r.-1.-1.mca"), new int[][]{{0, 0}, {31, 31}});
writeRegion(regionDirectory.resolve("r.0.0.mca"), new int[][]{{0, 0}, {31, 31}});
writeRegion(regionDirectory.resolve("r.1.-2.mca"), new int[][]{{0, 31}});
WorldChunkInventory inventory = WorldChunkInventory.scan(world);
assertEquals(5, inventory.size());
assertTrue(inventory.contains(-32, -32));
assertTrue(inventory.contains(-1, -1));
assertTrue(inventory.contains(0, 0));
assertTrue(inventory.contains(31, 31));
assertTrue(inventory.contains(32, -33));
assertFalse(inventory.contains(32, -32));
}
@Test
public void scanNeverReadsOrDecompressesAllocatedChunkPayloads() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-payload").toPath();
Path region = regionDirectory.resolve("r.0.0.mca");
writeRegion(region, new int[][]{{7, 11}});
try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) {
file.seek(SECTOR_BYTES * 2L);
file.writeInt(Integer.MAX_VALUE);
file.writeByte(127);
}
WorldChunkInventory inventory = WorldChunkInventory.scanRegionDirectory(regionDirectory);
assertEquals(1, inventory.size());
assertTrue(inventory.contains(7, 11));
}
@Test
public void inventoryRemainsUsableAfterTheRegionFilesDisappear() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-memory").toPath();
Path region = regionDirectory.resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
WorldChunkInventory inventory = WorldChunkInventory.scanRegionDirectory(regionDirectory);
Files.delete(region);
Files.delete(regionDirectory);
assertTrue(inventory.contains(1, 2));
assertEquals(1, inventory.size());
}
@Test
public void truncatedRegionFileFailsClosed() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-truncated").toPath();
Path region = regionDirectory.resolve("r.0.0.mca");
try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) {
file.setLength(SECTOR_BYTES);
}
IOException error = assertThrows(
IOException.class,
() -> WorldChunkInventory.scanRegionDirectory(regionDirectory)
);
assertTrue(error.getMessage().contains("Truncated world region file"));
}
@Test
public void allocationOutsideTheFileFailsClosed() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-outside").toPath();
Path region = regionDirectory.resolve("r.0.0.mca");
try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) {
file.setLength(SECTOR_BYTES * 2L);
writeLocation(file, 0, 0, 2, 1);
}
IOException error = assertThrows(
IOException.class,
() -> WorldChunkInventory.scanRegionDirectory(regionDirectory)
);
assertTrue(error.getMessage().contains("Invalid chunk allocation"));
}
@Test
public void overlappingAllocationsFailClosed() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-overlap").toPath();
Path region = regionDirectory.resolve("r.0.0.mca");
try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) {
file.setLength(SECTOR_BYTES * 3L);
writeLocation(file, 0, 0, 2, 1);
writeLocation(file, 1, 0, 2, 1);
}
IOException error = assertThrows(
IOException.class,
() -> WorldChunkInventory.scanRegionDirectory(regionDirectory)
);
assertTrue(error.getMessage().contains("Overlapping chunk allocations"));
}
@Test
public void malformedRegionFilenameFailsClosed() throws Exception {
Path regionDirectory = temporaryFolder.newFolder("region-name").toPath();
Path region = regionDirectory.resolve("not-a-region.mca");
try (RandomAccessFile file = new RandomAccessFile(region.toFile(), "rw")) {
file.setLength(SECTOR_BYTES * 2L);
}
IOException error = assertThrows(
IOException.class,
() -> WorldChunkInventory.scanRegionDirectory(regionDirectory)
);
assertTrue(error.getMessage().contains("Invalid world region filename"));
}
@Test
public void packedInventoryIsDeduplicatedAndDefensivelyCopied() throws Exception {
long first = ChunkGenerationOwnership.packChunk(-1, 2);
long second = ChunkGenerationOwnership.packChunk(3, -4);
long[] packed = new long[]{first, second, first};
WorldChunkInventory inventory = WorldChunkInventory.ofPackedChunks(packed);
packed[0] = 0L;
List<Long> visited = new ArrayList<>();
inventory.forEach((chunkX, chunkZ) -> visited.add(ChunkGenerationOwnership.packChunk(chunkX, chunkZ)));
assertEquals(2, inventory.size());
assertTrue(inventory.contains(-1, 2));
assertTrue(inventory.contains(3, -4));
assertEquals(List.of(first, second), visited);
}
@Test
public void largeInventoryRetainsOnlyRegionMasksAndStreamsOwnership() throws Exception {
Path world = temporaryFolder.newFolder("many-regions").toPath();
Path regions = Files.createDirectory(world.resolve("region"));
int[][] chunks = new int[1024][2];
for (int index = 0; index < chunks.length; index++) {
chunks[index][0] = index & 31;
chunks[index][1] = index >> 5;
}
for (int regionX = -48; regionX < 48; regionX++) {
writeRegion(regions.resolve("r." + regionX + ".-2.mca"), chunks);
}
WorldChunkInventory inventory = WorldChunkInventory.scan(world);
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(world.resolve("ownership"));
assertEquals(96 * 1024, inventory.size());
assertEquals(96 * 128, inventory.retainedAllocationBytes());
assertEquals(inventory.size(), ownership.assignUnassigned(inventory, 1L));
assertEquals(0, ownership.assignUnassigned(inventory, 2L));
inventory.forEach((chunkX, chunkZ) -> assertEquals(1L, ownership.resolve(chunkX, chunkZ, 2L)));
assertEquals(inventory.size(), ownership.explicitChunkCount());
}
@Test
public void missingRegionDirectoryProducesAnEmptyInventory() throws Exception {
Path missing = temporaryFolder.getRoot().toPath().resolve("missing");
WorldChunkInventory inventory = WorldChunkInventory.scanRegionDirectory(missing);
assertTrue(inventory.isEmpty());
}
private static void writeRegion(Path file, int[][] chunks) throws IOException {
try (RandomAccessFile output = new RandomAccessFile(file.toFile(), "rw")) {
output.setLength((long) (2 + chunks.length) * SECTOR_BYTES);
for (int index = 0; index < chunks.length; index++) {
writeLocation(output, chunks[index][0], chunks[index][1], 2 + index, 1);
}
}
}
private static void writeLocation(
RandomAccessFile file,
int localChunkX,
int localChunkZ,
int sectorOffset,
int sectorCount
) throws IOException {
int chunkIndex = MCAFile.getChunkIndex(localChunkX, localChunkZ);
file.seek((long) chunkIndex * Integer.BYTES);
file.writeInt(sectorOffset << Byte.SIZE | sectorCount);
}
}
@@ -668,15 +668,56 @@ public class HydrologyModelTest {
HydrologyColumnSample column = new HydrologyColumnSample(
1000, 0, 80, 63, false, "parent", List.of(layer)
);
HydrologyFeatureRef middle = new HydrologyFeatureRef(
203L,
HydrologyFeatureType.DEEP_CHANNEL,
course.id(),
segment.id(),
500,
20,
0,
1,
0,
false
);
HydrologyColumnLayer middleLayer = new HydrologyColumnLayer(
middle,
18,
20,
24,
true,
false,
false,
true,
false,
false,
true,
true,
false,
"acid",
"surface",
"mouth",
"shore",
"dry",
"flooded"
);
HydrologyColumnSample middleColumn = new HydrologyColumnSample(
500, 0, 80, 63, false, "parent", List.of(middleLayer)
);
HashMap<CavePosition, HydrologyCaveAction> actions = new HashMap<>();
HashMap<CavePosition, CaveVoxelPrecondition> preconditions = new HashMap<>();
for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) {
CavePosition position = new CavePosition(column.x(), y, column.z());
actions.put(
position,
y > layer.fluidHeadY() ? HydrologyCaveAction.DRY_AIR : HydrologyCaveAction.WET_SOURCE
);
preconditions.put(position, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
for (HydrologyColumnSample plannedColumn : List.of(column, middleColumn)) {
HydrologyColumnLayer plannedLayer = plannedColumn.layers().getFirst();
for (int y = plannedLayer.bedY() + 1; y <= plannedLayer.ceilingY(); y++) {
CavePosition position = new CavePosition(plannedColumn.x(), y, plannedColumn.z());
actions.put(
position,
y > plannedLayer.fluidHeadY()
? HydrologyCaveAction.DRY_AIR
: HydrologyCaveAction.WET_SOURCE
);
preconditions.put(position, new CaveVoxelPrecondition(CaveVoxel.SOLID, false));
}
}
HydrologyCavePlan plan = new HydrologyCavePlan(
new HydrologyCaveSource(
@@ -702,13 +743,24 @@ public class HydrologyModelTest {
List.of(course),
List.of(plan),
List.of(),
new RiverFootprint(Map.of(RiverFootprint.pack(1000, 0), column))
new RiverFootprint(Map.of(
RiverFootprint.pack(1000, 0), column,
RiverFootprint.pack(500, 0), middleColumn
))
);
assertEquals(endpoint, tile.nearestFeature(
Set.of(HydrologyFeatureType.DEEP_CHANNEL), "acid", 1000, 0, 10).orElseThrow());
assertTrue(tile.nearestFeature(
Set.of(HydrologyFeatureType.DEEP_CHANNEL), "deep_lava", 1000, 0, 10).isEmpty());
assertEquals(middle, tile.nearestFeature(
Set.of(HydrologyFeatureType.DEEP_CHANNEL),
"acid",
1000,
0,
1000,
feature -> feature.x() != 1000
).orElseThrow());
}
private HydrologyFeatureRef feature(HydrologyFeatureType type, long id, int y) {
@@ -2,6 +2,8 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.link.Identifier;
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.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.engine.object.IrisDimension;
@@ -43,6 +45,8 @@ public class MantleWriterOverlayTest {
private MatterSlice<MatterCavern> cavernSlice;
private MatterSlice<HydrologyCaveCell> hydrologySlice;
private IrisDimension dimension;
private EngineMantle engineMantle;
private Mantle<Matter> mantle;
@Before
@SuppressWarnings("unchecked")
@@ -55,10 +59,10 @@ public class MantleWriterOverlayTest {
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
EngineMantle engineMantle = mock(EngineMantle.class);
engineMantle = mock(EngineMantle.class);
Engine engine = mock(Engine.class);
dimension = mock(IrisDimension.class);
Mantle<Matter> mantle = mock(Mantle.class);
mantle = mock(Mantle.class);
chunk = mock(MantleChunk.class);
matter = mock(Matter.class);
blockSlice = mock(MatterSlice.class);
@@ -184,6 +188,29 @@ public class MantleWriterOverlayTest {
verify(cavernSlice, never()).set(X, Y, Z, cavern);
}
@Test
public void historicalChunkRejectsAllMutationKinds() {
writer.close();
IrisComplex complex = mock(IrisComplex.class);
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class);
when(engineMantle.getComplex()).thenReturn(complex);
when(complex.getTransitionGenerationPlan()).thenReturn(transition);
when(transition.isHistoricalBlock(X, Z)).thenReturn(true);
writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false);
PlatformBlockState replacement = mock(PlatformBlockState.class);
MatterCavern cavern = new MatterCavern(true, "", (byte) 3);
writer.setData(X, Y, Z, replacement);
assertFalse(writer.setDataIfAbsent(X, Y, Z, cavern));
assertFalse(writer.carveDataIfAbsent(X, Y, Z, cavern));
writer.setForcedCarve(X, Y, Z, cavern);
writer.clearBlock(X, Y, Z);
writer.clearData(X, Y, Z, PlatformBlockState.class);
verify(blockSlice, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(cavernSlice, never()).set(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void hydrologyOverridesBaselineCarvedQueriesWithoutChangingIt() {
MatterCavern baseline = new MatterCavern(true, "", (byte) 0);
@@ -3,12 +3,18 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.LinkedHashSet;
@@ -19,11 +25,34 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MatterGeneratorCarvePassRadiusTest {
private static IrisPlatform previousPlatform;
@BeforeClass
public static void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
PlatformBlockState air = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
IrisPlatform platform = mock(IrisPlatform.class);
when(registries.block(anyString())).thenReturn(air);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
@SuppressWarnings("unchecked")
public void platformFilterRemovesSkippedComponentFromGenerationReach() {
@@ -345,6 +374,9 @@ public class MatterGeneratorCarvePassRadiusTest {
private final List<MantlePass> components;
private TestMatterGenerator(Engine engine, Mantle<Matter> mantle, List<MantlePass> components) {
EngineMantle engineMantle = mock(EngineMantle.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getEngine()).thenReturn(engine);
this.engine = engine;
this.mantle = mantle;
this.components = components;
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
@@ -126,6 +127,25 @@ public class CaveObjectPlacementTransactionTest {
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void transitionIntersectionRejectsTheWholePlacement() {
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(128);
IrisComplex complex = mock(IrisComplex.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.allowsNewDiscreteContentAt(4, 7)).thenReturn(false);
IObjectPlacer delegate = mock(IObjectPlacer.class);
when(delegate.getEngine()).thenReturn(engine);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, mock(PlatformBlockState.class));
transaction.setData(4, 30, 7, "object@1");
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_TRANSITION, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
private IObjectPlacer createPlacer(int worldHeight, int surfaceHeight, int caveFloor, int caveCeiling) {
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(worldHeight);
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
@@ -47,6 +48,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -54,6 +56,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
public class MantleObjectComponentBoundaryRadiusTest {
@Rule
@@ -102,6 +105,22 @@ public class MantleObjectComponentBoundaryRadiusTest {
assertEquals(43, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement));
}
@Test
public void knownObjectFootprintMustClearTheTransitionBand() {
IrisComplex complex = mock(IrisComplex.class);
IrisObject object = new IrisObject(5, 4, 3);
IrisObjectPlacement placement = new IrisObjectPlacement();
when(complex.allowsNewGenerationFootprint(95, 195, 105, 205)).thenReturn(false);
assertFalse(MantleObjectComponent.allowsObjectPlacement(
complex,
object,
placement,
100,
200));
verify(complex).allowsNewGenerationFootprint(95, 195, 105, 205);
}
@Test
public void sourceReplayUsesAscendingSignedCoordinates() {
List<String> replayedSources = new ArrayList<>();
@@ -0,0 +1,91 @@
package art.arcane.iris.engine.platform;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public final class BukkitChunkGeneratorGenerationHistoryContractTest {
@Test
public void setupPromotesBeforePublicationAndRebuildsFullServicesFromTheNewPack() throws IOException {
String source = source();
String setup = method(source, "private void setupEngine()");
assertBefore(setup, "IrisEngine createdEngine = createEngine(engineTarget);",
"createdEngine.attachGenerationHistory(");
assertBefore(setup, "createdEngine.attachGenerationHistory(",
"generationHistory.activeActivation().activationId() != initialActivationId");
assertBefore(setup, "createdEngine.close();", "createEngine(loadActiveGenerationHistoryTarget())");
assertTrue(count(setup, "createdEngine.attachGenerationHistory(") == 2);
assertBefore(setup, "createEngine(loadActiveGenerationHistoryTarget())", "engine = createdEngine;");
assertBefore(setup, "IrisBoundarySignatureSampler.INSTANCE", "engine = createdEngine;");
assertTrue(setup.contains("getGenerationTransitionWidthBlocks()"));
}
@Test
public void historyEngineStartsWithTheActiveMantleKernelAndTransition() throws IOException {
String createEngine = method(source(), "private IrisEngine createEngine(EngineTarget engineTarget)");
assertTrue(createEngine.contains("generationHistory.paths().activationMantleRoot(active.activationId())"));
assertTrue(createEngine.contains("epoch.kernelVersion()"));
assertTrue(createEngine.contains("generationHistory.transitionPlan(active.activationId())"));
assertTrue(createEngine.contains("if (generationHistory == null)"));
}
@Test
public void publicBukkitTerrainClaimsSemanticsOnlyAfterSuccessfulApplication() throws IOException {
String generateNoise = method(
source(),
"public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d)"
);
assertBefore(generateNoise, "engine.generate(x << 4, z << 4", "blocks.apply()");
assertBefore(generateNoise, "blocks.apply()", "historyScope.claimGeneratedSemantics()");
}
private static String source() throws IOException {
return Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")))
.replace("\r\n", "\n");
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract token: " + signature, start >= 0);
int openingBrace = source.indexOf('{', start);
assertTrue(openingBrace >= 0);
int depth = 0;
for (int index = openingBrace; index < source.length(); index++) {
char character = source.charAt(index);
if (character == '{') {
depth++;
} else if (character == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new AssertionError("Method body did not close: " + signature);
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int count(String source, String token) {
int count = 0;
int index = 0;
while ((index = source.indexOf(token, index)) >= 0) {
count++;
index += token.length();
}
return count;
}
}