This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-10 04:05:24 -04:00
1 parent 6111b5670a
commit 402bcb04fe
112 files changed
+5610 -733

No files matched your search

@@ -0,0 +1,42 @@
package art.arcane.iris.core.nms.datapack.v1217;
import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class DataFixerV1217ClockTest {
@Test
public void overworldProvidesTheVanillaDefaultClock() {
JSONObject json = fixed(Dimension.OVERWORLD);
assertEquals("minecraft:overworld", json.getString("default_clock"));
assertFalse(json.optBoolean("has_fixed_time", false));
}
@Test
public void endProvidesItsFixedVanillaClock() {
JSONObject json = fixed(Dimension.END);
assertEquals("minecraft:the_end", json.getString("default_clock"));
assertTrue(json.getBoolean("has_fixed_time"));
}
@Test
public void netherRemainsFixedWithoutADefaultClock() {
JSONObject json = fixed(Dimension.NETHER);
assertFalse(json.has("default_clock"));
assertTrue(json.getBoolean("has_fixed_time"));
}
private JSONObject fixed(Dimension dimension) {
DataFixerV1217 fixer = new DataFixerV1217();
JSONObject json = fixer.resolve(dimension, null);
fixer.fixDimension(dimension, json);
return json;
}
}
@@ -0,0 +1,58 @@
package art.arcane.iris.core.pack;
import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class PackDirectoryResolverTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void resolvesOnlyExistingDirectChildren() throws Exception {
File packs = temporaryFolder.newFolder("packs");
File overworld = new File(packs, "overworld");
Files.createDirectory(overworld.toPath());
assertEquals(overworld.getAbsoluteFile(), PackDirectoryResolver.resolveExisting(packs, "overworld"));
assertEquals(overworld.getAbsoluteFile(), PackDirectoryResolver.resolveExisting(packs, "./overworld"));
assertNull(PackDirectoryResolver.resolveExisting(packs, "missing"));
assertNull(PackDirectoryResolver.resolveExisting(packs, ""));
}
@Test
public void rejectsTraversalAbsoluteAndNestedPaths() throws Exception {
File packs = temporaryFolder.newFolder("pack-root");
File outside = temporaryFolder.newFolder("outside");
File nested = new File(packs, "nested/pack");
Files.createDirectories(nested.toPath());
assertNull(PackDirectoryResolver.resolveExisting(packs, "../outside"));
assertNull(PackDirectoryResolver.resolveExisting(packs, outside.getAbsolutePath()));
assertNull(PackDirectoryResolver.resolveExisting(packs, "nested/pack"));
assertNull(PackDirectoryResolver.resolveExisting(packs, "."));
}
@Test
public void rejectsSymbolicLinkChildren() throws Exception {
File packs = temporaryFolder.newFolder("symlink-root");
File outside = temporaryFolder.newFolder("symlink-target");
Path link = new File(packs, "linked").toPath();
try {
Files.createSymbolicLink(link, outside.toPath());
} catch (IOException | UnsupportedOperationException | SecurityException e) {
Assume.assumeNoException(e);
}
assertNull(PackDirectoryResolver.resolveExisting(packs, "linked"));
}
}
@@ -0,0 +1,67 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pack;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class PackDownloaderTest {
@Test
public void resolvesBranchReference() {
assertEquals(
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/heads/feature/release",
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "feature/release")
);
}
@Test
public void resolvesQualifiedHeadReference() {
assertEquals(
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/heads/master",
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/heads/master")
);
}
@Test
public void resolvesTagReference() {
assertEquals(
"https://codeload.github.com/IrisDimensions/overworld/zip/refs/tags/v4.0.0",
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/tags/v4.0.0")
);
}
@Test
public void resolvesCommitReference() {
assertEquals(
"https://github.com/IrisDimensions/overworld/archive/8e32852ee6ecd039fae27a36f701f57cdc02e83f.zip",
PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "8e32852ee6ecd039fae27a36f701f57cdc02e83f")
);
}
@Test
public void rejectsUnsafeRepositoryAndReference() {
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld?raw=1", "master"));
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("../overworld", "master"));
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/heads/../master"));
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", "refs/pull/123/head"));
assertThrows(IllegalArgumentException.class, () -> PackDownloader.resolveGithubArchiveUrl("IrisDimensions/overworld", ""));
}
}
@@ -0,0 +1,250 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
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;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
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.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PackResourceCleanupTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void previewIsReadOnlyAndReturnsSortedCandidates() throws Exception {
File pack = createPack("preview");
write(pack, "dimensions/main.json", "{\"biome\":\"nested/used\"}");
write(pack, "biomes/z-last.json", "{}");
write(pack, "biomes/a-first.json", "{}");
write(pack, "biomes/nested/used.json", "{}");
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(pack);
assertTrue(preview.success());
assertTrue(preview.hasCandidates());
assertEquals(List.of("biomes/a-first.json", "biomes/z-last.json"), preview.candidatePaths());
assertFalse(new File(pack, ".iris-trash").exists());
assertTrue(new File(pack, "biomes/a-first.json").isFile());
}
@Test
public void applyRunsFreshScanBeforeMutating() throws Exception {
File pack = createPack("fresh-scan");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/kept.json", "{}");
PackResourceCleanup.Preview preview = PackResourceCleanup.preview(pack);
assertEquals(List.of("biomes/kept.json"), preview.candidatePaths());
write(pack, "dimensions/main.json", "{\"biome\":\"kept\"}");
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(pack);
assertTrue(result.success());
assertFalse(result.changed());
assertNull(result.quarantinePath());
assertTrue(new File(pack, "biomes/kept.json").isFile());
assertFalse(new File(pack, ".iris-trash").exists());
}
@Test
public void applyCreatesUniqueDumpsAndPreservesRelativePaths() throws Exception {
File pack = createPack("unique-dumps");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/nested/first.json", "{\"value\":1}");
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
assertTrue(first.success());
assertEquals(List.of("biomes/nested/first.json"), first.quarantinedPaths());
assertTrue(new File(pack, first.quarantinePath() + "/biomes/nested/first.json").isFile());
write(pack, "biomes/nested/second.json", "{\"value\":2}");
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
assertTrue(second.success());
assertNotEquals(first.quarantinePath(), second.quarantinePath());
assertTrue(new File(pack, first.quarantinePath() + "/biomes/nested/first.json").isFile());
assertTrue(new File(pack, second.quarantinePath() + "/biomes/nested/second.json").isFile());
}
@Test
public void restorePreviewReportsLatestDumpAndAllConflictsWithoutMutation() throws Exception {
File pack = createPack("restore-conflict");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/first.json", "{\"value\":1}");
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
write(pack, "biomes/second.json", "{\"value\":2}");
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
write(pack, "biomes/second.json", "{\"replacement\":true}");
PackResourceCleanup.RestorePreview preview = PackResourceCleanup.previewRestore(pack);
assertTrue(preview.success());
assertEquals(second.quarantinePath(), preview.dumpPath());
assertEquals(List.of("biomes/second.json"), preview.filePaths());
assertEquals(List.of("biomes/second.json"), preview.conflicts());
assertTrue(preview.hasConflicts());
assertFalse(preview.canRestore());
assertTrue(new File(pack, second.quarantinePath() + "/biomes/second.json").isFile());
assertTrue(new File(pack, first.quarantinePath() + "/biomes/first.json").isFile());
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
assertFalse(result.success());
assertEquals(List.of("biomes/second.json"), result.conflicts());
assertTrue(new File(pack, second.quarantinePath() + "/biomes/second.json").isFile());
assertEquals("{\"replacement\":true}", read(pack, "biomes/second.json"));
}
@Test
public void restoreLatestRestoresOnlyNewestDump() throws Exception {
File pack = createPack("restore-latest");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/first.json", "{\"value\":1}");
PackResourceCleanup.ApplyResult first = PackResourceCleanup.apply(pack);
write(pack, "biomes/second.json", "{\"value\":2}");
PackResourceCleanup.ApplyResult second = PackResourceCleanup.apply(pack);
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
assertTrue(result.success());
assertTrue(result.changed());
assertEquals(second.quarantinePath(), result.dumpPath());
assertEquals(List.of("biomes/second.json"), result.restoredPaths());
assertEquals("{\"value\":2}", read(pack, "biomes/second.json"));
assertFalse(new File(pack, "biomes/first.json").exists());
assertTrue(new File(pack, first.quarantinePath() + "/biomes/first.json").isFile());
assertFalse(new File(pack, second.quarantinePath()).exists());
}
@Test
public void restoreCopyFailureRollsBackCreatedDestinations() throws Exception {
File pack = createPack("restore-rollback");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/a-readable.json", "{\"value\":1}");
write(pack, "biomes/z-unreadable.json", "{\"value\":2}");
PackResourceCleanup.ApplyResult applied = PackResourceCleanup.apply(pack);
Path unreadable = new File(pack, applied.quarantinePath() + "/biomes/z-unreadable.json").toPath();
Assume.assumeTrue(Files.getFileStore(unreadable).supportsFileAttributeView("posix"));
Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(unreadable);
Files.setPosixFilePermissions(unreadable, Set.of(PosixFilePermission.OWNER_WRITE));
try {
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
assertFalse(result.success());
assertTrue(result.error().contains("rolled back"));
assertFalse(new File(pack, "biomes/a-readable.json").exists());
assertFalse(new File(pack, "biomes/z-unreadable.json").exists());
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/a-readable.json").isFile());
assertTrue(Files.exists(unreadable));
} finally {
Files.setPosixFilePermissions(unreadable, originalPermissions);
}
}
@Test
public void restoreSourceRemovalFailureRollsBackDestinationsAndPreservesDump() throws Exception {
File pack = createPack("restore-delete-rollback");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/a.json", "{\"value\":1}");
write(pack, "biomes/b.json", "{\"value\":2}");
PackResourceCleanup.ApplyResult applied = PackResourceCleanup.apply(pack);
Path quarantineBiomes = new File(pack, applied.quarantinePath() + "/biomes").toPath();
Assume.assumeTrue(Files.getFileStore(quarantineBiomes).supportsFileAttributeView("posix"));
Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(quarantineBiomes);
Files.setPosixFilePermissions(quarantineBiomes, Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_EXECUTE));
try {
PackResourceCleanup.RestoreResult result = PackResourceCleanup.restoreLatest(pack);
assertFalse(result.success());
assertTrue(result.error().contains("source removal failed"));
assertFalse(new File(pack, "biomes/a.json").exists());
assertFalse(new File(pack, "biomes/b.json").exists());
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/a.json").isFile());
assertTrue(new File(pack, applied.quarantinePath() + "/biomes/b.json").isFile());
} finally {
Files.setPosixFilePermissions(quarantineBiomes, originalPermissions);
}
}
@Test
public void cleanupFailsClosedWhenManagedContentContainsSymbolicLink() throws Exception {
File pack = createPack("symlink");
write(pack, "dimensions/main.json", "{}");
File outside = temporaryFolder.newFile("outside.json");
Path link = new File(pack, "biomes/linked.json").toPath();
Files.createDirectories(link.getParent());
try {
Files.createSymbolicLink(link, outside.toPath());
} catch (IOException | UnsupportedOperationException | SecurityException e) {
Assume.assumeNoException(e);
}
PackResourceCleanup.ApplyResult result = PackResourceCleanup.apply(pack);
assertFalse(result.success());
assertFalse(result.changed());
assertTrue(Files.isSymbolicLink(link));
assertFalse(new File(pack, ".iris-trash").exists());
}
@Test
public void concurrentAppliesSerializePerPack() throws Exception {
File pack = createPack("concurrent");
write(pack, "dimensions/main.json", "{}");
write(pack, "biomes/unused.json", "{}");
CountDownLatch start = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<PackResourceCleanup.ApplyResult> first = executor.submit(() -> {
start.await();
return PackResourceCleanup.apply(pack);
});
Future<PackResourceCleanup.ApplyResult> second = executor.submit(() -> {
start.await();
return PackResourceCleanup.apply(pack);
});
start.countDown();
List<PackResourceCleanup.ApplyResult> results = List.of(first.get(), second.get());
assertTrue(results.stream().allMatch(PackResourceCleanup.ApplyResult::success));
assertEquals(1L, results.stream().filter(PackResourceCleanup.ApplyResult::changed).count());
assertFalse(new File(pack, "biomes/unused.json").exists());
} finally {
executor.shutdownNow();
}
}
private File createPack(String name) throws Exception {
return temporaryFolder.newFolder(name);
}
private void write(File pack, String relativePath, String content) throws Exception {
Path path = new File(pack, relativePath).toPath();
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private String read(File pack, String relativePath) throws Exception {
return Files.readString(new File(pack, relativePath).toPath(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,179 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PackValidatorCustomBiomeSpawnTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void acceptsExplicitMatchingSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
assertTrue(errors.isEmpty());
}
@Test
public void acceptsBiomeWithoutCustomSpawns() throws Exception {
File biomes = createBiomes("{\"name\":\"Plains\"}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsMissingSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("must declare group 'MONSTER'"));
}
@Test
public void acceptsImplicitMiscSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"effects\",\"spawns\":[{\"type\":\"minecraft:armor_stand\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("misc"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsSpawnGroupThatDisagreesWithRegistry() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MISC\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("live entity registry requires 'MONSTER'"));
}
@Test
public void acceptsAxolotlSpawnCategory() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"cave\",\"spawns\":[{\"type\":\"minecraft:axolotl\",\"group\":\"AXOLOTLS\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("axolotls"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsUnknownSpawnEntity() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"missing:entity\",\"group\":\"MISC\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.unknown());
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("unknown entity type 'missing:entity'"));
}
@Test
public void rejectsCaseNormalizedGroupThatRuntimeWouldNotParse() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"monster\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(
biomes, key -> PackValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("unknown group 'monster'"));
}
@Test
public void rejectsWrongCustomDerivativeContainerType() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":{}}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("customDerivitives must be an array"));
}
@Test
public void rejectsWrongSpawnContainerType() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":{}}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("spawns must be an array"));
}
@Test
public void acceptsNullCustomDerivativeContainerAsAbsent() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":null}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void acceptsNullSpawnContainerAsAbsent() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":null}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void acceptsNamespacedCustomBiomeTags() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:allows_surface_slime_spawns\"]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void rejectsUnsafeCustomBiomeTags() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:../outside\"]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("invalid tag"));
}
@Test
public void convertsSpawnCategoryResolverFailureIntoBlockingError() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
List<String> errors = PackValidator.validateCustomBiomeSpawns(biomes, key -> {
throw new IllegalStateException("registry unavailable");
});
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("spawn category lookup failed"));
assertTrue(errors.get(0).contains("registry unavailable"));
}
private File createBiomes(String json) throws Exception {
File biomes = temporaryFolder.newFolder("biomes");
File biome = new File(biomes, "test.json");
Files.writeString(biome.toPath(), json, StandardCharsets.UTF_8);
return biomes;
}
}
@@ -0,0 +1,59 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
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.assertTrue;
public class PackValidatorReadOnlyTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void validationDoesNotMoveOrRewritePackFiles() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
write(pack, "generators/unused.json", "{\"name\":\"Unused\"}");
Map<String, byte[]> before = snapshot(pack.toPath());
PackValidationResult result = PackValidator.validate(pack);
assertTrue(result.isLoadable());
assertEquals(before.keySet(), snapshot(pack.toPath()).keySet());
Map<String, byte[]> after = snapshot(pack.toPath());
for (Map.Entry<String, byte[]> entry : before.entrySet()) {
assertArrayEquals(entry.getValue(), after.get(entry.getKey()));
}
assertFalse(new File(pack, ".iris-trash").exists());
}
private void write(File root, String relative, String content) throws Exception {
Path path = root.toPath().resolve(relative);
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private Map<String, byte[]> snapshot(Path root) throws Exception {
Map<String, byte[]> files = new LinkedHashMap<>();
try (Stream<Path> stream = Files.walk(root)) {
for (Path path : stream.filter(Files::isRegularFile).sorted().toList()) {
files.put(root.relativize(path).toString(), Files.readAllBytes(path));
}
}
return files;
}
}
@@ -0,0 +1,122 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PackValidatorSpawnerEntityTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void acceptsNestedEntitiesReferencedByBothSpawnLists() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/frozen/passive.json", """
{
"spawns": [{"entity": "standard/passive/cod"}],
"initialSpawns": [{"entity": "standard/passive/camel"}]
}
""");
write(pack, "entities/standard/passive/cod.json", "{\"type\":\"COD\",\"surface\":\"WATER\"}");
write(pack, "entities/standard/passive/camel.json", "{\"type\":\"CAMEL\"}");
List<String> errors = validate(pack);
assertTrue(errors.isEmpty());
}
@Test
public void rejectsMalformedContainersAndEntries() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/broken.json", """
{
"spawns": null,
"initialSpawns": [7, {}, {"entity": 9}, {"entity": " "}]
}
""");
List<String> errors = validate(pack);
assertEquals(5, errors.size());
assertTrue(errors.stream().anyMatch(error -> error.contains("spawns must be an array")));
assertTrue(errors.stream().anyMatch(error -> error.contains("non-object entry")));
assertTrue(errors.stream().anyMatch(error -> error.contains("without an entity reference")));
assertTrue(errors.stream().anyMatch(error -> error.contains("must be a string")));
assertTrue(errors.stream().anyMatch(error -> error.contains("blank entity reference")));
}
@Test
public void rejectsMissingAndMalformedReferencedEntities() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/broken.json", """
{
"spawns": [
{"entity": "standard/hostile/missing"},
{"entity": "standard/hostile/broken"}
]
}
""");
write(pack, "entities/standard/hostile/broken.json", "not-json");
List<String> errors = validate(pack);
assertEquals(2, errors.size());
assertTrue(errors.stream().anyMatch(error -> error.contains("references missing entity 'standard/hostile/missing'")));
assertTrue(errors.stream().anyMatch(error -> error.contains("references malformed entity 'standard/hostile/broken'")));
}
@Test
public void rejectsMalformedSpawnerJson() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/broken.json", "not-json");
List<String> errors = validate(pack);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("Spawner 'broken' has invalid JSON"));
}
@Test
public void rejectsEntityReferenceThatEscapesPack() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/broken.json", "{\"spawns\":[{\"entity\":\"../outside\"}]}");
write(pack, "outside.json", "{\"type\":\"COW\"}");
List<String> errors = validate(pack);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("unsafe entity reference '../outside'"));
}
@Test
public void ignoresMalformedEntityThatNoSpawnerReferences() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "spawners/passive.json", "{\"spawns\":[{\"entity\":\"standard/passive/cow\"}]}");
write(pack, "entities/standard/passive/cow.json", "{\"type\":\"COW\"}");
write(pack, "entities/unique/unused.json", "not-json");
List<String> errors = validate(pack);
assertTrue(errors.isEmpty());
}
private List<String> validate(File pack) {
return PackValidator.validateSpawnerEntityReferences(
new File(pack, "spawners"), new File(pack, "entities"));
}
private void write(File root, String relative, String content) throws Exception {
Path path = root.toPath().resolve(relative);
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,64 @@
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackValidatorStructureTransformTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void reportsUnsupportedTransformsAcrossEveryStructureHost() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"test\"],\"rotation\":{}}]}");
write(pack, "regions/nested/region.json", "{\"structures\":[{\"structures\":[\"test\"],\"translate\":{}}]}");
write(pack, "biomes/nested/biome.json", "{\"structures\":[{\"structures\":[\"test\"],\"scale\":{}}]}");
List<String> errors = PackValidator.validateUnsupportedStructureTransforms(pack);
assertEquals(List.of(
"Dimension 'main' structures[0] declares unsupported field 'rotation'. Structure placement transforms are not supported; remove the field.",
"Region 'nested/region' structures[0] declares unsupported field 'translate'. Structure placement transforms are not supported; remove the field.",
"Biome 'nested/biome' structures[0] declares unsupported field 'scale'. Structure placement transforms are not supported; remove the field."
), errors);
}
@Test
public void ignoresTransformsOutsideStructurePlacementArrays() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/biome.json", "{\"objects\":[{\"rotation\":{},\"translate\":{},\"scale\":{}}],\"structures\":[{\"structures\":[\"test\"]}]}");
assertTrue(PackValidator.validateUnsupportedStructureTransforms(pack).isEmpty());
}
@Test
public void unsupportedTransformBlocksFullPackValidation() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"],\"structures\":[{\"structures\":[\"test\"],\"rotation\":null}]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
PackValidationResult result = PackValidator.validate(pack);
assertFalse(result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Dimension 'main' structures[0] declares unsupported field 'rotation'. Structure placement transforms are not supported; remove the field."));
}
private void write(File root, String relative, String content) throws Exception {
Path path = root.toPath().resolve(relative);
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
}
}
@@ -10,8 +10,10 @@ import java.io.PrintStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisPregeneratorInitTest {
@@ -53,6 +55,35 @@ public class IrisPregeneratorInitTest {
assertTrue(output.contains("Pregen finished: generated=8 total=9 failed=1"));
}
@Test
public void cancellationSummaryDoesNotReportPartialRunAsFinished() {
IrisSettings previousSettings = IrisSettings.settings;
PrintStream previousOut = System.out;
IrisSettings.settings = new IrisSettings();
ByteArrayOutputStream output = new ByteArrayOutputStream();
System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8));
AtomicReference<IrisPregenerator> pregeneratorReference = new AtomicReference<>();
CancelAfterFirstChunkMethod method = new CancelAfterFirstChunkMethod(pregeneratorReference);
PregenTask task = PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(1)
.radiusZ(1)
.build();
try {
IrisPregenerator pregenerator = new IrisPregenerator(task, method, new NoOpPregenListener());
pregeneratorReference.set(pregenerator);
pregenerator.start();
String summary = output.toString(StandardCharsets.UTF_8);
assertTrue(summary.contains("Pregen cancelled: generated=1 total=9 failed=0 remaining=8"));
assertFalse(summary.contains("Pregen finished:"));
} finally {
System.setOut(previousOut);
IrisSettings.settings = previousSettings;
}
}
private String runCompletionOnClose(boolean failLast) {
IrisSettings previousSettings = IrisSettings.settings;
PrintStream previousOut = System.out;
@@ -173,6 +204,55 @@ public class IrisPregeneratorInitTest {
}
}
private static final class CancelAfterFirstChunkMethod implements PregeneratorMethod {
private final AtomicReference<IrisPregenerator> pregeneratorReference;
private boolean cancelled;
private CancelAfterFirstChunkMethod(AtomicReference<IrisPregenerator> pregeneratorReference) {
this.pregeneratorReference = pregeneratorReference;
}
@Override
public void init() {
}
@Override
public void close() {
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "test";
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerated(x, z);
if (!cancelled) {
cancelled = true;
pregeneratorReference.get().close();
}
}
@Override
public Mantle getMantle() {
return null;
}
}
private static final class NoOpPregenListener implements PregenListener {
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
@@ -0,0 +1,33 @@
package art.arcane.iris.core.project;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.volmlib.util.collection.KSet;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisProjectEntityDependencyTest {
@Test
public void collectsInitialOnlyEntityAlongsideNormalSpawnEntityForExport() {
IrisSpawner spawner = new IrisSpawner();
spawner.getSpawns().add(new IrisEntitySpawn().setEntity("standard/passive/cow"));
spawner.getInitialSpawns().add(new IrisEntitySpawn().setEntity("beta/sentinel"));
KSet<IrisSpawner> spawners = new KSet<>();
spawners.add(spawner);
KSet<String> entityKeys = IrisProject.collectSpawnerEntityKeys(spawners);
assertEquals(2, entityKeys.size());
assertTrue(entityKeys.contains("standard/passive/cow"));
assertTrue(entityKeys.contains("beta/sentinel"));
}
@Test
public void returnsNoEntityDependenciesForEmptySpawnerSet() {
KSet<String> entityKeys = IrisProject.collectSpawnerEntityKeys(new KSet<>());
assertTrue(entityKeys.isEmpty());
}
}
@@ -19,6 +19,10 @@
package art.arcane.iris.core.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
@@ -50,6 +54,10 @@ import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SchemaBuilderParityTest {
private static final List<String> POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
@@ -108,6 +116,26 @@ public class SchemaBuilderParityTest {
assertEquals(List.of("ALPHA", "BETA"), enumValues(schema.getJSONObject("definitions"), flavorDefinitionKey()));
}
@Test
public void structurePlacementSchemaOmitsUnsupportedTransforms() {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
when(data.getStructureLoader()).thenReturn(structureLoader);
when(data.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(structureLoader.getPossibleKeys()).thenReturn(new String[0]);
when(pieceLoader.getPossibleKeys()).thenReturn(new String[0]);
JSONObject properties = new SchemaBuilder(IrisStructurePlacement.class, data)
.construct().getJSONObject("properties");
assertTrue(properties.has("structures"));
assertTrue(properties.has("distribution"));
assertFalse(properties.has("rotation"));
assertFalse(properties.has("translate"));
assertFalse(properties.has("scale"));
}
private static String flavorDefinitionKey() {
return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
}
@@ -0,0 +1,37 @@
package art.arcane.iris.core.tools;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class IrisToolbeltPackReferenceTest {
@Test
public void plainPackUsesMatchingDimensionKey() {
IrisToolbelt.PackReference reference = IrisToolbelt.parsePackReference("overworld");
assertEquals("overworld", reference.pack());
assertEquals("overworld", reference.dimension());
assertFalse(reference.explicitDimension());
}
@Test
public void explicitDimensionKeepsPackAndDimensionSeparate() {
IrisToolbelt.PackReference reference = IrisToolbelt.parsePackReference(" custom_pack : dimensions/sky ");
assertEquals("custom_pack", reference.pack());
assertEquals("dimensions/sky", reference.dimension());
assertTrue(reference.explicitDimension());
}
@Test
public void malformedReferencesAreRejected() {
assertNull(IrisToolbelt.parsePackReference(null));
assertNull(IrisToolbelt.parsePackReference(""));
assertNull(IrisToolbelt.parsePackReference(":"));
assertNull(IrisToolbelt.parsePackReference("pack:"));
assertNull(IrisToolbelt.parsePackReference(":dimension"));
}
}
@@ -0,0 +1,126 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds;
import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBoundsProvider;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
public class IrisComplexGridBoundsCacheTest {
@Test
public void gridBoundsCacheIsIsolatedPerComplex() throws Exception {
IrisComplex first = createComplex();
IrisComplex second = createComplex();
Method cornerBounds = cornerBoundsMethod();
long firstPacked = invokeCornerBounds(first, cornerBounds, new CountingInterpolator(1.25D, 2.5D), 64, -32);
long secondPacked = invokeCornerBounds(second, cornerBounds, new CountingInterpolator(10.25D, 20.5D), 64, -32);
assertNotEquals(firstPacked, secondPacked);
assertEquals(1.25F, unpackLow(firstPacked), 0D);
assertEquals(2.5F, unpackHigh(firstPacked), 0D);
assertEquals(10.25F, unpackLow(secondPacked), 0D);
assertEquals(20.5F, unpackHigh(secondPacked), 0D);
}
@Test
public void gridBoundsCacheReusesCornersWithinSameComplex() throws Exception {
IrisComplex complex = createComplex();
Method cornerBounds = cornerBoundsMethod();
CountingInterpolator interpolator = new CountingInterpolator(3.5D, 7.25D);
long firstPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
long secondPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
assertEquals(firstPacked, secondPacked);
assertEquals(1, interpolator.getInvocations());
}
private IrisComplex createComplex() throws Exception {
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
Field generatorBounds = IrisComplex.class.getDeclaredField("generatorBounds");
generatorBounds.setAccessible(true);
generatorBounds.set(complex, new HashMap<>());
Class<?> cacheClass = Class.forName("art.arcane.iris.engine.IrisComplex$GridBoundsCache");
Constructor<?> cacheConstructor = cacheClass.getDeclaredConstructor();
cacheConstructor.setAccessible(true);
ThreadLocal<Object> cache = ThreadLocal.withInitial(() -> newCache(cacheConstructor));
Field gridBoundsCache = IrisComplex.class.getDeclaredField("gridBoundsCache");
gridBoundsCache.setAccessible(true);
gridBoundsCache.set(complex, cache);
return complex;
}
private Object newCache(Constructor<?> constructor) {
try {
return constructor.newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}
private Method cornerBoundsMethod() throws Exception {
Class<?> cacheClass = Class.forName("art.arcane.iris.engine.IrisComplex$GridBoundsCache");
Method method = IrisComplex.class.getDeclaredMethod(
"cornerBounds",
cacheClass,
Engine.class,
IrisInterpolator.class,
int.class,
Set.class,
int.class,
int.class
);
method.setAccessible(true);
return method;
}
private long invokeCornerBounds(IrisComplex complex, Method method, IrisInterpolator interpolator, int x, int z) throws Exception {
Field gridBoundsCache = IrisComplex.class.getDeclaredField("gridBoundsCache");
gridBoundsCache.setAccessible(true);
ThreadLocal<?> cache = (ThreadLocal<?>) gridBoundsCache.get(complex);
return (long) method.invoke(complex, cache.get(), null, interpolator, 0, Set.<IrisGenerator>of(), x, z);
}
private float unpackLow(long packed) {
return Float.intBitsToFloat((int) (packed >>> 32));
}
private float unpackHigh(long packed) {
return Float.intBitsToFloat((int) packed);
}
private static final class CountingInterpolator extends IrisInterpolator {
private final NoiseBounds bounds;
private final AtomicInteger invocations = new AtomicInteger();
private CountingInterpolator(double low, double high) {
bounds = new NoiseBounds(low, high);
}
@Override
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
invocations.incrementAndGet();
return bounds;
}
private int getInvocations() {
return invocations.get();
}
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.engine.actuator;
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.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.After;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
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.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeActuatorCoordinateTest {
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
@SuppressWarnings("unchecked")
public void derivativeScatterUsesEachWorldColumnCoordinate() {
bindPlatform();
Engine engine = engine();
IrisBiomeActuator actuator = new IrisBiomeActuator(engine);
Hunk<PlatformBiome> output = mock(Hunk.class);
when(output.getWidth()).thenReturn(2);
when(output.getDepth()).thenReturn(2);
when(output.getHeight()).thenReturn(1);
List<String> samples = new ArrayList<>();
IrisBiome biome = mock(IrisBiome.class);
when(biome.getSkyBiomeKey(any(RNG.class), same(engine), anyDouble(), anyDouble(), anyDouble()))
.thenAnswer(invocation -> {
double worldX = invocation.getArgument(2);
double worldZ = invocation.getArgument(4);
samples.add((int) worldX + "," + (int) worldZ);
return "minecraft:plains";
});
ChunkedDataCache<IrisBiome> biomeCache = mock(ChunkedDataCache.class);
when(biomeCache.get(anyInt(), anyInt())).thenReturn(biome);
ChunkContext context = mock(ChunkContext.class);
when(context.getBiome()).thenReturn(biomeCache);
actuator.onActuate(100, -200, output, false, context);
assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples);
}
@SuppressWarnings("unchecked")
private Engine engine() {
Mantle<Matter> mantle = mock(Mantle.class);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getMantle()).thenReturn(mantle);
SeedManager seedManager = mock(SeedManager.class);
when(seedManager.getBiome()).thenReturn(1337L);
Engine engine = mock(Engine.class);
when(engine.getCacheID()).thenReturn(1);
when(engine.getSeedManager()).thenReturn(seedManager);
when(engine.getMantle()).thenReturn(engineMantle);
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
return engine;
}
private void bindPlatform() {
IrisPlatforms.unbind();
PlatformBiome biome = mock(PlatformBiome.class);
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.biome(anyString())).thenReturn(biome);
when(registries.block(anyString())).thenReturn(block);
PlatformBiomeWriter biomeWriter = mock(PlatformBiomeWriter.class);
when(biomeWriter.biomeIdFor(anyString())).thenReturn(1);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
when(platform.biomeWriter()).thenReturn(biomeWriter);
IrisPlatforms.bind(platform);
}
}
@@ -0,0 +1,70 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisDecoratorCaveContextTest {
@Test
@SuppressWarnings("unchecked")
public void explicitCaveContextSkipsFluidWithoutMutatingBiome() {
Engine engine = mock(Engine.class);
SeedManager seedManager = mock(SeedManager.class);
doReturn(seedManager).when(engine).getSeedManager();
doReturn(mock(IrisData.class)).when(engine).getData();
IrisDimension dimension = mock(IrisDimension.class);
doReturn(63).when(dimension).getFluidHeight();
doReturn(dimension).when(engine).getDimension();
IrisDecorator decorator = mock(IrisDecorator.class);
doReturn(true).when(decorator).passesChanceGate(any(), anyDouble(), anyDouble(), any());
doReturn(true).when(decorator).isStacking();
doReturn(true).when(decorator).isForcePlace();
doReturn(1).when(decorator).getHeight(any(), anyDouble(), anyDouble(), any());
IrisBiome biome = mock(IrisBiome.class);
doReturn(InferredType.LAND).when(biome).getInferredType();
doReturn(new IrisDecorator[]{decorator}).when(biome).getDecoratorBucket(IrisDecorationPart.NONE);
doReturn(new IrisDecorator[]{decorator}).when(biome).getDecoratorBucket(IrisDecorationPart.CEILING);
PlatformBlockState fluid = mock(PlatformBlockState.class);
doReturn(true).when(fluid).isFluid();
Hunk<PlatformBlockState> output = mock(Hunk.class);
doReturn(128).when(output).getHeight();
doReturn(fluid).when(output).get(0, 10, 0);
IrisSurfaceDecorator surfaceDecorator = new IrisSurfaceDecorator(engine);
surfaceDecorator.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, InferredType.CAVE, 10, 10);
DecoratorCore.PlaceOpts surfaceOptions = DecoratorCore.SCRATCH_OPTS.get();
assertTrue(surfaceOptions.caveSkipFluid);
assertFalse(surfaceOptions.underwater);
IrisCeilingDecorator ceilingDecorator = new IrisCeilingDecorator(engine);
ceilingDecorator.decorate(0, 0, 0, 0, 0, 0, 0, 0, output, biome, InferredType.CAVE, 10, 10);
DecoratorCore.PlaceOpts ceilingOptions = DecoratorCore.SCRATCH_OPTS.get();
assertTrue(ceilingOptions.caveSkipFluid);
assertEquals(InferredType.LAND, biome.getInferredType());
}
@Test
public void nullInferenceRemainsSafeForNormalSurfaceContext() {
assertTrue(IrisSurfaceDecorator.isUnderwater(null, 10, 63));
assertFalse(IrisSurfaceDecorator.skipsFluid(null));
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class StructurePlacementMarkerTest {
@Test
public void structureMarkerRoundTripsDelimiterSafeKeys() {
String objectKey = "pieces/room@east:alpha/樹";
String structureKey = "structures/village@night:beta/城";
String encoded = StructurePlacementMarker.encodeStructure(objectKey, 918273, structureKey);
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode(encoded);
assertTrue(encoded.startsWith("@iris-structure:v1:"));
assertEquals(objectKey, decoded.objectKey());
assertEquals(918273, decoded.placementId());
assertEquals(structureKey, decoded.structureKey());
assertTrue(decoded.structureAware());
}
@Test
public void legacyMarkerDecodesWithoutChangingFields() {
StructurePlacementMarker.Decoded decoded = StructurePlacementMarker.decode("objects/oak_tree@42");
assertEquals("objects/oak_tree", decoded.objectKey());
assertEquals(42, decoded.placementId());
assertNull(decoded.structureKey());
assertFalse(decoded.structureAware());
}
@Test
public void malformedAndUnknownMarkersFailSafely() {
assertNull(StructurePlacementMarker.decode(null));
assertNull(StructurePlacementMarker.decode(""));
assertNull(StructurePlacementMarker.decode("objects/oak_tree"));
assertNull(StructurePlacementMarker.decode("objects/oak_tree@not-an-id"));
assertNull(StructurePlacementMarker.decode("objects/oak_tree@42@extra"));
assertNull(StructurePlacementMarker.decode("@iris-structure:v2:b2JqZWN0:42:c3RydWN0dXJl"));
assertNull(StructurePlacementMarker.decode("@iris-structure:v1:not%base64:42:c3RydWN0dXJl"));
assertNull(StructurePlacementMarker.decode("@iris-structure:v1:b2JqZWN0:42:"));
}
}
@@ -37,9 +37,11 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -117,6 +119,43 @@ public class IrisCaveCarver3DNearParityTest {
assertEquals(firstCapture.carvedLiquids, secondCapture.carvedLiquids);
}
@Test
public void warpCacheDoesNotCrossContaminateCarverInstancesOnSameThread() throws Exception {
Engine engine = createEngine(128, 92);
IrisCaveProfile firstProfile = createProfile(true, false);
firstProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.12D));
IrisCaveProfile secondProfile = createProfile(true, false);
secondProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
IrisCaveProfile controlProfile = createProfile(true, false);
controlProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, firstProfile);
IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, secondProfile);
IrisCaveCarver3D controlCarver = new IrisCaveCarver3D(engine, controlProfile);
int x = 48;
int y = 64;
int z = -352;
double firstDensity = sampleDensity(firstCarver, x, y, z);
double secondDensity = sampleDensity(secondCarver, x, y, z);
AtomicReference<Double> controlDensity = new AtomicReference<>();
AtomicReference<Throwable> controlFailure = new AtomicReference<>();
Thread controlThread = new Thread(() -> {
try {
controlDensity.set(sampleDensity(controlCarver, x, y, z));
} catch (Throwable throwable) {
controlFailure.set(throwable);
}
}, "Iris cave warp cache control");
controlThread.start();
controlThread.join();
if (controlFailure.get() != null) {
throw new AssertionError(controlFailure.get());
}
assertNotEquals(firstDensity, controlDensity.get(), 0D);
assertEquals(controlDensity.get(), secondDensity, 0D);
}
@Test
public void exactPathCarvesChunkEdgesAndRespectsWorldHeightClipping() {
Engine engine = createEngine(48, 46);
@@ -277,6 +316,10 @@ public class IrisCaveCarver3DNearParityTest {
return elapsed;
}
private double sampleDensity(IrisCaveCarver3D carver, int x, int y, int z) throws Exception {
return (double) sampleDensityMethod.invoke(carver, x, y, z);
}
private long runNaiveOnce(IrisCaveCarver3D carver, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights, int worldHeight) throws Exception {
WriterCapture capture = createWriterCapture(worldHeight);
long start = System.nanoTime();
@@ -0,0 +1,35 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisStructureComponentMarkerTest {
@Test
public void markerFilterAcceptsOnlyStorageContainers() {
PlatformBlockState storage = mock(PlatformBlockState.class);
PlatformBlockState solid = mock(PlatformBlockState.class);
when(storage.isStorageChest()).thenReturn(true);
when(solid.isStorageChest()).thenReturn(false);
assertTrue(IrisStructureComponent.shouldWriteStructureMarker(storage));
assertFalse(IrisStructureComponent.shouldWriteStructureMarker(solid));
assertFalse(IrisStructureComponent.shouldWriteStructureMarker(null));
}
@Test
public void placementIdIsStableAndCoordinateSensitive() {
int first = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 20, -14, 35);
int repeated = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 20, -14, 35);
int moved = IrisStructureComponent.structurePlacementId("structures/village", "pieces/house", 21, -14, 35);
assertEquals(first, repeated);
assertNotEquals(first, moved);
}
}
@@ -0,0 +1,135 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MatterGenerator;
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.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
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.spy;
import static org.mockito.Mockito.when;
public class MantleCarvingComponentBoundaryRadiusTest {
@Before
public 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);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void boundaryWallResolutionSchedulesAdjacentCarvingChunks() {
EngineMantle engineMantle = mock(EngineMantle.class);
MantleCarvingComponent component = new MantleCarvingComponent(engineMantle);
assertEquals(1, component.getRadius());
assertEquals(1, Math.ceilDiv(component.getRadius(), 16));
}
@Test
@SuppressWarnings("unchecked")
public void matterGenerationRunsCarvingForAllAdjacentChunks() {
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
doAnswer(invocation -> {
Runnable task = invocation.getArgument(1);
task.run();
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
EngineMantle engineMantle = mock(EngineMantle.class);
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getEngine()).thenReturn(engine);
List<String> generatedChunks = new ArrayList<>();
MantleCarvingComponent component = spy(new MantleCarvingComponent(engineMantle));
doAnswer(invocation -> {
int chunkX = invocation.getArgument(1);
int chunkZ = invocation.getArgument(2);
generatedChunks.add(chunkX + "," + chunkZ);
return null;
}).when(component).generateLayer(any(), anyInt(), anyInt(), any());
TestMatterGenerator generator = new TestMatterGenerator(engine, mantle, component);
generator.generateMatter(4, -7, false, mock(ChunkContext.class));
assertEquals(List.of(
"3,-8", "3,-7", "3,-6",
"4,-8", "4,-7", "4,-6",
"5,-8", "5,-7", "5,-6"
), generatedChunks);
}
private static final class TestMatterGenerator implements MatterGenerator {
private final Engine engine;
private final Mantle<Matter> mantle;
private final List<Pair<List<MantleComponent>, Integer>> components;
private TestMatterGenerator(Engine engine, Mantle<Matter> mantle, MantleComponent component) {
this.engine = engine;
this.mantle = mantle;
this.components = List.of(new Pair<>(List.of(component), 1));
}
@Override
public Engine getEngine() {
return engine;
}
@Override
public Mantle<Matter> getMantle() {
return mantle;
}
@Override
public int getRadius() {
return 1;
}
@Override
public int getRealRadius() {
return 0;
}
@Override
public List<Pair<List<MantleComponent>, Integer>> getComponents() {
return components;
}
}
}
@@ -0,0 +1,124 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisRegion;
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.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisCarveModifierInferenceIsolationTest {
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
doReturn(block).when(registries).block(anyString());
IrisPlatform platform = mock(IrisPlatform.class);
doReturn(registries).when(platform).registries();
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
@SuppressWarnings("unchecked")
public void cavePaintingDoesNotMutateSharedBiomeInference() throws Exception {
IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND);
biome.getLayers().clear();
biome.getCaveCeilingLayers().clear();
Engine engine = mock(Engine.class);
doReturn(mock(IrisData.class)).when(engine).getData();
doReturn(mock(IrisDimension.class)).when(engine).getDimension();
doReturn(mock(IrisComplex.class)).when(engine).getComplex();
doReturn(IrisWorld.builder().minHeight(-256).maxHeight(512).build()).when(engine).getWorld();
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
Map<String, IrisBiome> customBiomes = new HashMap<>();
customBiomes.put("shared", biome);
Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
"paintBoundaryZone",
Hunk.class,
MatterCavern.class,
int.class,
int.class,
int.class,
int.class,
int.class,
int.class,
IrisDimensionCarvingResolver.State.class,
Long2ObjectOpenHashMap.class,
Map.class
);
paintBoundaryZone.setAccessible(true);
paintBoundaryZone.invoke(
modifier,
mock(Hunk.class),
new MatterCavern(true, "shared", (byte) 0),
0,
0,
0,
0,
1,
1,
new IrisDimensionCarvingResolver.State(),
new Long2ObjectOpenHashMap<IrisBiome>(),
customBiomes
);
IrisBiome fallback = new IrisBiome();
ProceduralStream<IrisBiome> landStream = mock(ProceduralStream.class);
doReturn(fallback).when(landStream).get(anyDouble(), anyDouble());
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
Field landBiomeStream = IrisComplex.class.getDeclaredField("landBiomeStream");
landBiomeStream.setAccessible(true);
landBiomeStream.set(complex, landStream);
IrisRegion region = mock(IrisRegion.class);
doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble());
Method fixBiomeType = IrisComplex.class.getDeclaredMethod(
"fixBiomeType",
Double.class,
IrisBiome.class,
IrisRegion.class,
Double.class,
Double.class,
double.class
);
fixBiomeType.setAccessible(true);
IrisBiome resolved = (IrisBiome) fixBiomeType.invoke(complex, 10D, biome, region, 0D, 0D, 0D);
assertSame(biome, resolved);
assertEquals(InferredType.LAND, biome.getInferredType());
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class IrisBiomeCustomSpawnTest {
@Test
public void normalizesNamespacedEntityKey() {
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType(" Minecraft:Slime ");
assertEquals("minecraft:slime", spawn.getTypeKey());
}
@Test
public void prefixesBareEntityKey() {
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType("Slime");
assertEquals("minecraft:slime", spawn.getTypeKey());
}
@Test
public void returnsNullForBlankEntityKey() {
IrisBiomeCustomSpawn spawn = new IrisBiomeCustomSpawn().setType(" ");
assertNull(spawn.getTypeKey());
}
}
@@ -0,0 +1,139 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeGeneratorSeedIsolationTest {
private static final double SAMPLE_X = 128D;
private static final double SAMPLE_Z = -64D;
@Test
public void engineSeedMakesPaperStyleFirstCallOrderIrrelevant() {
IrisData data = dataWithActiveEngine(new AtomicReference<>(engine(1337L)));
IrisBiome paperFirst = biome(data);
IrisBiome moddedFirst = biome(data);
CNG paperGenerator = paperFirst.getBiomeGenerator(new RNG(8457289L));
CNG moddedGenerator = moddedFirst.getBiomeGenerator(new RNG(-991245L));
assertEquals(paperGenerator.noise(SAMPLE_X, SAMPLE_Z), moddedGenerator.noise(SAMPLE_X, SAMPLE_Z), 0D);
}
@Test
public void distinctEnginesKeepDistinctBiomeGenerators() {
Engine firstEngine = engine(1337L);
Engine secondEngine = engine(7331L);
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
CNG first = biome.getBiomeGenerator(new RNG(1L), firstEngine);
CNG second = biome.getBiomeGenerator(new RNG(1L), secondEngine);
assertNotSame(first, second);
assertNotEquals(first.noise(SAMPLE_X, SAMPLE_Z), second.noise(SAMPLE_X, SAMPLE_Z), 0D);
}
@Test
public void sameSeedEnginesSharingBiomeReuseBiomeGenerator() {
Engine firstEngine = engine(1337L);
Engine secondEngine = engine(1337L);
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
CNG first = biome.getBiomeGenerator(new RNG(11L), firstEngine);
CNG second = biome.getBiomeGenerator(new RNG(99L), secondEngine);
assertSame(first, second);
}
@Test
public void engineLessToolingUsesSuppliedRngSeed() {
IrisBiome biome = biome(null);
CNG first = biome.getBiomeGenerator(new RNG(11L));
CNG sameSeed = biome.getBiomeGenerator(new RNG(11L));
CNG second = biome.getBiomeGenerator(new RNG(99L));
assertSame(first, sameSeed);
assertNotSame(first, second);
assertNotEquals(first.noise(SAMPLE_X, SAMPLE_Z), second.noise(SAMPLE_X, SAMPLE_Z), 0D);
}
@Test
public void concurrentExactEnginesIgnoreLoaderEngine() throws Exception {
Engine firstEngine = engine(1337L);
Engine secondEngine = engine(7331L);
IrisBiome biome = biome(dataWithActiveEngine(new AtomicReference<>(secondEngine)));
CountDownLatch start = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<CNG> firstFuture = executor.submit(() -> repeatedlyResolve(biome, firstEngine, start));
Future<CNG> secondFuture = executor.submit(() -> repeatedlyResolve(biome, secondEngine, start));
start.countDown();
CNG first = firstFuture.get();
CNG second = secondFuture.get();
assertNotSame(first, second);
assertSame(first, biome.getBiomeGenerator(new RNG(99L), firstEngine));
assertSame(second, biome.getBiomeGenerator(new RNG(99L), secondEngine));
} finally {
executor.shutdownNow();
}
}
@Test
public void biomeGeneratorCacheEvictsOldSeeds() {
IrisBiome biome = biome(null);
CNG first = biome.getBiomeGenerator(new RNG(0L));
for (long seed = 1L; seed <= 8L; seed++) {
biome.getBiomeGenerator(new RNG(seed));
}
assertNotSame(first, biome.getBiomeGenerator(new RNG(0L)));
}
private IrisBiome biome(IrisData data) {
IrisBiome biome = new IrisBiome().setName("Scatter Test");
biome.setLoader(data);
return biome;
}
private IrisData dataWithActiveEngine(AtomicReference<Engine> activeEngine) {
IrisData data = mock(IrisData.class);
when(data.getEngine()).thenAnswer(ignored -> activeEngine.get());
return data;
}
private Engine engine(long biomeSeed) {
SeedManager seedManager = mock(SeedManager.class);
when(seedManager.getBiome()).thenReturn(biomeSeed);
Engine engine = mock(Engine.class);
when(engine.getSeedManager()).thenReturn(seedManager);
return engine;
}
private CNG repeatedlyResolve(IrisBiome biome, Engine engine, CountDownLatch start) throws InterruptedException {
start.await();
CNG generator = biome.getBiomeGenerator(new RNG(1L), engine);
for (int index = 0; index < 100; index++) {
assertSame(generator, biome.getBiomeGenerator(new RNG(index + 2L), engine));
}
return generator;
}
}
@@ -0,0 +1,49 @@
package art.arcane.iris.engine.object;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class IrisDimensionBiomeTagTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void biomeTagWritesAreSortedAndDeduplicated() throws Exception {
Path output = temporaryFolder.getRoot().toPath().resolve("allows_surface_slime_spawns.json");
IrisDimension.writeBiomeTag(output, "overworld:swamp_b");
IrisDimension.writeBiomeTag(output, "overworld:swamp_a");
IrisDimension.writeBiomeTag(output, "overworld:swamp_b");
JSONObject tag = new JSONObject(Files.readString(output, StandardCharsets.UTF_8));
JSONArray values = tag.getJSONArray("values");
assertFalse(tag.getBoolean("replace"));
assertEquals(2, values.length());
assertEquals("overworld:swamp_a", values.getString(0));
assertEquals("overworld:swamp_b", values.getString(1));
}
@Test
public void customBiomeTagUsesTheMinecraftBiomeTagPath() throws Exception {
KList<String> tags = new KList<>();
tags.add("minecraft:allows_surface_slime_spawns");
IrisDimension.installBiomeTags(temporaryFolder.getRoot(), "overworld:swamp", tags);
Path output = temporaryFolder.getRoot().toPath()
.resolve("iris/data/minecraft/tags/worldgen/biome/allows_surface_slime_spawns.json");
JSONObject tag = new JSONObject(Files.readString(output, StandardCharsets.UTF_8));
assertEquals("overworld:swamp", tag.getJSONArray("values").getString(0));
}
}
@@ -0,0 +1,38 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisStructureLootPlacementTest {
@Test
public void createsPiecePlacementWithAuthoredLootOrderAndDefaultWeights() {
IrisStructure structure = new IrisStructure();
structure.getLoot().add("village_common");
structure.getLoot().add("village_rare");
IrisObjectPlacement placement = structure.createLootPlacement("pieces/village_house");
assertEquals(1, placement.getPlace().size());
assertEquals("pieces/village_house", placement.getPlace().get(0));
assertEquals(2, placement.getLoot().size());
assertEquals("village_common", placement.getLoot().get(0).getName());
assertEquals(1, placement.getLoot().get(0).getWeight());
assertTrue(placement.getLoot().get(0).getFilter().isEmpty());
assertEquals("village_rare", placement.getLoot().get(1).getName());
assertEquals(1, placement.getLoot().get(1).getWeight());
assertTrue(placement.getLoot().get(1).getFilter().isEmpty());
assertFalse(placement.isOverrideGlobalLoot());
}
@Test
public void createsEmptyLootPlacementWhenStructureHasNoLoot() {
IrisObjectPlacement placement = new IrisStructure().createLootPlacement("pieces/empty_house");
assertEquals("pieces/empty_house", placement.getPlace().get(0));
assertTrue(placement.getLoot().isEmpty());
assertFalse(placement.isOverrideGlobalLoot());
}
}