This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-16 12:26:01 -04:00
1 parent 55b7460b9a
commit 699247b1de
327 files changed
+30138 -3934

No files matched your search

@@ -0,0 +1,47 @@
package art.arcane.iris.core.datapack;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class DatapackIngestServiceTest {
@Test
public void manifestRemainsPendingWhenAnyPackFails() {
DatapackIngestService.Entry first = new DatapackIngestService.Entry();
DatapackIngestService.Entry second = new DatapackIngestService.Entry();
boolean completed = DatapackIngestService.markStructuresImportedIfComplete(
List.of(first, second), 2, 1);
assertFalse(completed);
assertFalse(first.structuresImported);
assertFalse(second.structuresImported);
}
@Test
public void manifestRemainsPendingWhenNoPackWasAttempted() {
DatapackIngestService.Entry entry = new DatapackIngestService.Entry();
boolean completed = DatapackIngestService.markStructuresImportedIfComplete(
List.of(entry), 0, 0);
assertFalse(completed);
assertFalse(entry.structuresImported);
}
@Test
public void manifestCompletesOnlyAfterEveryAttemptedPackSucceeds() {
DatapackIngestService.Entry first = new DatapackIngestService.Entry();
DatapackIngestService.Entry second = new DatapackIngestService.Entry();
boolean completed = DatapackIngestService.markStructuresImportedIfComplete(
List.of(first, second), 2, 2);
assertTrue(completed);
assertTrue(first.structuresImported);
assertTrue(second.structuresImported);
}
}
@@ -1,31 +0,0 @@
package art.arcane.iris.core.nms;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class INMSBindingProbeCodesTest {
@Test
public void skipsSyntheticBukkitBindingWhenNmsIsEnabled() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", false, List.of("v26_2_R1"));
assertEquals(List.of("v26_2_R1"), probeCodes);
}
@Test
public void leavesBukkitFallbackEmptyWhenNmsIsDisabled() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", true, List.of("v26_2_R1"));
assertTrue(probeCodes.isEmpty());
}
@Test
public void keepsConcreteBindingCodesAsPrimaryProbe() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("v26_2_R1", false, List.of("v26_2_R1"));
assertEquals(List.of("v26_2_R1"), probeCodes);
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.core.nms;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NmsBindingSelectorTest {
@Test
public void selectsOnlyTheExactSupportedRevision() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.2-R0.1-SNAPSHOT");
assertEquals("v26_2_R1", NmsBindingSelector.select(version));
}
@Test
public void rejectsUnsupportedVersionsWithoutProbingAnotherRevision() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.3-R0.1-SNAPSHOT");
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> NmsBindingSelector.select(version));
assertTrue(failure.getMessage().contains("26.3"));
}
@Test
public void rejectsMissingVersionDetection() {
assertThrows(IllegalStateException.class, () -> NmsBindingSelector.select(null));
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.core.nms.v1X;
import art.arcane.iris.core.nms.INMSBinding;
import org.bukkit.World;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NMSBinding1XWorldGenerationTest {
@Test
public void limitedBindingRefusesIrisWorldInitialization() {
NMSBinding1X binding = new NMSBinding1X();
assertFalse(binding.supportsIrisWorldGeneration());
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> binding.inject(1L, null, null));
assertTrue(error.getMessage().contains("general.disableNMS=true"));
}
@Test
public void structureRegistryHooksAreRequiredBindingMethods() throws NoSuchMethodException {
assertFalse(INMSBinding.class.getMethod("getStructureKeys").isDefault());
assertFalse(INMSBinding.class.getMethod("getStructureSetKeys").isDefault());
assertFalse(INMSBinding.class.getMethod("getReachableStructureKeys", World.class).isDefault());
assertFalse(INMSBinding.class.getMethod("getStructureBiomeKeys", String.class).isDefault());
assertFalse(INMSBinding.class.getMethod("getPossibleBiomeKeys", World.class).isDefault());
}
@Test
public void limitedBindingRefusesStructureRegistryHooks() {
NMSBinding1X binding = new NMSBinding1X();
IllegalStateException structureKeys = assertThrows(
IllegalStateException.class, binding::getStructureKeys);
IllegalStateException structureSetKeys = assertThrows(
IllegalStateException.class, binding::getStructureSetKeys);
IllegalStateException reachableKeys = assertThrows(
IllegalStateException.class, () -> binding.getReachableStructureKeys(null));
IllegalStateException structureBiomeKeys = assertThrows(
IllegalStateException.class, () -> binding.getStructureBiomeKeys("minecraft:village"));
IllegalStateException possibleBiomeKeys = assertThrows(
IllegalStateException.class, () -> binding.getPossibleBiomeKeys(null));
assertTrue(structureKeys.getMessage().contains("read registered structure keys"));
assertTrue(structureSetKeys.getMessage().contains("read registered structure-set keys"));
assertTrue(reachableKeys.getMessage().contains("resolve reachable structures"));
assertTrue(structureBiomeKeys.getMessage().contains("resolve structure biome keys"));
assertTrue(possibleBiomeKeys.getMessage().contains("resolve possible biome keys"));
}
}
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pack;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PackValidationRegistryTest {
@Before
public void setUp() {
PackValidationRegistry.clear();
}
@After
public void tearDown() {
PackValidationRegistry.clear();
}
@Test
public void missingValidationFailsClosed() {
assertBroken("overworld", "has not completed");
}
@Test
public void blockingValidationFailsClosedWithOriginalReasons() {
PackValidationRegistry.publish(new PackValidationResult(
"overworld", List.of("replacement graph is not runtime-viable"), List.of(), 1L));
assertBroken("overworld", "replacement graph is not runtime-viable");
}
@Test
public void successfulValidationAuthorizesUse() {
PackValidationResult result = new PackValidationResult(
"overworld", List.of(), List.of("warning"), 1L);
PackValidationRegistry.publish(result);
assertEquals(result, PackValidationRegistry.requireLoadable("overworld"));
}
private void assertBroken(String pack, String expectedReason) {
try {
PackValidationRegistry.requireLoadable(pack);
} catch (BrokenPackException e) {
assertEquals(pack, e.getPackName());
assertTrue(e.getReasons().toString(), e.getReasons().stream().anyMatch(
reason -> reason.contains(expectedReason)));
return;
}
throw new AssertionError("Expected pack validation to fail closed");
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.core.pack;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PackValidatorImportedStructurePolicyTest {
@Test
public void denyOnlyPolicyAcceptsDisabledKeysAndAdjustments() {
JSONObject policy = new JSONObject()
.put("disabled", new JSONArray().put("minecraft:stronghold"))
.put("adjustments", new JSONArray().put(new JSONObject()
.put("match", new JSONArray().put("minecraft:village"))));
List<String> errors = validate(policy);
assertTrue(errors.isEmpty());
}
@Test
public void legacyModeAndEnabledWhitelistAreRejected() {
JSONObject policy = new JSONObject()
.put("mode", "ALL_OFF")
.put("enabled", new JSONArray().put("minecraft:monument"));
List<String> errors = validate(policy);
assertEquals(2, errors.size());
assertTrue(errors.get(0).contains("importedStructures.mode is not supported"));
assertTrue(errors.get(1).contains("importedStructures.enabled is not supported"));
}
@Test
public void explicitNullsAndWrongShapesAreRejected() {
JSONObject policy = new JSONObject()
.put("disabled", JSONObject.NULL)
.put("adjustments", new JSONObject());
List<String> errors = validate(policy);
assertEquals(2, errors.size());
assertTrue(errors.stream().anyMatch(error -> error.contains("'disabled' must be an array")));
assertTrue(errors.stream().anyMatch(error -> error.contains("adjustments must be an array")));
}
@Test
public void explicitNullPolicyIsRejectedWhileOmissionUsesDefaults() {
List<String> missingErrors = new ArrayList<>();
PackValidator.validateImportedStructurePolicy("overworld", new JSONObject(), missingErrors);
assertTrue(missingErrors.isEmpty());
List<String> nullErrors = new ArrayList<>();
PackValidator.validateImportedStructurePolicy("overworld",
new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors);
assertEquals(List.of("Dimension 'overworld' importedStructures must be an object."), nullErrors);
}
private List<String> validate(JSONObject policy) {
List<String> errors = new ArrayList<>();
PackValidator.validateImportedStructurePolicy("overworld",
new JSONObject().put("importedStructures", policy), errors);
return errors;
}
}
@@ -0,0 +1,129 @@
package art.arcane.iris.core.pack;
import art.arcane.iris.engine.object.IrisLoot;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.lang.reflect.Field;
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 PackValidatorLootTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void acceptsValidLootTableAndReference() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "loot/global/clutter.json", "{\"name\":\"Clutter\",\"minPicked\":1,\"maxPicked\":2,"
+ "\"loot\":[{\"type\":\"stick\",\"rarity\":2,\"minAmount\":1,\"maxAmount\":3}]}");
write(pack, "dimensions/main.json", "{\"loot\":{\"mode\":\"FALLBACK\",\"multiplier\":0.5,"
+ "\"tables\":[\"global/clutter\"]}}");
assertTrue(PackValidator.validateLootGraph(pack).isEmpty());
}
@Test
public void rejectsMissingLootReference() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"loot\":{\"tables\":[\"missing\"]}}");
assertEquals(List.of("Dimension 'main'.loot.tables[0] references missing loot table 'missing'."),
PackValidator.validateLootGraph(pack));
}
@Test
public void rejectsInvalidLootBoundsAndEmptyEntries() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "loot/broken.json", "{\"rarity\":0,\"minPicked\":4,\"maxPicked\":2,\"maxTries\":0,"
+ "\"loot\":[{\"type\":\"\",\"rarity\":0,\"minAmount\":3,\"maxAmount\":1,"
+ "\"enchantments\":[{\"enchantment\":\"\",\"minLevel\":4,\"maxLevel\":2,\"chance\":2}]}]}");
List<String> errors = PackValidator.validateLootGraph(pack);
assertTrue(errors.stream().anyMatch(error -> error.contains(".rarity must be at least 1")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".minPicked must not exceed")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".maxTries must be at least 1")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".type must not be blank")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".minAmount must not exceed")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".minLevel must not exceed")));
assertTrue(errors.stream().anyMatch(error -> error.contains(".chance must be a finite number from 0 to 1")));
}
@Test
public void rejectsInvalidReferenceModeAndMultiplier() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"loot\":{\"mode\":\"MERGE\",\"multiplier\":-1,\"tables\":[]}}");
List<String> errors = PackValidator.validateLootGraph(pack);
assertEquals(2, errors.size());
assertTrue(errors.get(0).contains(".mode must be"));
assertTrue(errors.get(1).contains(".multiplier must be"));
}
@Test
public void enforcesPublishedLootMultiplierCap() throws Exception {
File acceptedPack = temporaryFolder.newFolder("accepted-pack");
write(acceptedPack, "dimensions/main.json", "{\"loot\":{\"multiplier\":16,\"tables\":[]}}");
assertTrue(PackValidator.validateLootGraph(acceptedPack).isEmpty());
File rejectedPack = temporaryFolder.newFolder("rejected-pack");
write(rejectedPack, "dimensions/main.json", "{\"loot\":{\"multiplier\":16.01,\"tables\":[]}}");
assertEquals(List.of("Dimension 'main'.loot.multiplier must be a finite number from 0 to 16."),
PackValidator.validateLootGraph(rejectedPack));
}
@Test
public void acceptsPublishedLootWorkBounds() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "loot/boundary.json", "{\"minPicked\":64,\"maxPicked\":64,\"maxTries\":256,"
+ "\"loot\":[{\"type\":\"stone\",\"minAmount\":64,\"maxAmount\":64}]}");
assertTrue(PackValidator.validateLootGraph(pack).isEmpty());
}
@Test
public void rejectsLootWorkBoundsAbovePublishedCaps() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "loot/excessive.json", "{\"minPicked\":65,\"maxPicked\":65,\"maxTries\":257,"
+ "\"loot\":[{\"type\":\"stone\",\"minAmount\":65,\"maxAmount\":65}]}");
List<String> errors = PackValidator.validateLootGraph(pack);
assertTrue(errors.contains("Loot table 'excessive'.minPicked must be at most 64."));
assertTrue(errors.contains("Loot table 'excessive'.maxPicked must be at most 64."));
assertTrue(errors.contains("Loot table 'excessive'.maxTries must be at most 256."));
assertTrue(errors.contains("Loot table 'excessive'.loot[0].minAmount must be at most 64."));
assertTrue(errors.contains("Loot table 'excessive'.loot[0].maxAmount must be at most 64."));
}
@Test
public void lootWorkCapsArePublishedInSchemaMetadata() throws Exception {
assertSchemaMaximum(IrisLootTable.class, "minPicked", IrisLootTable.MAX_PICKED);
assertSchemaMaximum(IrisLootTable.class, "maxPicked", IrisLootTable.MAX_PICKED);
assertSchemaMaximum(IrisLootTable.class, "maxTries", IrisLootTable.MAX_TRIES);
assertSchemaMaximum(IrisLoot.class, "minAmount", IrisLoot.MAX_AMOUNT);
assertSchemaMaximum(IrisLoot.class, "maxAmount", IrisLoot.MAX_AMOUNT);
}
private void assertSchemaMaximum(Class<?> type, String fieldName, int expected) throws Exception {
Field field = type.getDeclaredField(fieldName);
MaxNumber maximum = field.getAnnotation(MaxNumber.class);
assertEquals(expected, maximum.value(), 0D);
}
private void write(File root, String relative, String content) throws Exception {
File file = new File(root, relative);
Files.createDirectories(file.toPath().getParent());
Files.writeString(file.toPath(), content, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,54 @@
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 PackValidatorRemovedWorldgenFieldTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsFluidBodiesAcrossEveryFormerHost() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"fluidBodies\":{}}");
write(pack, "regions/nested/region.json", "{\"fluidBodies\":null}");
write(pack, "biomes/nested/biome.json", "{\"fluidBodies\":{\"lakes\":[]}}");
assertEquals(List.of(
"Dimension 'main' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported.",
"Region 'nested/region' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported.",
"Biome 'nested/biome' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported."
), PackValidator.validateRemovedWorldgenFields(pack));
}
@Test
public void removedFluidBodiesBlocksFullPackValidation() 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\",\"fluidBodies\":{}}");
PackValidationResult result = PackValidator.validate(pack);
assertFalse(result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Biome 'biome' declares removed field 'fluidBodies'. Remove it because fluid-body generation is not supported."));
}
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);
}
}
@@ -4,13 +4,17 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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.assertTrue;
public class PackValidatorStructureGraphTest {
@@ -30,6 +34,18 @@ public class PackValidatorStructureGraphTest {
assertTrue(PackValidator.validateStructureGraph(pack).isEmpty());
}
@Test
public void collectsOnlyStructuresReferencedByRuntimePlacements() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"active\"]}]}");
write(pack, "regions/forest.json", "{\"structures\":[{\"structures\":[\"regional\"]}]}");
write(pack, "structures/active.json", "{}");
write(pack, "structures/regional.json", "{}");
write(pack, "structures/library.json", "{}");
assertEquals(Set.of("active", "regional"), PackValidator.collectPlacedStructureKeys(pack));
}
@Test
public void reportsMissingReferencesInDeterministicGraphOrder() throws Exception {
File pack = temporaryFolder.newFolder("pack");
@@ -101,9 +117,232 @@ public class PackValidatorStructureGraphTest {
), PackValidator.validateStructureGraph(pack));
}
@Test
public void acceptsViableDimensionLevelNativeReplacement() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"city\"],"
+ "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}");
write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}");
assertTrue(PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, 0, 0)).isEmpty());
}
@Test
public void rejectsNonViableNativeReplacementWithoutFallingBack() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"city\"],"
+ "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}");
write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}");
List<String> errors = PackValidator.validateNativeStructureReplacements(pack, Set.of(), Map.of());
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("not runtime-viable")));
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("will not be used as a fallback")));
}
@Test
public void rejectsReplacementWithoutValidVanillaSource() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"structures\":[{\"structures\":[\"city\"],"
+ "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}");
write(pack, "structures/city.json", "{\"vanillaSource\":\"\"}");
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -1, 1));
assertTrue(errors.toString(), errors.stream().anyMatch(
message -> message.contains("vanillaSource is not a valid namespaced registry key")));
}
@Test
public void rejectsReplacementOutsideDimensionPlacement() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "regions/main.json", "{\"structures\":[{\"structures\":[\"city\"],"
+ "\"nativeSuppression\":\"REPLACE_SOURCE\"}]}");
write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\"}");
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -1, 1));
assertTrue(errors.toString(), errors.stream().anyMatch(
message -> message.contains("only valid on dimension-level placements")));
}
@Test
public void rejectsUndergroundReplacementBelowDimensionWritableRange() throws Exception {
File pack = replacementPack("below", "STRUCTURE_PIECE", true, -80, -80);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4));
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0")
&& message.contains("piece envelope -5..4")
&& message.contains("placement band -80..-80")
&& message.contains("writable world -63..319")
&& message.contains("will not be used as a fallback")));
}
@Test
public void rejectsUndergroundReplacementAboveDimensionWritableRange() throws Exception {
File pack = replacementPack("above", "STRUCTURE_PIECE", true, 318, 318);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4));
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0")
&& message.contains("piece envelope -5..4")
&& message.contains("placement band 318..318")
&& message.contains("writable world -63..319")));
}
@Test
public void surfaceAlignsMultiPieceEnvelopeBeforeCheckingWorldBounds() throws Exception {
File pack = replacementPack("surface-aligned", "CENTER_HEIGHT", false, 290, 290);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 2, -30, 10));
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("surface-aligned")
&& message.contains("piece envelope 0..40")
&& message.contains("placement band 290..290")));
}
@Test
public void doesNotApplyExactYEnvelopeGateToSingleSurfacePiece() throws Exception {
File pack = replacementPack("surface-single", "CENTER_HEIGHT", false, -63, 319);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -500, 500));
assertTrue(errors.toString(), errors.isEmpty());
}
@Test
public void validatesEverySurfaceExactYAnchorAgainstWritableWorldBounds() throws Exception {
File pack = replacementPack("surface-exact-range", "STRUCTURE_PIECE", false, -63, 319);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4));
assertTrue(errors.toString(), errors.stream().anyMatch(message -> message.contains("sampled seed 0")
&& message.contains("piece envelope -5..4")
&& message.contains("placement band -63..319")
&& message.contains("writable world -63..319")));
}
@Test
public void acceptsSurfaceExactYWhenEveryConfiguredAnchorFits() throws Exception {
File pack = replacementPack("surface-exact-safe", "STRUCTURE_PIECE", false, -58, 315);
List<String> errors = PackValidator.validateNativeStructureReplacements(
pack, Set.of("city"), sampledEnvelope("city", 1, -5, 4));
assertTrue(errors.toString(), errors.isEmpty());
}
@Test
public void fullPackValidationRejectsReplacementOutsideDimensionEnvelope() throws Exception {
File pack = replacementPack("full-pack-below", "STRUCTURE_PIECE", true, -80, -80);
write(pack, "dimensions/main.json", "{\"dimensionHeight\":{\"min\":-64,\"max\":320},"
+ "\"regions\":[\"region\"],\"structures\":[{\"structures\":[\"city\"],"
+ "\"nativeSuppression\":\"REPLACE_SOURCE\",\"underground\":true,"
+ "\"minHeight\":-80,\"maxHeight\":-80}]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
write(pack, "structures/city.json", "{\"startPool\":\"city/start\","
+ "\"vanillaSource\":\"minecraft:ancient_city\",\"placeMode\":\"STRUCTURE_PIECE\"}");
write(pack, "jigsaw-pools/city/start.json", "{\"pieces\":[{\"piece\":\"city/start\"}]}");
write(pack, "jigsaw-pieces/city/start.json", "{\"object\":\"city/start\",\"connectors\":[]}");
writeObjectHeader(pack, "objects/city/start.iob", 3, 9, 3);
PackValidationResult result = PackValidator.validate(pack);
assertTrue(result.getBlockingErrors().toString(), result.getBlockingErrors().stream().anyMatch(
message -> message.contains("REPLACE_SOURCE structure 'city'")
&& message.contains("cannot fit placement band -80..-80")
&& message.contains("will not be used as a fallback")));
}
@Test
public void fullPackValidationPreservesMalformedActiveObjectPathAndReason() throws Exception {
File pack = temporaryFolder.newFolder("malformed-active-object");
write(pack, "dimensions/main.json", "{\"dimensionHeight\":{\"min\":-64,\"max\":320},"
+ "\"regions\":[\"region\"],\"structures\":[{\"structures\":[\"castle\"]}]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
write(pack, "biomes/biome.json", "{\"name\":\"Biome\"}");
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
Path object = pack.toPath().resolve("objects/castle/start.iob");
Files.createDirectories(object.getParent());
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(object))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
PackValidationResult result = PackValidator.validate(pack);
String expected = "Malformed Iris object resource " + object + ": EOFException";
assertTrue(result.getBlockingErrors().toString(), result.getBlockingErrors().contains(expected));
assertFalse(result.getBlockingErrors().toString(), result.getBlockingErrors().stream().anyMatch(
message -> message.contains("references missing object 'castle/start'")));
}
private File replacementPack(
String name,
String placeMode,
boolean underground,
int minimumY,
int maximumY
) throws Exception {
File pack = temporaryFolder.newFolder(name);
write(pack, "dimensions/main.json", "{\"dimensionHeight\":{\"min\":-64,\"max\":320},"
+ "\"structures\":[{\"structures\":[\"city\"],\"nativeSuppression\":\"REPLACE_SOURCE\","
+ "\"underground\":" + underground + ",\"minHeight\":" + minimumY
+ ",\"maxHeight\":" + maximumY + "}]}");
write(pack, "structures/city.json", "{\"vanillaSource\":\"minecraft:ancient_city\","
+ "\"placeMode\":\"" + placeMode + "\"}");
return pack;
}
private Map<String, List<StructureGraphPackValidator.SampledVerticalEnvelope>> sampledEnvelope(
String structureKey,
int pieceCount,
int minimumYOffset,
int maximumYOffset
) {
return Map.of(structureKey, List.of(
new StructureGraphPackValidator.SampledVerticalEnvelope(
0L, pieceCount, minimumYOffset, maximumYOffset)));
}
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 void writeObjectHeader(
File root,
String relative,
int width,
int height,
int depth
) throws Exception {
Path path = root.toPath().resolve(relative);
Files.createDirectories(path.getParent());
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(path))) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
output.writeUTF("Iris V2 IOB;");
output.writeShort(0);
output.writeInt(0);
output.writeInt(0);
}
}
}
@@ -0,0 +1,347 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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.assertThrows;
import static org.junit.Assert.assertTrue;
public class StructureGraphPackValidatorTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void reportsCompilerOnlyWeightAndConnectorBoundsFailures() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\",\"weight\":0}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[{\"position\":{\"x\":2,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"castle/start\",\"name\":\"path\",\"targetName\":\"path\"}]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().stream().anyMatch(message -> message.contains("non-positive weight 0")));
assertTrue(validation.errors().stream().anyMatch(message -> message.contains("outside object bounds")));
}
@Test
public void reportsFallbackCyclesBeforeRuntime() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/a\"}");
write(pack, "jigsaw-pools/castle/a.json",
"{\"pieces\":[{\"piece\":\"castle/piece\"}],\"fallback\":\"castle/b\"}");
write(pack, "jigsaw-pools/castle/b.json",
"{\"pieces\":[{\"piece\":\"castle/piece\"}],\"fallback\":\"castle/a\"}");
write(pack, "jigsaw-pieces/castle/piece.json",
"{\"object\":\"castle/piece\",\"connectors\":[]}");
writeObjectHeader(pack.resolve("objects/castle/piece.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().stream().anyMatch(message -> message.contains("fallback cycle")));
}
@Test
public void blocksGraphsThatCanOnlyProducePartialAssemblies() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\",\"maxDepth\":2}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pools/castle/target.json",
"{\"pieces\":[{\"piece\":\"castle/target\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"castle/target\",\"name\":\"source\",\"targetName\":\"door\"}]}");
write(pack, "jigsaw-pieces/castle/target.json",
"{\"object\":\"castle/target\",\"rotatable\":false,\"connectors\":[{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"castle/target\",\"name\":\"door\",\"targetName\":\"unused\"}]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 1, 1, 1);
writeObjectHeader(pack.resolve("objects/castle/target.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().stream().anyMatch(
message -> message.contains("does not produce a complete deterministic assembly")));
}
@Test
public void inactiveLibraryGraphDoesNotEnterTheRuntimeGate() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/library.json", "{\"startPool\":\"library/start\",\"maxDepth\":2}");
write(pack, "jigsaw-pools/library/start.json",
"{\"pieces\":[{\"piece\":\"library/start\"}]}");
write(pack, "jigsaw-pools/library/target.json",
"{\"pieces\":[{\"piece\":\"library/target\"}]}");
write(pack, "jigsaw-pieces/library/start.json",
"{\"object\":\"library/start\",\"connectors\":[{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"library/target\",\"name\":\"source\",\"targetName\":\"door\"}]}");
write(pack, "jigsaw-pieces/library/target.json",
"{\"object\":\"library/target\",\"rotatable\":false,\"connectors\":[{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"library/target\",\"name\":\"door\",\"targetName\":\"unused\"}]}");
writeObjectHeader(pack.resolve("objects/library/start.iob"), 1, 1, 1);
writeObjectHeader(pack.resolve("objects/library/target.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation =
StructureGraphPackValidator.validate(pack, Set.of());
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
assertTrue(validation.warnings().toString(), validation.warnings().isEmpty());
}
@Test
public void rejectsTruncatedObjectsBeforeRuntime() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
Path object = pack.resolve("objects/castle/start.iob");
writeTruncatedObjectHeader(object, 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertEquals(List.of("Malformed Iris object resource " + object
+ ": EOFException"), validation.errors());
}
@Test
public void doesNotReadMalformedObjectsOutsideTheActiveGraph() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/library.json", "{\"startPool\":\"library/start\"}");
write(pack, "jigsaw-pools/library/start.json",
"{\"pieces\":[{\"piece\":\"library/start\"}]}");
write(pack, "jigsaw-pieces/library/start.json",
"{\"object\":\"library/start\",\"connectors\":[]}");
writeTruncatedObjectHeader(pack.resolve("objects/library/start.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation =
StructureGraphPackValidator.validate(pack, Set.of());
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
}
@Test
public void acceptsSignedObjectCoordinates() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
writeSignedObject(pack.resolve("objects/castle/start.iob"), 18, 31, 41, -9, -15, -20);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertFalse(validation.errors().toString(),
validation.errors().stream().anyMatch(message -> message.contains("missing object")));
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
}
@Test
public void capturesSampledVerticalEnvelopesFromRuntimeAssembly() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 3, 9, 3);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
List<StructureGraphPackValidator.SampledVerticalEnvelope> envelopes =
validation.sampledVerticalEnvelopes().get("castle");
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
assertEquals(16, envelopes.size());
assertTrue(envelopes.stream().allMatch(envelope -> envelope.pieceCount() == 1
&& envelope.minimumYOffset() == -4
&& envelope.maximumYOffset() == 4));
assertThrows(UnsupportedOperationException.class,
() -> envelopes.add(new StructureGraphPackValidator.SampledVerticalEnvelope(1L, 1, 0, 0)));
assertThrows(UnsupportedOperationException.class,
() -> validation.sampledVerticalEnvelopes().put("other", List.of()));
}
@Test
public void treatsAnEmptyTargetPoolAsIntentionalTermination() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pools/castle/empty.json", "{\"pieces\":[]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"pool\":\"castle/empty\","
+ "\"name\":\"source\",\"targetName\":\"door\"}]}" );
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
}
@Test
public void acceptsIntentionalEmptyStartButDoesNotApproveItAsAReplacement() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"empty\":true,\"weight\":1}]}");
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
assertFalse(validation.replacementOutputStructures().contains("castle"));
}
@Test
public void rareWeightedEmptyStartCannotApproveNativeReplacement() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\"}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\",\"weight\":2147483647},"
+ "{\"empty\":true,\"weight\":1}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 1, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
assertFalse(validation.replacementOutputStructures().contains("castle"));
}
@Test
public void oversizedStartCannotApproveNativeReplacement() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
write(pack, "structures/castle.json",
"{\"startPool\":\"castle/start\",\"maxSizeChunks\":1}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\"}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"connectors\":[]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 34, 1, 1);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().toString(), validation.errors().isEmpty());
assertFalse(validation.replacementOutputStructures().contains("castle"));
}
@Test
public void reportsSampledRuntimeGeometryFailureWithoutEscapingValidation() throws Exception {
Path pack = temporaryFolder.newFolder("pack").toPath();
String source = "{\"position\":{\"x\":1,\"y\":1,\"z\":0},"
+ "\"direction\":\"NORTH_NEGATIVE_Z\",\"top\":\"UP_POSITIVE_Y\","
+ "\"pool\":\"castle/recursive\",\"name\":\"source\",\"targetName\":\"door\","
+ "\"joint\":\"ROLLABLE\"}";
String target = "{\"position\":{\"x\":1,\"y\":1,\"z\":2},"
+ "\"direction\":\"SOUTH_POSITIVE_Z\",\"top\":\"UP_POSITIVE_Y\","
+ "\"pool\":\"castle/recursive\",\"name\":\"door\",\"targetName\":\"unused\","
+ "\"joint\":\"ROLLABLE\"}";
write(pack, "structures/castle.json",
"{\"startPool\":\"castle/start\",\"maxDepth\":30,\"maxSizeChunks\":32}");
write(pack, "jigsaw-pools/castle/start.json",
"{\"pieces\":[{\"piece\":\"castle/start\",\"weight\":1}]}");
write(pack, "jigsaw-pools/castle/recursive.json",
"{\"pieces\":[{\"piece\":\"castle/recursive\",\"weight\":1}]}");
write(pack, "jigsaw-pieces/castle/start.json",
"{\"object\":\"castle/start\",\"rotatable\":false,\"connectors\":[" + source + "]}");
write(pack, "jigsaw-pieces/castle/recursive.json",
"{\"object\":\"castle/recursive\",\"rotatable\":false,\"connectors\":["
+ target + "," + source + "," + source + "]}");
writeObjectHeader(pack.resolve("objects/castle/start.iob"), 3, 3, 3);
writeObjectHeader(pack.resolve("objects/castle/recursive.iob"), 3, 3, 3);
StructureGraphPackValidator.Validation validation = StructureGraphPackValidator.validate(pack);
assertTrue(validation.errors().toString(), validation.errors().stream().anyMatch(
message -> message.contains("fails sampled collision- and radius-aware assembly")
&& message.contains("seed 0")));
}
private void write(Path root, String relativePath, String content) throws Exception {
Path file = root.resolve(relativePath);
Files.createDirectories(file.getParent());
Files.writeString(file, content, StandardCharsets.UTF_8);
}
private void writeObjectHeader(Path file, int width, int height, int depth) throws Exception {
Files.createDirectories(file.getParent());
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(file))) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
output.writeUTF("Iris V2 IOB;");
output.writeShort(0);
output.writeInt(0);
output.writeInt(0);
}
}
private void writeTruncatedObjectHeader(Path file, int width, int height, int depth) throws Exception {
Files.createDirectories(file.getParent());
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(file))) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
}
}
private void writeSignedObject(
Path file,
int width,
int height,
int depth,
int x,
int y,
int z
) throws Exception {
Files.createDirectories(file.getParent());
try (DataOutputStream output = new DataOutputStream(Files.newOutputStream(file))) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
output.writeUTF("Iris V2 IOB;");
output.writeShort(1);
output.writeUTF("minecraft:stone");
output.writeInt(1);
output.writeShort(x);
output.writeShort(y);
output.writeShort(z);
output.writeShort(0);
output.writeInt(0);
}
}
}
@@ -0,0 +1,254 @@
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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
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.assertTrue;
import static org.junit.Assert.fail;
public class StructurePackageClosureTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void collectsAndWritesCompleteReachableGraph() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/castle.json", "{\"startPool\":\"castle/start\",\"loot\":[\"castle/chests\"]}");
write(pack, "jigsaw-pools/castle/start.json", "{\"pieces\":[{\"piece\":\"castle/start\",\"weight\":1}],\"fallback\":\"castle/end\"}");
write(pack, "jigsaw-pools/castle/branch.json", "{\"pieces\":[{\"piece\":\"castle/end\",\"weight\":1}]}");
write(pack, "jigsaw-pools/castle/end.json", "{\"pieces\":[{\"piece\":\"castle/end\",\"weight\":1}]}");
write(pack, "jigsaw-pieces/castle/start.json", "{\"object\":\"castle/start\",\"connectors\":[{\"pool\":\"castle/branch\"}]}");
write(pack, "jigsaw-pieces/castle/end.json", "{\"object\":\"castle/end\",\"connectors\":[]}");
write(pack, "objects/castle/start.iob", "start-object");
write(pack, "objects/castle/end.iob", "end-object");
write(pack, "loot/castle/chests.json", "{\"tables\":[]}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("castle"));
assertTrue(closure.errors().toString(), closure.isValid());
assertEquals(Set.of("castle"), closure.structures());
assertEquals(Set.of("castle/start", "castle/branch", "castle/end"), closure.pools());
assertEquals(Set.of("castle/start", "castle/end"), closure.pieces());
assertEquals(Set.of("castle/start", "castle/end"), closure.objects());
assertEquals(Set.of("castle/chests"), closure.loot());
File output = temporaryFolder.newFolder("output");
closure.writeTo(output, true);
assertTrue(new File(output, "structures/castle.json").isFile());
assertTrue(new File(output, "jigsaw-pools/castle/branch.json").isFile());
assertTrue(new File(output, "jigsaw-pieces/castle/end.json").isFile());
assertTrue(new File(output, "objects/castle/end.iob").isFile());
assertTrue(new File(output, "loot/castle/chests.json").isFile());
}
@Test
public void excludesUnreachableStructures() throws Exception {
File pack = temporaryFolder.newFolder("pack");
writeSinglePieceStructure(pack, "selected");
writeSinglePieceStructure(pack, "unrelated");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("selected"));
assertTrue(closure.errors().toString(), closure.isValid());
assertEquals(Set.of("selected"), closure.structures());
assertFalse(closure.objects().contains("unrelated/piece"));
}
@Test
public void rejectsMissingDependenciesBeforeWriting() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/broken.json", "{\"startPool\":\"broken/missing\"}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("broken"));
assertFalse(closure.isValid());
assertEquals(List.of("Missing jigsaw-pools resource 'broken/missing'."), closure.errors());
try {
closure.writeTo(temporaryFolder.newFolder("output"), false);
fail("Invalid closure must not be written");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("broken/missing"));
}
}
@Test
public void rejectsTraversalKeys() throws Exception {
File pack = temporaryFolder.newFolder("pack");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("../outside"));
assertFalse(closure.isValid());
assertEquals(List.of("Missing or invalid structure."), closure.errors());
assertTrue(closure.structures().isEmpty());
}
@Test
public void rejectsNonCanonicalAndNonPortableKeys() throws Exception {
List<String> invalidKeys = List.of(
" leading",
"trailing ",
"folder\\structure",
"minecraft:structure",
"folder/con",
"folder/trailing.",
"folder/control\u007fkey"
);
for (int index = 0; index < invalidKeys.size(); index++) {
File pack = temporaryFolder.newFolder("invalid-key-" + index);
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of(invalidKeys.get(index)));
assertFalse(invalidKeys.get(index), closure.isValid());
assertEquals(invalidKeys.get(index), List.of("Missing or invalid structure."), closure.errors());
}
}
@Test
public void rejectsCaseInsensitiveResourceCollisions() throws Exception {
File pack = temporaryFolder.newFolder("pack");
writeSinglePieceStructure(pack, "Castle");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("Castle", "castle"));
assertFalse(closure.isValid());
assertTrue(closure.errors().toString(), closure.errors().stream()
.anyMatch(error -> error.contains("Case-insensitive structures resource collision")));
}
@Test
public void rejectsMismatchedSourcePathCasing() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "Structures/case.json", "{\"startPool\":\"case/start\"}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("case"));
assertFalse(closure.isValid());
}
@Test
public void rejectsWrongStructureAndPoolFieldShapes() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/broken.json", "{\"startPool\":7,\"loot\":{}}");
write(pack, "structures/pool-shapes.json", "{\"startPool\":\"pool-shapes/start\"}");
write(pack, "jigsaw-pools/pool-shapes/start.json", "{\"pieces\":{},\"fallback\":[]}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("broken", "pool-shapes"));
assertFalse(closure.isValid());
assertTrue(closure.errors().toString(), closure.errors().contains("Structure 'broken' requires string field 'startPool'."));
assertTrue(closure.errors().toString(), closure.errors().contains("Structure 'broken' requires array field 'loot'."));
assertTrue(closure.errors().toString(), closure.errors().contains("Jigsaw pool 'pool-shapes/start' requires array field 'pieces'."));
assertTrue(closure.errors().toString(), closure.errors().contains("Jigsaw pool 'pool-shapes/start' requires string field 'fallback'."));
}
@Test
public void rejectsMalformedPoolEntryShapes() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/broken.json", "{\"startPool\":\"broken/start\"}");
write(pack, "jigsaw-pools/broken/start.json",
"{\"pieces\":[{},{\"empty\":\"true\"},{\"empty\":true,\"piece\":\"broken/piece\"}]}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("broken"));
assertFalse(closure.isValid());
assertTrue(closure.errors().toString(), closure.errors().contains(
"Piece entry 0 in jigsaw pool 'broken/start' requires string field 'piece'."));
assertTrue(closure.errors().toString(), closure.errors().contains(
"Piece entry 1 in jigsaw pool 'broken/start' requires boolean field 'empty'."));
assertTrue(closure.errors().toString(), closure.errors().contains(
"Empty piece entry 2 in jigsaw pool 'broken/start' cannot define field 'piece'."));
}
@Test
public void acceptsExplicitEmptyPoolEntry() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/empty.json", "{\"startPool\":\"empty/start\"}");
write(pack, "jigsaw-pools/empty/start.json", "{\"pieces\":[{\"empty\":true,\"weight\":3}]}");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("empty"));
assertTrue(closure.errors().toString(), closure.isValid());
assertEquals(Set.of("empty/start"), closure.pools());
assertTrue(closure.pieces().isEmpty());
assertTrue(closure.objects().isEmpty());
}
@Test
public void rejectsWrongPieceAndConnectorFieldShapes() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "structures/broken.json", "{\"startPool\":\"broken/start\"}");
write(pack, "jigsaw-pools/broken/start.json", "{\"pieces\":[{\"piece\":\"broken/shape\",\"weight\":1},{\"piece\":\"broken/connector\",\"weight\":1}]}");
write(pack, "jigsaw-pieces/broken/shape.json", "{\"object\":{},\"connectors\":{}}");
write(pack, "jigsaw-pieces/broken/connector.json", "{\"object\":\"broken/connector\",\"connectors\":[{},7]}");
write(pack, "objects/broken/connector.iob", "object");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("broken"));
assertFalse(closure.isValid());
assertTrue(closure.errors().toString(), closure.errors().contains("Jigsaw piece 'broken/shape' requires string field 'object'."));
assertTrue(closure.errors().toString(), closure.errors().contains("Jigsaw piece 'broken/shape' requires array field 'connectors'."));
assertTrue(closure.errors().toString(), closure.errors().contains(
"Connector 0 in jigsaw piece 'broken/connector' requires string field 'pool'."));
assertTrue(closure.errors().toString(), closure.errors().contains(
"Jigsaw piece 'broken/connector' has a non-object connector at index 1."));
}
@Test
public void rejectsSourceSymlinksOutsidePack() throws Exception {
File pack = temporaryFolder.newFolder("pack");
File outside = temporaryFolder.newFolder("outside");
write(outside, "escape.json", "{\"startPool\":\"escape/start\"}");
Path link = pack.toPath().resolve("structures/escape.json");
Files.createDirectories(link.getParent());
Files.createSymbolicLink(link, outside.toPath().resolve("escape.json"));
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("escape"));
assertFalse(closure.isValid());
assertTrue(closure.errors().toString(), closure.errors().stream()
.anyMatch(error -> error.contains("escapes the structure package through a symbolic link")));
}
@Test
public void rejectsTargetSymlinks() throws Exception {
File pack = temporaryFolder.newFolder("pack");
writeSinglePieceStructure(pack, "selected");
StructurePackageClosure closure = StructurePackageClosure.collect(pack, List.of("selected"));
File output = temporaryFolder.newFolder("output");
File outside = temporaryFolder.newFolder("outside");
Files.createSymbolicLink(output.toPath().resolve("structures"), outside.toPath());
try {
closure.writeTo(output, true);
fail("Symbolic link target must not be written");
} catch (IOException expected) {
assertTrue(expected.getMessage(), expected.getMessage().contains("symbolic link"));
}
assertEquals(0, outside.list().length);
}
private void writeSinglePieceStructure(File pack, String key) throws Exception {
write(pack, "structures/" + key + ".json", "{\"startPool\":\"" + key + "/start\"}");
write(pack, "jigsaw-pools/" + key + "/start.json", "{\"pieces\":[{\"piece\":\"" + key + "/piece\",\"weight\":1}]}");
write(pack, "jigsaw-pieces/" + key + "/piece.json", "{\"object\":\"" + key + "/piece\",\"connectors\":[]}");
write(pack, "objects/" + key + "/piece.iob", key);
}
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,39 @@
package art.arcane.iris.core.project;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class NativeStructureSuppressionSchemaTest {
@Test
public void suppressionTypeAndValuesAreSchemaDescribed() throws NoSuchFieldException {
assertNotNull(NativeStructureSuppression.class.getAnnotation(Desc.class));
assertNotNull(NativeStructureSuppression.class.getField("NONE").getAnnotation(Desc.class));
assertNotNull(NativeStructureSuppression.class.getField("REPLACE_SOURCE").getAnnotation(Desc.class));
JSONObject schema = new SchemaBuilder(SuppressionModel.class, null).construct();
JSONObject suppression = schema.getJSONObject("properties").getJSONObject("suppression");
String definitionKey = suppression.getString("$ref").substring("#/definitions/".length());
JSONArray values = schema.getJSONObject("definitions")
.getJSONObject(definitionKey)
.getJSONArray("oneOf");
assertEquals("NONE", values.getJSONObject(0).getString("const"));
assertEquals("Leaves native generation enabled alongside this Iris structure placement.",
values.getJSONObject(0).getString("description"));
assertEquals("REPLACE_SOURCE", values.getJSONObject(1).getString("const"));
assertEquals("Suppresses the structure's vanillaSource when used by a validated dimension-level replacement placement.",
values.getJSONObject(1).getString("description"));
}
@Desc("Schema model for native structure suppression.")
public static class SuppressionModel {
@Desc("Native structure suppression behavior.")
private NativeStructureSuppression suppression = NativeStructureSuppression.NONE;
}
}
@@ -19,10 +19,14 @@
package art.arcane.iris.core.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisExpression;
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.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
@@ -30,6 +34,7 @@ import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.LogLevel;
@@ -42,6 +47,8 @@ import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
@@ -56,6 +63,7 @@ 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.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -64,6 +72,7 @@ public class SchemaBuilderParityTest {
private static final List<String> ENCHANT_KEYS = List.of("minecraft:sharpness", "cool_mod:vorpal");
private static final List<String> ITEM_KEYS = List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
private static final List<String> ENTITY_KEYS = List.of("minecraft:zombie", "cool_mod:grizzly_bear");
private static final List<String> STRUCTURE_KEYS = List.of("minecraft:monument", "minecraft:stronghold", "cool_mod:sky_temple");
private static final List<String> EXPECTED_POTIONS = List.of("SPEED", "SLOW_FALLING", "MEGA_BOOST");
private static final List<String> EXPECTED_ENCHANTS = List.of("sharpness", "vorpal");
@@ -117,25 +126,56 @@ public class SchemaBuilderParityTest {
}
@Test
public void structurePlacementSchemaOmitsUnsupportedTransforms() {
public void structurePlacementSchemaIncludesFoundationSettingsAndOmitsUnsupportedTransforms() {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
ResourceLoader<IrisExpression> expressionLoader = mock(ResourceLoader.class);
ResourceLoader<IrisBlockData> blockLoader = mock(ResourceLoader.class);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisExpression.class, expressionLoader);
when(data.getStructureLoader()).thenReturn(structureLoader);
when(data.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(data.getBlockLoader()).thenReturn(blockLoader);
when(data.getLoaders()).thenReturn(loaders);
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
when(structureLoader.getPossibleKeys()).thenReturn(new String[0]);
when(pieceLoader.getPossibleKeys()).thenReturn(new String[0]);
when(blockLoader.getPossibleKeys()).thenReturn(new String[0]);
when(expressionLoader.getPossibleKeys()).thenReturn(new String[0]);
when(expressionLoader.getFolderName()).thenReturn("expressions");
when(expressionLoader.getResourceTypeName()).thenReturn("Expression");
JSONObject properties = new SchemaBuilder(IrisStructurePlacement.class, data)
.construct().getJSONObject("properties");
JSONObject schema = new SchemaBuilder(IrisStructurePlacement.class, data).construct();
JSONObject properties = schema.getJSONObject("properties");
JSONObject stilt = properties.getJSONObject("stilt");
JSONObject stiltDefinition = schema.getJSONObject("definitions")
.getJSONObject(stilt.getString("$ref").substring("#/definitions/".length()));
JSONObject stiltProperties = stiltDefinition.getJSONObject("properties");
JSONObject maxDepth = stiltProperties.getJSONObject("maxDepth");
assertTrue(properties.has("structures"));
assertTrue(properties.has("distribution"));
assertEquals("object", stilt.getString("type"));
assertTrue(stiltProperties.has("palette"));
assertEquals(1, maxDepth.getInt("minimum"));
assertEquals(4064, maxDepth.getInt("maximum"));
assertFalse(properties.has("rotation"));
assertFalse(properties.has("translate"));
assertFalse(properties.has("scale"));
}
@Test
public void structurePolicyArraysUseLiveRegistryKeys() {
JSONObject schema = new SchemaBuilder(StructureArrayModel.class, (IrisData) null).construct();
JSONObject disabled = schema.getJSONObject("properties").getJSONObject("disabled");
assertEquals("#/definitions/enum-vanilla-structure",
disabled.getJSONObject("items").getString("$ref"));
assertEquals(STRUCTURE_KEYS,
enumValues(schema.getJSONObject("definitions"), "enum-vanilla-structure"));
}
private static String flavorDefinitionKey() {
return "enum-" + Flavor.class.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
}
@@ -182,6 +222,14 @@ public class SchemaBuilderParityTest {
private Flavor flavor = Flavor.ALPHA;
}
@Desc("Structure array model.")
public static class StructureArrayModel {
@Desc("Disabled structures.")
@ArrayType(type = String.class)
@RegistryListVanillaStructure
private KList<String> disabled = new KList<>();
}
public enum Flavor {
ALPHA,
BETA
@@ -240,7 +288,7 @@ public class SchemaBuilderParityTest {
@Override
public List<String> structureKeys() {
return List.of();
return STRUCTURE_KEYS;
}
@Override
@@ -0,0 +1,85 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.scoreboard.Scoreboard;
import org.junit.Test;
import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class BoardSVCStudioEligibilityTest {
@Test
public void productionIrisGeneratorIsNotEligible() {
PlatformChunkGenerator generator = generator(false, false, mock(Engine.class));
assertFalse(BoardSVC.isStudioGeneratorEligible(generator));
}
@Test
public void activeStudioGeneratorIsEligible() {
PlatformChunkGenerator generator = generator(true, false, mock(Engine.class));
assertTrue(BoardSVC.isStudioGeneratorEligible(generator));
}
@Test
public void studioGeneratorWithoutEngineIsNotEligible() {
PlatformChunkGenerator generator = generator(true, false, null);
assertFalse(BoardSVC.isStudioGeneratorEligible(generator));
}
@Test
public void closingStudioGeneratorIsNotEligible() {
PlatformChunkGenerator generator = generator(true, true, mock(Engine.class));
assertFalse(BoardSVC.isStudioGeneratorEligible(generator));
}
@Test
public void playerToggleIsSessionScopedAndDefaultsVisible() {
BoardSVC service = new BoardSVC();
UUID playerId = UUID.randomUUID();
assertTrue(service.isPlayerBoardEnabled(playerId));
assertFalse(service.togglePlayerBoard(playerId));
assertFalse(service.isPlayerBoardEnabled(playerId));
assertTrue(service.togglePlayerBoard(playerId));
assertTrue(service.isPlayerBoardEnabled(playerId));
service.togglePlayerBoard(playerId);
service.clearPlayerPreference(playerId);
assertTrue(service.isPlayerBoardEnabled(playerId));
}
@Test
public void restoresPreviousScoreboardWhileIrisStillOwnsThePlayer() {
Scoreboard previous = mock(Scoreboard.class);
Scoreboard iris = mock(Scoreboard.class);
assertSame(previous, BoardSVC.selectScoreboardToRestore(iris, iris, previous));
}
@Test
public void preservesScoreboardAssignedAfterIris() {
Scoreboard previous = mock(Scoreboard.class);
Scoreboard iris = mock(Scoreboard.class);
Scoreboard replacement = mock(Scoreboard.class);
assertSame(replacement, BoardSVC.selectScoreboardToRestore(replacement, iris, previous));
}
private PlatformChunkGenerator generator(boolean studio, boolean closing, Engine engine) {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
doReturn(studio).when(generator).isStudio();
doReturn(closing).when(generator).isClosing();
doReturn(engine).when(generator).getEngine();
return generator;
}
}
@@ -0,0 +1,48 @@
/*
* 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.structure;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureLocateCapabilityTest {
@Test
public void monumentLocateIsUnavailableWithoutChangingGenerationPolicy() {
assertTrue(NativeStructureLocateCapability.isPaperUnavailable("minecraft:monument"));
assertTrue(NativeStructureLocateCapability.isPaperUnavailable(" MINECRAFT:MONUMENT "));
assertTrue(new IrisImportedStructureControl().shouldGenerate("minecraft:monument"));
}
@Test
public void otherStructuresRemainLocatable() {
assertFalse(NativeStructureLocateCapability.isPaperUnavailable(null));
assertFalse(NativeStructureLocateCapability.isPaperUnavailable(""));
assertFalse(NativeStructureLocateCapability.isPaperUnavailable("minecraft:stronghold"));
}
@Test
public void messageStatesThatGenerationRemainsEnabled() {
String message = NativeStructureLocateCapability.unavailableMessage().toLowerCase();
assertTrue(message.contains("generation is enabled"));
assertTrue(message.contains("locat"));
}
}
@@ -0,0 +1,33 @@
package art.arcane.iris.core.structure;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class StructureCaptureImporterFailureTest {
@Test
public void captureFailurePreservesStructureChunkDetailAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("placement broke");
IllegalStateException error = StructureCaptureImporter.captureFailure(
"minecraft:monument", 12, -8, "platform placement failed", cause);
assertSame(cause, error.getCause());
assertTrue(error.getMessage().contains("minecraft:monument"));
assertTrue(error.getMessage().contains("12,-8"));
assertTrue(error.getMessage().contains("platform placement failed"));
}
@Test
public void captureFailureSupportsInfrastructureFailuresWithoutACause() {
IllegalStateException error = StructureCaptureImporter.captureFailure(
"minecraft:stronghold", -3, 5, "region task was not accepted", null);
assertNull(error.getCause());
assertTrue(error.getMessage().contains("minecraft:stronghold"));
assertTrue(error.getMessage().contains("-3,5"));
assertTrue(error.getMessage().contains("region task was not accepted"));
}
}
@@ -0,0 +1,70 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.structure.Palette;
import org.bukkit.structure.Structure;
import org.bukkit.util.BlockVector;
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.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureImporterCaptureTest {
@Test
public void captureReturnsAnInMemorySnapshotWithDistinctBlockMarkerLoss() {
Structure structure = mock(Structure.class);
Palette palette = mock(Palette.class);
BlockState marker = mock(BlockState.class);
BlockData markerData = mock(BlockData.class);
Location location = mock(Location.class);
when(structure.getSize()).thenReturn(new BlockVector(2, 3, 4));
when(structure.getPalettes()).thenReturn(List.of(palette));
when(structure.getPaletteCount()).thenReturn(2);
when(structure.getEntityCount()).thenReturn(1);
when(palette.getBlocks()).thenReturn(List.of(marker));
when(marker.getLocation()).thenReturn(location);
when(marker.getBlockData()).thenReturn(markerData);
when(markerData.getMaterial()).thenReturn(Material.STRUCTURE_BLOCK);
StructureImporter.CapturedStructure captured = StructureImporter.captureStructure(structure);
assertEquals(2, captured.width());
assertEquals(3, captured.height());
assertEquals(4, captured.depth());
assertEquals(1, captured.structureMarkers());
assertEquals(0, captured.blocks());
assertTrue(captured.capabilities().contains(StructureCapability.BLOCKS));
assertFalse(captured.capabilities().contains(StructureCapability.CONNECTORS));
assertTrue(captured.losses().stream().anyMatch(loss ->
loss.capability() == StructureCapability.BLOCKS
&& loss.code().equals("structure_markers_resolved")));
assertFalse(captured.losses().stream().anyMatch(loss -> loss.code().equals("connectors_not_imported")));
}
}
@@ -22,18 +22,20 @@ import org.bukkit.NamespacedKey;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class StructureImporterModeTest {
@Test
public void parseModeDefaultsToOverwriteForNull() {
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode(null));
public void parseModeDefaultsToAddOnlyForNull() {
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode(null));
}
@Test
public void parseModeDefaultsToOverwriteForUnknownAndEmpty() {
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode(""));
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("garbage"));
public void parseModeDefaultsToAddOnlyForEmptyAndRejectsUnknown() {
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode(""));
assertThrows(IllegalArgumentException.class, () -> StructureImporter.parseMode("garbage"));
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("overwrite"));
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("replace"));
}
@Test
@@ -47,8 +49,8 @@ public class StructureImporterModeTest {
@Test
public void parseModeIsCaseInsensitive() {
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("ADD"));
assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("MERGE"));
assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("merge"));
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("OVERWRITE"));
assertThrows(IllegalArgumentException.class, () -> StructureImporter.parseMode("MERGE"));
}
@Test
@@ -1,21 +1,3 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
import art.arcane.volmlib.util.collection.KList;
@@ -31,127 +13,59 @@ import static org.junit.Assert.assertTrue;
public class StructureSchemaKeysTest {
@Test
public void vanillaKeysAreNormalizedToIrisLoadKeyForm() {
public void exposesOnlyExistingIrisStructureResources() {
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("minecraft:village_plains", "minecraft:stronghold", "minecraft:ancient_city"),
Collections.emptyList(),
Arrays.asList("minecraft_stronghold", "custom/cool_tower"),
Collections.emptyList());
assertTrue(result.contains("minecraft_village_plains"));
assertTrue(result.contains("minecraft_stronghold"));
assertTrue(result.contains("minecraft_ancient_city"));
assertEquals(Arrays.asList("custom/cool_tower", "minecraft_stronghold"), result);
}
@Test
public void strongholdIsAlwaysOfferedEvenWithoutAnImportedFile() {
public void nativeRegistryKeysAreNotSynthesized() {
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("minecraft:stronghold", "minecraft:fortress", "minecraft:monument"),
Collections.emptyList(),
Collections.singletonList("custom/castle"),
Collections.emptyList());
assertTrue(result.contains("minecraft_stronghold"));
assertTrue(result.contains("minecraft_fortress"));
assertTrue(result.contains("minecraft_monument"));
}
@Test
public void datapackStructuresAreNormalizedAndIncluded() {
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("nova_structures:temple", "Nova_Structures:Big/Castle"),
Collections.emptyList(),
Collections.emptyList());
assertTrue(result.contains("nova_structures_temple"));
assertTrue(result.contains("nova_structures_big_castle"));
assertFalse(result.contains("minecraft_stronghold"));
assertFalse(result.contains("minecraft:stronghold"));
assertTrue(result.contains("custom/castle"));
}
@Test
public void jigsawComponentPiecesAreExcluded() {
List<String> pieces = Arrays.asList(
"village/taiga/houses/taiga_small_house_5",
"bastion/units/rampart_plates/plate_0",
"village/common/animals/cat_black",
"trial_chambers/corridor/straight_1",
"end_city/third_floor_1",
"igloo/middle");
"trial_chambers/corridor/straight_1");
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("minecraft:village_taiga", "minecraft:bastion_remnant"),
pieces,
Arrays.asList("village/taiga/houses/taiga_small_house_5", "custom/castle"),
pieces);
for (String piece : pieces) {
assertFalse("component piece must not be a valid structure key: " + piece, result.contains(piece));
}
assertTrue(result.contains("minecraft_village_taiga"));
assertTrue(result.contains("minecraft_bastion_remnant"));
}
@Test
public void importedCustomStructuresAreKept() {
KList<String> result = StructureSchemaKeys.collect(
Collections.emptyList(),
Arrays.asList("mypack_castle", "custom/cool_tower"),
Collections.emptyList());
assertTrue(result.contains("mypack_castle"));
assertTrue(result.contains("custom/cool_tower"));
}
@Test
public void importedStructureThatCollidesWithAPieceIsDropped() {
KList<String> result = StructureSchemaKeys.collect(
Collections.emptyList(),
Arrays.asList("village/taiga/houses/taiga_small_house_5", "minecraft_village_plains"),
Collections.singletonList("village/taiga/houses/taiga_small_house_5"));
assertFalse(result.contains("village/taiga/houses/taiga_small_house_5"));
assertTrue(result.contains("minecraft_village_plains"));
assertTrue(result.contains("custom/castle"));
}
@Test
public void vanillaAndImportedEquivalentsDeduplicate() {
public void resultIsSortedAndDeduplicated() {
KList<String> result = StructureSchemaKeys.collect(
Collections.singletonList("minecraft:ancient_city"),
Collections.singletonList("minecraft_ancient_city"),
Arrays.asList("zeta", "alpha", "zeta"),
Collections.emptyList());
int occurrences = 0;
for (String key : result) {
if (key.equals("minecraft_ancient_city")) {
occurrences++;
}
}
assertEquals(1, occurrences);
}
@Test
public void resultIsSorted() {
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("minecraft:zombie_village", "minecraft:ancient_city", "minecraft:monument"),
Collections.emptyList(),
Collections.emptyList());
KList<String> sorted = new KList<>(result);
Collections.sort(sorted);
assertEquals(sorted, result);
assertEquals(Arrays.asList("alpha", "zeta"), result);
}
@Test
public void nullCollectionsAreHandled() {
KList<String> result = StructureSchemaKeys.collect(null, null, null);
assertTrue(result.isEmpty());
assertTrue(StructureSchemaKeys.collect(null, null).isEmpty());
}
@Test
public void blankAndNullEntriesAreIgnored() {
KList<String> result = StructureSchemaKeys.collect(
Arrays.asList("minecraft:village_plains", "", null, " "),
Arrays.asList("custom_one", "", null),
Arrays.asList("custom_one", "", null, " "),
Arrays.asList("", null));
assertTrue(result.contains("minecraft_village_plains"));
assertTrue(result.contains("custom_one"));
assertEquals(2, result.size());
assertEquals(Collections.singletonList("custom_one"), result);
}
}
@@ -0,0 +1,168 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
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.Files;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class VillageImporterBundleTest {
private static final StructureKey BUNDLE_KEY = StructureKey.parse("iris:minecraft_village_plains");
private static final StructureSource SOURCE = new StructureSource(
StructureSource.Kind.VANILLA,
StructureKey.parse("minecraft:village_plains"),
"1.21.9-R0.1-SNAPSHOT",
""
);
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void bundleOwnsObjectsPiecesPoolsAndRootAsOneTransaction() throws IOException {
Path root = temporaryFolder.newFolder("village-bundle").toPath();
StructureResourceBundle bundle = bundle("object-v1");
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.write(bundle, StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.ADDED, result.status());
assertEquals(4, bundle.resources().size());
assertTrue(bundle.resources().containsKey("objects/minecraft_village_plains/piece/minecraft/house.iob"));
assertTrue(bundle.resources().containsKey("jigsaw-pieces/minecraft_village_plains/piece/minecraft/house.json"));
assertTrue(bundle.resources().containsKey("jigsaw-pools/minecraft_village_plains/pool/minecraft/houses.json"));
assertTrue(bundle.resources().containsKey("structures/minecraft_village_plains.json"));
StructureOwnershipManifest manifest = StructureOwnershipManifest.fromJson(
Files.readAllBytes(writer.ownershipManifestPath(BUNDLE_KEY))
);
assertEquals(SOURCE, manifest.source());
assertEquals(StructureBackend.IRIS_ASSEMBLY, manifest.backend());
assertEquals(bundle.resources().keySet(), manifest.resourceHashes().keySet());
}
@Test
public void overwriteRefusesOneModifiedVillageResourceAndPreservesTheWholeClosure() throws IOException {
Path root = temporaryFolder.newFolder("village-overwrite").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult initial = writer.write(bundle("object-v1"), StructureWriteMode.ADD_ONLY);
Path object = root.resolve("objects/minecraft_village_plains/piece/minecraft/house.iob");
Path structure = root.resolve("structures/minecraft_village_plains.json");
byte[] originalStructure = Files.readAllBytes(structure);
byte[] handEdit = "hand-edit".getBytes(StandardCharsets.UTF_8);
Files.write(object, handEdit);
StructureWriteResult overwrite = writer.write(bundle("object-v2"), StructureWriteMode.OVERWRITE);
assertEquals(StructureWriteResult.Status.ADDED, initial.status());
assertEquals(StructureWriteResult.Status.OWNERSHIP_CONFLICT, overwrite.status());
assertArrayEquals(handEdit, Files.readAllBytes(object));
assertArrayEquals(originalStructure, Files.readAllBytes(structure));
}
@Test
public void sourcePathsDoNotCollapseDifferentNamespacedKeys() {
String nestedPath = VillageImporter.pieceName("village", "mod:a/b");
String underscoredNamespace = VillageImporter.pieceName("village", "mod_a:b");
assertEquals("village/piece/mod/a/b", nestedPath);
assertEquals("village/piece/mod_a/b", underscoredNamespace);
assertNotEquals(nestedPath, underscoredNamespace);
}
@Test
public void nativeMaximumDistanceControlsIrisAssemblyRadius() {
Map<String, Object> structure = VillageImporter.structureJson(
"minecraft:village_plains",
"village/pool/minecraft/start",
6,
81
);
assertEquals(6, structure.get("maxDepth"));
assertEquals(6, structure.get("maxSizeChunks"));
}
private StructureResourceBundle bundle(String objectContent) {
String pieceKey = "minecraft_village_plains/piece/minecraft/house";
String poolKey = "minecraft_village_plains/pool/minecraft/houses";
Map<String, byte[]> objects = Map.of(pieceKey, objectContent.getBytes(StandardCharsets.UTF_8));
Map<String, Object> piece = new LinkedHashMap<>();
piece.put("object", pieceKey);
piece.put("connectors", List.of());
piece.put("rotatable", true);
Map<String, Map<String, Object>> pieces = Map.of(pieceKey, piece);
Map<String, Object> poolEntry = new LinkedHashMap<>();
poolEntry.put("piece", pieceKey);
poolEntry.put("weight", 1);
Map<String, Object> pool = new LinkedHashMap<>();
pool.put("pieces", List.of(poolEntry));
Map<String, Map<String, Object>> pools = Map.of(poolKey, pool);
Map<String, Object> structure = VillageImporter.structureJson(
SOURCE.key().value(),
poolKey,
6,
80
);
Set<StructureCapability> capabilities = EnumSet.of(
StructureCapability.BLOCKS,
StructureCapability.CONNECTORS,
StructureCapability.IRIS_PLACEMENT
);
List<StructureLoss> losses = List.of(StructureLoss.warning(
StructureCapability.NATIVE_PLACEMENT,
"native_placement_settings_not_imported",
"Native placement metadata outside the Iris assembly contract was not imported."
));
return VillageImporter.buildBundle(
BUNDLE_KEY,
SOURCE,
objects,
pieces,
pools,
structure,
capabilities,
losses
);
}
}
@@ -0,0 +1,90 @@
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
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 java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
public class IrisDataStructureRecoveryTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisData data;
@Before
public void registerPreservationService() {
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
}
@After
public void closeDataAndServices() {
if (data != null) {
data.close();
}
IrisServices.clear();
}
@Test
public void packLoadingRecoversPreparedTransactionsBeforeCreatingLoaders() throws Exception {
File packDirectory = temporaryFolder.newFolder("pack");
Path packRoot = packDirectory.toPath();
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
byte[] replacement = "replacement".getBytes(StandardCharsets.UTF_8);
Path target = packRoot.resolve("objects/temple.iob");
Files.createDirectories(target.getParent());
Files.write(target, replacement);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = packRoot.resolve(".iris/structure-staging").resolve(transactionId.toString());
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
Files.write(backup, original);
StructureTransactionJournal journal = StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256(replacement)
)
));
Files.write(transactionRoot.resolve(StructureTransactionJournal.FILE_NAME), journal.toJson());
data = IrisData.get(packDirectory);
assertArrayEquals(original, Files.readAllBytes(target));
assertFalse(Files.exists(transactionRoot));
}
private static final class NoOpPreservationRegistry implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
}
@@ -0,0 +1,56 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.authoring;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class StructureKeyTest {
@Test
public void parsesExactNamespacedPath() {
StructureKey key = StructureKey.parse("nova_structures:temple/large");
assertEquals("nova_structures", key.namespace());
assertEquals("temple/large", key.path());
assertEquals("nova_structures:temple/large", key.toString());
}
@Test
public void appliesExplicitDefaultNamespaceOnlyWhenMissing() {
assertEquals(
new StructureKey("minecraft", "village/plains"),
StructureKey.parse("village/plains", "minecraft")
);
assertEquals(
new StructureKey("modid", "tower"),
StructureKey.parse("modid:tower", "minecraft")
);
}
@Test
public void rejectsLossyOrAmbiguousKeys() {
assertThrows(IllegalArgumentException.class, () -> StructureKey.parse("village"));
assertThrows(IllegalArgumentException.class, () -> StructureKey.parse("Minecraft:village"));
assertThrows(IllegalArgumentException.class, () -> StructureKey.parse("minecraft:village:plains"));
assertThrows(IllegalArgumentException.class, () -> StructureKey.parse("minecraft:village//plains"));
assertThrows(IllegalArgumentException.class, () -> StructureKey.parse("minecraft:village/../plains"));
}
}
@@ -0,0 +1,71 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.authoring;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
public class StructureResourceBundleTest {
private static final StructureKey KEY = StructureKey.parse("iris_test:temple");
@Test
public void resourceContentIsImmutableAcrossThePublicBoundary() {
byte[] input = "original".getBytes(StandardCharsets.UTF_8);
StructureResourceBundle bundle = builder()
.resource("objects/temple.iob", input)
.build();
input[0] = 'X';
byte[] exposed = bundle.resources().get("objects/temple.iob").content();
exposed[1] = 'X';
assertArrayEquals(
"original".getBytes(StandardCharsets.UTF_8),
bundle.resources().get("objects/temple.iob").content()
);
}
@Test
public void rejectsTraversalInternalAndNonPortablePaths() {
assertThrows(IllegalArgumentException.class, () -> builder().resource("../outside.json", new byte[0]));
assertThrows(IllegalArgumentException.class, () -> builder().resource(".iris/manifest.json", new byte[0]));
assertThrows(IllegalArgumentException.class, () -> builder().resource("objects/CON.iob", new byte[0]));
assertThrows(IllegalArgumentException.class, () -> builder().resource("objects/bad:name.iob", new byte[0]));
assertThrows(IllegalArgumentException.class, () -> builder().resource("objects\\bad.iob", new byte[0]));
assertThrows(IllegalArgumentException.class, () -> builder().resource("objects/bad\u007f.iob", new byte[0]));
}
@Test
public void rejectsResourcesThatCollideOnCaseInsensitiveFileSystems() {
StructureResourceBundle.Builder builder = builder().resource("objects/Temple.iob", new byte[0]);
assertThrows(IllegalArgumentException.class, () -> builder.resource("objects/temple.iob", new byte[0]));
}
private StructureResourceBundle.Builder builder() {
return StructureResourceBundle.builder(KEY)
.source(StructureSource.of(StructureSource.Kind.IRIS, KEY))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS);
}
}
@@ -0,0 +1,544 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.authoring;
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.util.List;
import java.util.UUID;
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 StructureTransactionWriterTest {
private static final StructureKey TARGET_KEY = StructureKey.parse("iris_test:temple");
private static final StructureKey SOURCE_KEY = StructureKey.parse("minecraft:trial_chambers");
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void dryRunPerformsPreflightWithoutWriting() throws IOException {
Path root = temporaryFolder.newFolder("dry-run").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.preview(bundle("object-v1", "structure-v1"), StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.DRY_RUN, result.status());
assertEquals(StructureWriteResult.Action.ADD, result.action());
assertTrue(result.successful());
assertFalse(result.committed());
assertFalse(Files.exists(root.resolve("objects/temple.iob")));
assertFalse(Files.exists(writer.ownershipManifestPath(TARGET_KEY)));
assertFalse(Files.exists(root.resolve(".iris")));
}
@Test
public void addOnlyReportsExistingResourcesWithoutChangingThem() throws IOException {
Path root = temporaryFolder.newFolder("add-only").toPath();
Path existing = root.resolve("objects/temple.iob");
Files.createDirectories(existing.getParent());
byte[] userContent = "user-content".getBytes(StandardCharsets.UTF_8);
Files.write(existing, userContent);
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.write(bundle("object-v1", "structure-v1"), StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, result.status());
assertEquals(1, result.conflicts().size());
assertEquals(StructureWriteResult.ConflictReason.RESOURCE_EXISTS, result.conflicts().get(0).reason());
assertArrayEquals(userContent, Files.readAllBytes(existing));
assertFalse(Files.exists(writer.ownershipManifestPath(TARGET_KEY)));
}
@Test
public void ownershipManifestProtectsHandEditedResources() throws IOException {
Path root = temporaryFolder.newFolder("ownership").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult initial = writer.write(bundle("object-v1", "structure-v1"), StructureWriteMode.ADD_ONLY);
Path object = root.resolve("objects/temple.iob");
byte[] handEdit = "hand-edit".getBytes(StandardCharsets.UTF_8);
assertEquals(failureMessage(initial), StructureWriteResult.Status.ADDED, initial.status());
Files.write(object, handEdit);
StructureWriteResult result = writer.write(bundle("object-v2", "structure-v2"), StructureWriteMode.OVERWRITE);
assertEquals(failureMessage(initial), StructureWriteResult.Status.ADDED, initial.status());
assertEquals(StructureWriteResult.Status.OWNERSHIP_CONFLICT, result.status());
assertTrue(result.conflicts().stream().anyMatch(conflict ->
conflict.reason() == StructureWriteResult.ConflictReason.MODIFIED_RESOURCE
&& conflict.relativePath().equals("objects/temple.iob")));
assertArrayEquals(handEdit, Files.readAllBytes(object));
StructureOwnershipManifest manifest = StructureOwnershipManifest.fromJson(
Files.readAllBytes(writer.ownershipManifestPath(TARGET_KEY))
);
assertEquals(StructureHash.sha256("object-v1".getBytes(StandardCharsets.UTF_8)),
manifest.resourceHashes().get("objects/temple.iob"));
}
@Test
public void overwriteReplacesOnlyOwnedUnmodifiedResources() throws IOException {
Path root = temporaryFolder.newFolder("overwrite").toPath();
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult initial = writer.write(bundle("object-v1", "structure-v1"), StructureWriteMode.ADD_ONLY);
StructureWriteResult result = writer.write(bundle("object-v2", "structure-v2"), StructureWriteMode.OVERWRITE);
assertEquals(failureMessage(initial), StructureWriteResult.Status.ADDED, initial.status());
assertEquals(StructureWriteResult.Status.OVERWRITTEN, result.status());
assertEquals(StructureWriteResult.Action.OVERWRITE, result.action());
assertArrayEquals("object-v2".getBytes(StandardCharsets.UTF_8),
Files.readAllBytes(root.resolve("objects/temple.iob")));
assertArrayEquals("structure-v2".getBytes(StandardCharsets.UTF_8),
Files.readAllBytes(root.resolve("structures/temple.json")));
}
@Test
public void failedMultiFileInstallRollsBackEveryResourceAndManifest() throws IOException {
Path root = temporaryFolder.newFolder("rollback").toPath();
StructureTransactionWriter initialWriter = new StructureTransactionWriter(root);
StructureWriteResult initial = initialWriter.write(
bundle("object-v1", "structure-v1"),
StructureWriteMode.ADD_ONLY
);
Path object = root.resolve("objects/temple.iob");
Path structure = root.resolve("structures/temple.json");
Path manifest = initialWriter.ownershipManifestPath(TARGET_KEY);
assertEquals(failureMessage(initial), StructureWriteResult.Status.ADDED, initial.status());
byte[] originalObject = Files.readAllBytes(object);
byte[] originalStructure = Files.readAllBytes(structure);
byte[] originalManifest = Files.readAllBytes(manifest);
FailOnceMoveOperations operations = new FailOnceMoveOperations("staged/structures/temple.json");
StructureTransactionWriter failingWriter = new StructureTransactionWriter(root, operations);
StructureWriteResult result = failingWriter.write(
bundle("object-v2", "structure-v2"),
StructureWriteMode.OVERWRITE
);
assertEquals(StructureWriteResult.Status.ADDED, initial.status());
assertEquals(StructureWriteResult.Status.ROLLED_BACK, result.status());
assertTrue(result.failure().isPresent());
assertArrayEquals(originalObject, Files.readAllBytes(object));
assertArrayEquals(originalStructure, Files.readAllBytes(structure));
assertArrayEquals(originalManifest, Files.readAllBytes(manifest));
Path staging = root.resolve(".iris/structure-staging");
if (Files.exists(staging)) {
try (Stream<Path> paths = Files.list(staging)) {
assertEquals(0, paths.count());
}
}
}
@Test
public void preparedTransactionRecoveryRestoresBackupsAndRemovesNewTargets() throws IOException {
Path root = temporaryFolder.newFolder("prepared-recovery").toPath();
Path originalTarget = root.resolve("objects/temple.iob");
Path newTarget = root.resolve("structures/temple.json");
Files.createDirectories(originalTarget.getParent());
Files.createDirectories(newTarget.getParent());
Files.write(originalTarget, "partially-installed".getBytes(StandardCharsets.UTF_8));
Files.write(newTarget, "new-target".getBytes(StandardCharsets.UTF_8));
byte[] partialReplacement = "partially-installed".getBytes(StandardCharsets.UTF_8);
byte[] newReplacement = "new-target".getBytes(StandardCharsets.UTF_8);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
Files.write(backup, original);
writeJournal(transactionRoot, StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256(partialReplacement)
),
new StructureTransactionJournal.Target(
"structures/temple.json",
false,
"",
StructureHash.sha256(newReplacement)
)
)));
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertTrue(result.successful());
assertEquals(1, result.restoredPreparedTransactions());
assertEquals(1, result.recoveredTransactions());
assertArrayEquals("original".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(originalTarget));
assertFalse(Files.exists(newTarget));
assertFalse(Files.exists(transactionRoot));
}
@Test
public void committedTransactionRecoveryKeepsInstalledTargetsAndCleansRecoveryData() throws IOException {
Path root = temporaryFolder.newFolder("committed-recovery").toPath();
Path target = root.resolve("objects/temple.iob");
Files.createDirectories(target.getParent());
Files.write(target, "committed".getBytes(StandardCharsets.UTF_8));
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
Files.write(backup, original);
writeJournal(transactionRoot, StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256("committed".getBytes(StandardCharsets.UTF_8))
)
)).committed());
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertTrue(result.successful());
assertEquals(1, result.cleanedCommittedTransactions());
assertEquals(1, result.recoveredTransactions());
assertArrayEquals("committed".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(target));
assertFalse(Files.exists(transactionRoot));
}
@Test
public void nextOnlyPreparedJournalIsRecoveredAfterANonAtomicMoveInterruption() throws IOException {
Path root = temporaryFolder.newFolder("next-journal-recovery").toPath();
Path target = root.resolve("objects/temple.iob");
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
byte[] replacement = "replacement".getBytes(StandardCharsets.UTF_8);
Files.createDirectories(target.getParent());
Files.write(target, replacement);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
Files.write(backup, original);
StructureTransactionJournal journal = StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256(replacement)
)
));
Files.write(transactionRoot.resolve(StructureTransactionJournal.NEXT_FILE_NAME), journal.toJson());
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertTrue(result.successful());
assertArrayEquals(original, Files.readAllBytes(target));
assertFalse(Files.exists(transactionRoot));
}
@Test
public void committedRecoveryRetainsBackupsWhenInstalledTargetsDoNotMatch() throws IOException {
Path root = temporaryFolder.newFolder("committed-mismatch").toPath();
Path target = root.resolve("objects/temple.iob");
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
byte[] unexpected = "unexpected".getBytes(StandardCharsets.UTF_8);
Files.createDirectories(target.getParent());
Files.write(target, unexpected);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
Files.write(backup, original);
StructureTransactionJournal journal = StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256("committed".getBytes(StandardCharsets.UTF_8))
)
)).committed();
Files.write(transactionRoot.resolve(StructureTransactionJournal.FILE_NAME), journal.toJson());
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertFalse(result.successful());
assertArrayEquals(unexpected, Files.readAllBytes(target));
assertTrue(Files.exists(backup));
assertTrue(Files.exists(transactionRoot));
}
@Test
public void invalidRecoveryJournalBlocksWritesAndRetainsRecoveryData() throws IOException {
Path root = temporaryFolder.newFolder("invalid-recovery").toPath();
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path journalPath = transactionRoot.resolve(StructureTransactionJournal.FILE_NAME);
Files.createDirectories(transactionRoot);
Files.write(journalPath, "not-json".getBytes(StandardCharsets.UTF_8));
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.write(bundle("object-v1", "structure-v1"),
StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.FAILED, result.status());
assertTrue(result.failure().isPresent());
assertTrue(Files.exists(journalPath));
assertFalse(Files.exists(root.resolve("objects/temple.iob")));
}
@Test
public void writeRecoversPreparedTransactionBeforePreflight() throws IOException {
Path root = temporaryFolder.newFolder("write-recovery").toPath();
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
byte[] interruptedReplacement = "interrupted".getBytes(StandardCharsets.UTF_8);
writeJournal(transactionRoot, StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
false,
"",
StructureHash.sha256(interruptedReplacement)
)
)));
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.write(bundle("object-v1", "structure-v1"),
StructureWriteMode.ADD_ONLY);
assertEquals(failureMessage(result), StructureWriteResult.Status.ADDED, result.status());
assertArrayEquals("object-v1".getBytes(StandardCharsets.UTF_8),
Files.readAllBytes(root.resolve("objects/temple.iob")));
assertFalse(Files.exists(transactionRoot));
}
@Test
public void preparedRecoveryRetainsUnexpectedReplacement() throws IOException {
Path root = temporaryFolder.newFolder("replacement-conflict").toPath();
Path target = root.resolve("objects/temple.iob");
Files.createDirectories(target.getParent());
byte[] userEdit = "user-edit".getBytes(StandardCharsets.UTF_8);
Files.write(target, userEdit);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
writeJournal(transactionRoot, StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
false,
"",
StructureHash.sha256("interrupted".getBytes(StandardCharsets.UTF_8))
)
)));
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertFalse(result.successful());
assertEquals(1, result.failures().size());
assertArrayEquals(userEdit, Files.readAllBytes(target));
assertTrue(Files.exists(transactionRoot));
}
@Test
public void concurrentTargetCreationIsNotDeletedByRollback() throws IOException {
Path root = temporaryFolder.newFolder("concurrent-target").toPath();
byte[] userContent = "concurrent-user-file".getBytes(StandardCharsets.UTF_8);
FailOnceMoveOperations operations = new FailOnceMoveOperations(
"staged/objects/temple.iob",
userContent
);
StructureTransactionWriter writer = new StructureTransactionWriter(root, operations);
StructureWriteResult result = writer.write(bundle("object-v1", "structure-v1"),
StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.ROLLED_BACK, result.status());
assertArrayEquals(userContent, Files.readAllBytes(root.resolve("objects/temple.iob")));
}
@Test
public void preparedRecoveryAcceptsAlreadyRestoredOriginalWithRemainingBackup() throws IOException {
Path root = temporaryFolder.newFolder("repeated-recovery").toPath();
byte[] original = "original".getBytes(StandardCharsets.UTF_8);
Path target = root.resolve("objects/temple.iob");
Files.createDirectories(target.getParent());
Files.write(target, original);
UUID transactionId = UUID.randomUUID();
Path transactionRoot = transactionRoot(root, transactionId);
Path backup = transactionRoot.resolve("backup/objects/temple.iob");
Files.createDirectories(backup.getParent());
Files.write(backup, original);
writeJournal(transactionRoot, StructureTransactionJournal.prepared(transactionId, List.of(
new StructureTransactionJournal.Target(
"objects/temple.iob",
true,
StructureHash.sha256(original),
StructureHash.sha256("replacement".getBytes(StandardCharsets.UTF_8))
)
)));
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureRecoveryResult result = writer.recoverIncompleteTransactions();
assertTrue(result.successful());
assertEquals(1, result.restoredPreparedTransactions());
assertArrayEquals(original, Files.readAllBytes(target));
assertFalse(Files.exists(transactionRoot));
}
@Test
public void symbolicLinkAncestorsCannotEscapeThePackRoot() throws IOException {
Path root = temporaryFolder.newFolder("symlink-pack").toPath();
Path outside = temporaryFolder.newFolder("symlink-outside").toPath();
try {
Files.createSymbolicLink(root.resolve("objects"), outside);
} catch (IOException | UnsupportedOperationException | SecurityException e) {
Assume.assumeNoException(e);
}
StructureTransactionWriter writer = new StructureTransactionWriter(root);
StructureWriteResult result = writer.write(bundle("object-v1", "structure-v1"),
StructureWriteMode.ADD_ONLY);
assertEquals(StructureWriteResult.Status.FAILED, result.status());
assertFalse(Files.exists(outside.resolve("temple.iob")));
}
private StructureResourceBundle bundle(String objectContent, String structureContent) {
return StructureResourceBundle.builder(TARGET_KEY)
.source(StructureSource.of(StructureSource.Kind.VANILLA, SOURCE_KEY))
.backend(StructureBackend.IRIS_ASSEMBLY)
.capability(StructureCapability.BLOCKS)
.capability(StructureCapability.CONNECTORS)
.loss(StructureLoss.warning(
StructureCapability.PROCESSORS,
"processors_omitted",
"The source processor list was not represented"
))
.resource("objects/temple.iob", objectContent.getBytes(StandardCharsets.UTF_8))
.textResource("structures/temple.json", structureContent)
.build();
}
private Path transactionRoot(Path root, UUID transactionId) {
return root.resolve(".iris/structure-staging").resolve(transactionId.toString());
}
private String failureMessage(StructureWriteResult result) {
return result.failure().map(Throwable::toString).orElse("");
}
private void writeJournal(
Path transactionRoot,
StructureTransactionJournal journal
) throws IOException {
Files.createDirectories(transactionRoot);
Files.write(transactionRoot.resolve(StructureTransactionJournal.FILE_NAME), journal.toJson());
}
private static final class FailOnceMoveOperations implements StructureFileOperations {
private final NioStructureFileOperations delegate;
private final String failingSuffix;
private final byte[] competingContent;
private boolean failed;
private FailOnceMoveOperations(String failingSuffix) {
this(failingSuffix, null);
}
private FailOnceMoveOperations(String failingSuffix, byte[] competingContent) {
delegate = new NioStructureFileOperations();
this.failingSuffix = failingSuffix;
this.competingContent = competingContent == null ? null : competingContent.clone();
}
@Override
public boolean exists(Path path) {
return delegate.exists(path);
}
@Override
public boolean isRegularFile(Path path) {
return delegate.isRegularFile(path);
}
@Override
public byte[] readAllBytes(Path path) throws IOException {
return delegate.readAllBytes(path);
}
@Override
public String sha256(Path path) throws IOException {
return delegate.sha256(path);
}
@Override
public void createDirectories(Path path) throws IOException {
delegate.createDirectories(path);
}
@Override
public void writeNew(Path path, byte[] content) throws IOException {
delegate.writeNew(path, content);
}
@Override
public void move(Path source, Path target) throws IOException {
String portableSource = source.toString().replace(File.separatorChar, '/');
if (!failed && portableSource.endsWith(failingSuffix)) {
failed = true;
throw new IOException("Injected install failure for " + failingSuffix);
}
delegate.move(source, target);
}
@Override
public void moveNew(Path source, Path target) throws IOException {
String portableSource = source.toString().replace(File.separatorChar, '/');
if (!failed && competingContent != null && portableSource.endsWith(failingSuffix)) {
failed = true;
delegate.createDirectories(target.getParent());
delegate.writeNew(target, competingContent);
delegate.moveNew(source, target);
return;
}
StructureFileOperations.super.moveNew(source, target);
}
@Override
public void deleteIfExists(Path path) throws IOException {
delegate.deleteIfExists(path);
}
@Override
public void deleteTree(Path root) throws IOException {
delegate.deleteTree(root);
}
}
}
@@ -0,0 +1,251 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
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 SimpleStructureStudioCompilerTest {
@Test
public void publishIdentityMustMatchTheRuntimeResourceKey() {
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioPublishConfig(
new StructureKey("iris", "studio/owned"),
"studio/different",
7,
8,
ObjectPlaceMode.STRUCTURE_PIECE
));
}
@Test
public void compilesDeterministicOwnedGraphWithExactConnectorGeometry() throws IOException {
CompilerFixture fixture = fixture();
StructureResourceBundle first = SimpleStructureStudioCompiler.compile(
fixture.draft(),
fixture.config(),
fixture.objects()
);
StructureResourceBundle second = SimpleStructureStudioCompiler.compile(
fixture.draft(),
fixture.config(),
fixture.objects()
);
assertEquals(StructureBackend.IRIS_ASSEMBLY, first.backend());
assertEquals(new StructureKey("iris", "studio/hall"), first.key());
assertTrue(first.capabilities().contains(StructureCapability.CONNECTORS));
assertTrue(first.capabilities().contains(StructureCapability.IRIS_PLACEMENT));
assertEquals(12, first.resources().size());
assertEquals(resourceHashes(first), resourceHashes(second));
StructureOwnershipManifest manifest = StructureOwnershipManifest.from(first);
assertEquals(first.key(), manifest.structure());
assertEquals(resourceHashes(first), manifest.resourceHashes());
JsonObject startPiece = json(first, "jigsaw-pieces/studio/hall/cells/0-0/start.json");
assertFalse(startPiece.get("rotatable").getAsBoolean());
JsonArray startConnectors = startPiece.getAsJsonArray("connectors");
assertEquals(1, startConnectors.size());
assertConnector(startConnectors.get(0).getAsJsonObject(), 3, 2, 0, "NORTH_NEGATIVE_Z");
JsonObject mainPiece = json(first, "jigsaw-pieces/studio/hall/cells/1-0/cross.json");
assertTrue(mainPiece.get("rotatable").getAsBoolean());
JsonArray mainConnectors = mainPiece.getAsJsonArray("connectors");
assertEquals(4, mainConnectors.size());
assertConnector(mainConnectors.get(0).getAsJsonObject(), 3, 2, 0, "NORTH_NEGATIVE_Z");
assertConnector(mainConnectors.get(1).getAsJsonObject(), 5, 2, 4, "EAST_POSITIVE_X");
assertConnector(mainConnectors.get(2).getAsJsonObject(), 3, 2, 7, "SOUTH_POSITIVE_Z");
assertConnector(mainConnectors.get(3).getAsJsonObject(), 0, 2, 4, "WEST_NEGATIVE_X");
JsonObject mainPool = json(first, "jigsaw-pools/studio/hall/main.json");
assertEquals("studio/hall/terminal", mainPool.get("fallback").getAsString());
assertEquals(3, mainPool.getAsJsonArray("pieces").get(0).getAsJsonObject().get("weight").getAsInt());
assertEquals(5, mainPool.getAsJsonArray("pieces").get(1).getAsJsonObject().get("weight").getAsInt());
JsonObject startPool = json(first, "jigsaw-pools/studio/hall/start.json");
assertEquals(1, startPool.getAsJsonArray("pieces").size());
JsonObject terminalPool = json(first, "jigsaw-pools/studio/hall/terminal.json");
assertEquals(1, terminalPool.getAsJsonArray("pieces").size());
JsonObject structure = json(first, "structures/studio/hall.json");
assertEquals("studio/hall/start", structure.get("startPool").getAsString());
assertEquals(1, structure.get("maxDepth").getAsInt());
assertEquals(4, structure.get("maxSizeChunks").getAsInt());
assertEquals("STRUCTURE_PIECE", structure.get("placeMode").getAsString());
}
@Test
public void failsClosedForHalfTurnOnlyPieces() {
CompilerFixture fixture = fixture();
SimpleStructureStudioCell halfTurnMain = fixture.draft().cellOrEmpty(1, 0)
.withRotationPolicy(SimpleStructureStudioRotationPolicy.HALF_TURNS);
SimpleStructureStudioDraft unsupported = fixture.draft().withCell(halfTurnMain);
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> SimpleStructureStudioCompiler.compile(unsupported, fixture.config(), fixture.objects())
);
assertTrue(failure.getMessage().contains("HALF_TURNS"));
}
@Test
public void rejectsMissingUnexpectedAndWrongSizedResolvedObjects() {
CompilerFixture fixture = fixture();
LinkedHashMap<SimpleStructureStudioVariantKey, IrisObject> missing = new LinkedHashMap<>(fixture.objects());
missing.remove(new SimpleStructureStudioVariantKey(1, 0, "cross"));
IllegalStateException missingFailure = assertThrows(
IllegalStateException.class,
() -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), missing)
);
assertTrue(missingFailure.getMessage().contains("missing=[1,0:cross]"));
LinkedHashMap<SimpleStructureStudioVariantKey, IrisObject> unexpected = new LinkedHashMap<>(fixture.objects());
unexpected.put(new SimpleStructureStudioVariantKey(3, 0, "stale"), object());
IllegalStateException unexpectedFailure = assertThrows(
IllegalStateException.class,
() -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), unexpected)
);
assertTrue(unexpectedFailure.getMessage().contains("unexpected=[3,0:stale]"));
LinkedHashMap<SimpleStructureStudioVariantKey, IrisObject> wrongSize = new LinkedHashMap<>(fixture.objects());
wrongSize.put(new SimpleStructureStudioVariantKey(2, 0, "cap"), new IrisObject(5, 4, 8));
IllegalStateException sizeFailure = assertThrows(
IllegalStateException.class,
() -> SimpleStructureStudioCompiler.compile(fixture.draft(), fixture.config(), wrongSize)
);
assertTrue(sizeFailure.getMessage().contains("expected 6x4x8"));
}
@Test
public void rejectsDisconnectedPoolChannelsBeforeWritingResources() {
CompilerFixture fixture = fixture();
SimpleStructureStudioCell wrongChannel = fixture.draft().cellOrEmpty(2, 0)
.withConnector("iris:other", 2);
SimpleStructureStudioDraft disconnected = fixture.draft().withCell(wrongChannel);
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> SimpleStructureStudioCompiler.compile(disconnected, fixture.config(), fixture.objects())
);
assertTrue(failure.getMessage().contains("must cover the same connector channels"));
}
private CompilerFixture fixture() {
SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(3, 1, 6, 8, 4);
SimpleStructureStudioCell start = SimpleStructureStudioCell
.create(0, 0, SimpleStructureStudioTopology.START)
.withRotationPolicy(SimpleStructureStudioRotationPolicy.FIXED)
.withConnector("iris:path", 2)
.addVariant(new SimpleStructureStudioVariant("start", 1));
SimpleStructureStudioCell main = SimpleStructureStudioCell
.create(1, 0, SimpleStructureStudioTopology.CROSS)
.withQuarterTurns(1)
.withConnector("iris:path", 2)
.addVariant(new SimpleStructureStudioVariant("cross", 3))
.addVariant(new SimpleStructureStudioVariant("mossy/cross", 5));
SimpleStructureStudioCell terminal = SimpleStructureStudioCell
.create(2, 0, SimpleStructureStudioTopology.TERMINAL)
.withConnector("iris:path", 2)
.addVariant(new SimpleStructureStudioVariant("cap", 2));
SimpleStructureStudioDraft draft = new SimpleStructureStudioDraft(
layout,
773L,
List.of(terminal, main, start)
);
SimpleStructureStudioPublishConfig config = new SimpleStructureStudioPublishConfig(
new StructureKey("iris", "studio/hall"),
"studio/hall",
1,
4,
ObjectPlaceMode.STRUCTURE_PIECE
);
LinkedHashMap<SimpleStructureStudioVariantKey, IrisObject> objects = new LinkedHashMap<>();
objects.put(SimpleStructureStudioVariantKey.of(start, start.variants().get(0)), object());
objects.put(SimpleStructureStudioVariantKey.of(main, main.variants().get(0)), object());
objects.put(SimpleStructureStudioVariantKey.of(main, main.variants().get(1)), object());
objects.put(SimpleStructureStudioVariantKey.of(terminal, terminal.variants().get(0)), object());
return new CompilerFixture(draft, config, objects);
}
private IrisObject object() {
return new IrisObject(6, 4, 8);
}
private Map<String, String> resourceHashes(StructureResourceBundle bundle) {
TreeMap<String, String> hashes = new TreeMap<>();
for (StructureResourceBundle.Resource resource : bundle.resources().values()) {
hashes.put(resource.relativePath(), resource.contentHash());
}
return hashes;
}
private JsonObject json(StructureResourceBundle bundle, String relativePath) {
StructureResourceBundle.Resource resource = bundle.resources().get(relativePath);
return JsonParser.parseString(new String(resource.content(), StandardCharsets.UTF_8)).getAsJsonObject();
}
private void assertConnector(
JsonObject connector,
int x,
int y,
int z,
String direction
) {
JsonObject position = connector.getAsJsonObject("position");
assertEquals(x, position.get("x").getAsInt());
assertEquals(y, position.get("y").getAsInt());
assertEquals(z, position.get("z").getAsInt());
assertEquals(direction, connector.get("direction").getAsString());
assertEquals("studio/hall/main", connector.get("pool").getAsString());
assertEquals("iris:path", connector.get("name").getAsString());
assertEquals("iris:path", connector.get("targetName").getAsString());
assertEquals("ALIGNED", connector.get("joint").getAsString());
}
private record CompilerFixture(
SimpleStructureStudioDraft draft,
SimpleStructureStudioPublishConfig config,
Map<SimpleStructureStudioVariantKey, IrisObject> objects
) {
}
}
@@ -0,0 +1,132 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.studio;
import com.google.gson.Gson;
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.assertThrows;
import static org.junit.Assert.assertTrue;
public class SimpleStructureStudioModelTest {
@Test
public void topologyMasksRotateAcrossCardinalDirections() {
assertEquals(0, SimpleStructureStudioTopology.EMPTY.baseConnectorMask());
assertEquals(1, SimpleStructureStudioTopology.END.baseConnectorMask());
assertEquals(5, SimpleStructureStudioTopology.STRAIGHT.baseConnectorMask());
assertEquals(3, SimpleStructureStudioTopology.CORNER.baseConnectorMask());
assertEquals(11, SimpleStructureStudioTopology.T.baseConnectorMask());
assertEquals(15, SimpleStructureStudioTopology.CROSS.baseConnectorMask());
assertEquals(6, SimpleStructureStudioTopology.CORNER.connectorMask(1));
assertEquals(12, SimpleStructureStudioTopology.CORNER.connectorMask(2));
assertEquals(9, SimpleStructureStudioTopology.CORNER.connectorMask(3));
assertTrue(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.EAST, 1));
assertTrue(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.SOUTH, 1));
assertFalse(SimpleStructureStudioTopology.CORNER.connects(SimpleStructureStudioDirection.NORTH, 1));
}
@Test
public void cellVariantsAreWeightedSelectableAndImmutable() {
SimpleStructureStudioCell cell = SimpleStructureStudioCell
.create(1, 2, SimpleStructureStudioTopology.CORNER)
.addVariant(new SimpleStructureStudioVariant("stone", 2))
.addVariant(new SimpleStructureStudioVariant("mossy", 5));
assertEquals("stone", cell.activeVariant().orElseThrow().id());
SimpleStructureStudioCell selected = cell.selectVariant("mossy");
assertEquals("mossy", selected.activeVariant().orElseThrow().id());
assertEquals("stone", selected.cycleVariant(1).activeVariant().orElseThrow().id());
assertEquals(7, selected.setVariantWeight("mossy", 7).variants().get(1).weight());
assertThrows(UnsupportedOperationException.class, () -> selected.variants().add(
new SimpleStructureStudioVariant("extra", 1)
));
assertThrows(IllegalArgumentException.class, () -> cell.addVariant(
new SimpleStructureStudioVariant("stone", 1)
));
}
@Test
public void draftIsCanonicalAndJsonRoundTrips() {
SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(4, 3, 9, 11, 24);
SimpleStructureStudioCell later = SimpleStructureStudioCell.create(
3,
2,
SimpleStructureStudioTopology.TERMINAL
);
SimpleStructureStudioCell earlier = SimpleStructureStudioCell.create(
0,
0,
SimpleStructureStudioTopology.START
);
SimpleStructureStudioDraft draft = new SimpleStructureStudioDraft(
layout,
9921L,
List.of(later, earlier)
);
assertEquals(earlier, draft.cells().get(0));
assertEquals(later, draft.cells().get(1));
assertEquals(36, layout.studioWidth());
assertEquals(33, layout.studioDepth());
assertThrows(
IllegalStateException.class,
() -> draft.withLayout(new SimpleStructureStudioLayout(5, 3, 9, 11, 24))
);
Gson gson = new Gson();
String json = gson.toJson(draft);
SimpleStructureStudioDraft restored = gson.fromJson(json, SimpleStructureStudioDraft.class);
assertEquals(draft, restored);
}
@Test
public void draftRejectsInvalidGeometryAndConnectorHeights() {
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioLayout(0, 1, 1, 1, 1));
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("../piece", 1));
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("piece.", 1));
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("con", 1));
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("nul", 1));
assertThrows(IllegalArgumentException.class, () -> new SimpleStructureStudioVariant("com1", 1));
SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(2, 2, 8, 8, 4);
SimpleStructureStudioCell tooTall = SimpleStructureStudioCell
.create(0, 0, SimpleStructureStudioTopology.END)
.withConnector("iris:path", 4);
assertThrows(
IllegalArgumentException.class,
() -> new SimpleStructureStudioDraft(layout, 0L, List.of(tooTall))
);
assertThrows(
IllegalArgumentException.class,
() -> new SimpleStructureStudioDraft(
layout,
0L,
List.of(
SimpleStructureStudioCell.create(0, 0, SimpleStructureStudioTopology.END),
SimpleStructureStudioCell.create(0, 0, SimpleStructureStudioTopology.CORNER)
)
)
);
}
}
@@ -0,0 +1,75 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.studio;
import art.arcane.iris.core.structure.authoring.StructureKey;
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 java.util.Optional;
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 class SimpleStructureStudioRepositoryTest {
private static final StructureKey KEY = StructureKey.parse("iris:castle/main");
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void savesLoadsAndAtomicallyReplacesDrafts() throws Exception {
Path root = temporaryFolder.newFolder("pack").toPath();
SimpleStructureStudioRepository repository = new SimpleStructureStudioRepository(root);
SimpleStructureStudioLayout layout = new SimpleStructureStudioLayout(3, 2, 16, 16, 12);
SimpleStructureStudioDraft first = SimpleStructureStudioDraft.empty(layout, 10L);
SimpleStructureStudioDraft second = first.withPreviewSeed(20L)
.withCell(SimpleStructureStudioCell.create(1, 1, SimpleStructureStudioTopology.CORNER));
assertTrue(repository.load(KEY).isEmpty());
repository.save(KEY, first);
assertEquals(Optional.of(first), repository.load(KEY));
repository.save(KEY, second);
assertEquals(Optional.of(second), repository.load(KEY));
try (Stream<Path> paths = Files.list(repository.draftPath(KEY).getParent())) {
assertFalse(paths.anyMatch(path -> path.getFileName().toString().endsWith(".tmp")));
}
}
@Test
public void malformedDraftFailsWithItsPathAndLeavesFileUntouched() throws Exception {
Path root = temporaryFolder.newFolder("pack").toPath();
SimpleStructureStudioRepository repository = new SimpleStructureStudioRepository(root);
Path draft = repository.draftPath(KEY);
Files.createDirectories(draft.getParent());
Files.writeString(draft, "{", StandardCharsets.UTF_8);
Exception failure = assertThrows(Exception.class, () -> repository.load(KEY));
assertTrue(failure.getMessage().contains(draft.toString()));
assertEquals("{", Files.readString(draft, StandardCharsets.UTF_8));
}
}
@@ -0,0 +1,140 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.structure.studio;
import org.junit.Test;
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 SimpleStructureStudioSessionTest {
@Test
public void sessionTracksDirtySavedUndoAndRedoStates() {
SimpleStructureStudioDraft initial = emptyDraft();
SimpleStructureStudioSession session = SimpleStructureStudioSession.open(initial, 8);
assertFalse(session.isDirty());
assertTrue(session.setTopology(1, 1, SimpleStructureStudioTopology.CORNER));
assertTrue(session.isDirty());
assertEquals(3, session.draft().cellOrEmpty(1, 1).connectorMask());
session.markSaved();
assertFalse(session.isDirty());
assertTrue(session.rotateClockwise(1, 1));
assertEquals(6, session.draft().cellOrEmpty(1, 1).connectorMask());
assertTrue(session.isDirty());
assertTrue(session.undo());
assertFalse(session.isDirty());
assertEquals(3, session.draft().cellOrEmpty(1, 1).connectorMask());
assertTrue(session.redo());
assertTrue(session.isDirty());
assertEquals(6, session.draft().cellOrEmpty(1, 1).connectorMask());
}
@Test
public void resizeIsAllowedOnlyBeforeContentExists() {
SimpleStructureStudioSession session = SimpleStructureStudioSession.open(emptyDraft(), 8);
SimpleStructureStudioLayout resized = new SimpleStructureStudioLayout(6, 5, 12, 14, 28);
assertTrue(session.resize(resized));
assertEquals(resized, session.draft().layout());
assertTrue(session.setTopology(0, 0, SimpleStructureStudioTopology.START));
assertThrows(
IllegalStateException.class,
() -> session.resize(new SimpleStructureStudioLayout(7, 5, 12, 14, 28))
);
assertTrue(session.clearCell(0, 0));
assertTrue(session.resize(new SimpleStructureStudioLayout(7, 5, 12, 14, 28)));
}
@Test
public void variantsConnectorsAndRotationPoliciesUseSharedActions() {
SimpleStructureStudioSession session = SimpleStructureStudioSession.open(emptyDraft(), 16);
session.setTopology(2, 1, SimpleStructureStudioTopology.END);
session.setConnector(2, 1, "iris:hall", 6);
session.addVariant(2, 1, new SimpleStructureStudioVariant("plain", 1));
session.addVariant(2, 1, new SimpleStructureStudioVariant("ruined", 3));
session.selectVariant(2, 1, "ruined");
session.setVariantWeight(2, 1, "ruined", 8);
SimpleStructureStudioCell configured = session.draft().cellOrEmpty(2, 1);
assertEquals("iris:hall", configured.connectorChannel());
assertEquals(6, configured.connectorHeight());
assertEquals("ruined", configured.activeVariant().orElseThrow().id());
assertEquals(8, configured.activeVariant().orElseThrow().weight());
assertTrue(session.setRotationPolicy(2, 1, SimpleStructureStudioRotationPolicy.HALF_TURNS));
assertTrue(session.rotateClockwise(2, 1));
assertEquals(2, session.draft().cellOrEmpty(2, 1).quarterTurns());
assertTrue(session.setRotationPolicy(2, 1, SimpleStructureStudioRotationPolicy.FIXED));
assertEquals(0, session.draft().cellOrEmpty(2, 1).quarterTurns());
assertFalse(session.rotateClockwise(2, 1));
}
@Test
public void previewSeedSequenceAndHistoryBoundAreDeterministic() {
SimpleStructureStudioSession first = SimpleStructureStudioSession.open(emptyDraft(), 3);
SimpleStructureStudioSession second = SimpleStructureStudioSession.open(emptyDraft(), 3);
assertEquals(first.advancePreviewSeed(), second.advancePreviewSeed());
assertEquals(first.advancePreviewSeed(), second.advancePreviewSeed());
first.setPreviewSeed(1L);
first.setPreviewSeed(2L);
first.setPreviewSeed(3L);
first.setPreviewSeed(4L);
assertEquals(3, first.undoDepth());
assertTrue(first.undo());
assertEquals(3L, first.draft().previewSeed());
assertTrue(first.undo());
assertEquals(2L, first.draft().previewSeed());
assertTrue(first.undo());
assertEquals(1L, first.draft().previewSeed());
assertFalse(first.undo());
assertTrue(first.redo());
assertTrue(first.setPreviewSeed(99L));
assertFalse(first.canRedo());
}
@Test
public void newDraftRemainsDirtyUntilSaved() {
SimpleStructureStudioSession session = SimpleStructureStudioSession.createNew(emptyDraft(), 8);
assertTrue(session.isDirty());
session.markSaved();
assertFalse(session.isDirty());
assertThrows(IllegalStateException.class, () -> session.addVariant(
0,
0,
new SimpleStructureStudioVariant("missing", 1)
));
}
private SimpleStructureStudioDraft emptyDraft() {
return SimpleStructureStudioDraft.empty(
new SimpleStructureStudioLayout(4, 4, 10, 10, 20),
12345L
);
}
}
@@ -0,0 +1,594 @@
package art.arcane.iris.core.tools;
import art.arcane.iris.engine.object.IrisObject;
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.math.IrisBlockVector;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TreePlausibilizerTest {
private static final String OAK_LOG = "minecraft:oak_log[axis=y]";
private static final String BIRCH_LOG = "minecraft:birch_log[axis=y]";
private static final String OAK_LEAVES = "minecraft:oak_leaves[distance=7,persistent=true,waterlogged=false]";
private static final String DIRT = "minecraft:dirt";
private static final String MUSHROOM_STEM = "minecraft:mushroom_stem";
private static final String MUSHROOM_BLOCK = "minecraft:red_mushroom_block";
private static final int[][] NEIGHBORS = {
{1, 0, 0}, {-1, 0, 0},
{0, 1, 0}, {0, -1, 0},
{0, 0, 1}, {0, 0, -1}
};
private record Pos(int x, int y, int z) {
}
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
PlatformBlockState defaultBlock = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
IrisPlatform platform = mock(IrisPlatform.class);
when(registries.block(anyString())).thenReturn(defaultBlock);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
private static final class TestState implements PlatformBlockState {
private final String key;
private TestState(String raw) {
this.key = canonical(raw);
}
static TestState of(String raw) {
return new TestState(raw);
}
private static String canonical(String raw) {
int bracket = raw.indexOf('[');
if (bracket < 0) {
return raw;
}
String material = raw.substring(0, bracket);
String body = raw.substring(bracket + 1, raw.length() - 1);
TreeMap<String, String> props = new TreeMap<>();
for (String pair : body.split(",")) {
if (pair.isEmpty()) {
continue;
}
int eq = pair.indexOf('=');
props.put(pair.substring(0, eq), pair.substring(eq + 1));
}
return render(material, props);
}
private static String render(String material, TreeMap<String, String> props) {
if (props.isEmpty()) {
return material;
}
StringBuilder sb = new StringBuilder(material).append('[');
boolean first = true;
for (Map.Entry<String, String> e : props.entrySet()) {
if (!first) {
sb.append(',');
}
first = false;
sb.append(e.getKey()).append('=').append(e.getValue());
}
return sb.append(']').toString();
}
@Override
public String key() {
return key;
}
@Override
public String namespace() {
return "minecraft";
}
@Override
public PlatformBlockState withProperty(String name, String value) {
int bracket = key.indexOf('[');
String material = bracket < 0 ? key : key.substring(0, bracket);
TreeMap<String, String> props = new TreeMap<>();
if (bracket >= 0) {
String body = key.substring(bracket + 1, key.length() - 1);
for (String pair : body.split(",")) {
if (pair.isEmpty()) {
continue;
}
int eq = pair.indexOf('=');
props.put(pair.substring(0, eq), pair.substring(eq + 1));
}
}
props.put(name, value);
return new TestState(render(material, props));
}
@Override
public Object nativeHandle() {
return null;
}
@Override
public boolean matches(PlatformBlockState state) {
return state != null && key.equals(state.key());
}
@Override
public boolean canPlaceOnto(PlatformBlockState onto) {
return true;
}
@Override
public boolean isAir() {
return false;
}
@Override
public boolean isSolid() {
return true;
}
@Override
public boolean isOccluding() {
return false;
}
@Override
public boolean isCustom() {
return false;
}
@Override
public boolean isFluid() {
return false;
}
@Override
public boolean isWater() {
return false;
}
@Override
public boolean isWaterLogged() {
return false;
}
@Override
public boolean isLit() {
return false;
}
@Override
public boolean isUpdatable() {
return false;
}
@Override
public boolean isFoliage() {
return false;
}
@Override
public boolean isTreeBlock() {
return false;
}
@Override
public boolean isFoliagePlantable() {
return false;
}
@Override
public boolean isDecorant() {
return false;
}
@Override
public boolean isStorage() {
return false;
}
@Override
public boolean isStorageChest() {
return false;
}
@Override
public boolean isOre() {
return false;
}
@Override
public boolean isDeepSlate() {
return false;
}
@Override
public boolean isVineBlock() {
return false;
}
@Override
public boolean hasTileEntity() {
return false;
}
@Override
public boolean equals(Object o) {
return o instanceof TestState other && key.equals(other.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
private static IrisObject emptyObject() {
return new IrisObject(32, 32, 32);
}
private static void put(IrisObject obj, int x, int y, int z, String state) {
obj.getBlocks().put(new IrisBlockVector(x, y, z), TestState.of(state));
}
private static void trunk(IrisObject obj, int height) {
for (int y = 0; y < height; y++) {
put(obj, 0, y, 0, OAK_LOG);
}
}
private static void blob(IrisObject obj, int cx, int cy, int cz, int r, String state) {
for (int x = cx - r; x <= cx + r; x++) {
for (int y = cy - r; y <= cy + r; y++) {
for (int z = cz - r; z <= cz + r; z++) {
put(obj, x, y, z, state);
}
}
}
}
private static Map<Pos, String> snapshot(IrisObject obj) {
Map<Pos, String> out = new HashMap<>();
for (Map.Entry<IrisBlockVector, PlatformBlockState> e : obj.getBlocks()) {
IrisBlockVector v = e.getKey();
out.put(new Pos(v.getBlockX(), v.getBlockY(), v.getBlockZ()), e.getValue().key());
}
return out;
}
private static String materialOf(String key) {
int bracket = key.indexOf('[');
return bracket < 0 ? key : key.substring(0, bracket);
}
private static String propertyOf(String key, String name) {
int bracket = key.indexOf('[');
if (bracket < 0) {
return null;
}
String body = key.substring(bracket + 1, key.length() - 1);
for (String pair : body.split(",")) {
int eq = pair.indexOf('=');
if (pair.substring(0, eq).equals(name)) {
return pair.substring(eq + 1);
}
}
return null;
}
private static boolean isWoodKey(String key) {
String name = materialOf(key);
return name.endsWith("_log") || name.endsWith("_wood") || name.endsWith("_hyphae");
}
private static boolean isLeafKey(String key) {
return propertyOf(key, "distance") != null && propertyOf(key, "persistent") != null;
}
private static Set<Pos> woodPositions(Map<Pos, String> snap) {
Set<Pos> out = new HashSet<>();
for (Map.Entry<Pos, String> e : snap.entrySet()) {
if (isWoodKey(e.getValue())) {
out.add(e.getKey());
}
}
return out;
}
private static Set<Pos> leafPositions(Map<Pos, String> snap) {
Set<Pos> out = new HashSet<>();
for (Map.Entry<Pos, String> e : snap.entrySet()) {
if (isLeafKey(e.getValue())) {
out.add(e.getKey());
}
}
return out;
}
private static Map<Pos, Integer> mirrorDistances(Map<Pos, String> snap) {
Set<Pos> wood = woodPositions(snap);
Set<Pos> leaves = leafPositions(snap);
Map<Pos, Integer> dist = new HashMap<>();
Deque<Pos> queue = new ArrayDeque<>();
for (Pos leaf : leaves) {
for (int[] n : NEIGHBORS) {
if (wood.contains(new Pos(leaf.x() + n[0], leaf.y() + n[1], leaf.z() + n[2]))) {
dist.put(leaf, 1);
queue.add(leaf);
break;
}
}
}
while (!queue.isEmpty()) {
Pos pos = queue.poll();
int d = dist.get(pos);
if (d >= 6) {
continue;
}
for (int[] n : NEIGHBORS) {
Pos nk = new Pos(pos.x() + n[0], pos.y() + n[1], pos.z() + n[2]);
if (leaves.contains(nk) && !dist.containsKey(nk)) {
dist.put(nk, d + 1);
queue.add(nk);
}
}
}
return dist;
}
@Test
public void everyLeafLegalAfterApply() {
IrisObject obj = emptyObject();
trunk(obj, 8);
blob(obj, 10, 8, 0, 2, OAK_LEAVES);
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(0, r.unreachableAfter());
Map<Pos, String> snap = snapshot(obj);
Map<Pos, Integer> dist = mirrorDistances(snap);
Set<Pos> leaves = leafPositions(snap);
assertFalse(leaves.isEmpty());
for (Pos leaf : leaves) {
String key = snap.get(leaf);
assertEquals("persistent at " + leaf, "false", propertyOf(key, "persistent"));
Integer d = dist.get(leaf);
assertNotNull("mirror distance at " + leaf, d);
assertTrue("distance in range at " + leaf, d >= 1 && d <= 6);
assertEquals("stored distance at " + leaf, String.valueOf(d), propertyOf(key, "distance"));
}
}
@Test
public void branchWoodIsDominantTrunkMaterial() {
IrisObject obj = emptyObject();
trunk(obj, 8);
put(obj, 3, 0, 0, BIRCH_LOG);
put(obj, 3, 1, 0, BIRCH_LOG);
blob(obj, 11, 7, 0, 1, OAK_LEAVES);
Set<Pos> originalWood = woodPositions(snapshot(obj));
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertTrue(r.woodPlaced() + r.leavesConvertedToWood() > 0);
Map<Pos, String> snap = snapshot(obj);
for (Pos pos : woodPositions(snap)) {
if (originalWood.contains(pos)) {
continue;
}
assertEquals("new wood material at " + pos, "minecraft:oak_log", materialOf(snap.get(pos)));
}
}
@Test
public void clusterBeyondReachIsPinned() {
IrisObject obj = emptyObject();
trunk(obj, 4);
blob(obj, 20, 2, 0, 1, OAK_LEAVES);
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, 5);
assertEquals(0, r.woodPlaced());
assertEquals(0, r.leavesConvertedToWood());
assertEquals(27, r.leavesPinnedPersistent());
Map<Pos, String> snap = snapshot(obj);
for (Pos leaf : leafPositions(snap)) {
String key = snap.get(leaf);
assertEquals("persistent at " + leaf, "true", propertyOf(key, "persistent"));
assertEquals("distance at " + leaf, "7", propertyOf(key, "distance"));
}
}
@Test
public void reachZeroIsUnlimited() {
IrisObject obj = emptyObject();
trunk(obj, 4);
blob(obj, 20, 2, 0, 1, OAK_LEAVES);
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, 0);
assertEquals(0, r.unreachableAfter());
assertEquals(0, r.leavesPinnedPersistent());
assertTrue(r.woodPlaced() > 0);
Map<Pos, String> snap = snapshot(obj);
for (Pos leaf : leafPositions(snap)) {
assertEquals("persistent at " + leaf, "false", propertyOf(snap.get(leaf), "persistent"));
}
}
@Test
public void deterministicOutput() {
IrisObject a = emptyObject();
trunk(a, 8);
blob(a, 10, 8, 0, 2, OAK_LEAVES);
IrisObject b = emptyObject();
trunk(b, 8);
blob(b, 10, 8, 0, 2, OAK_LEAVES);
TreePlausibilizer.apply(a, 42L, TreePlausibilizer.DEFAULT_REACH);
TreePlausibilizer.apply(b, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(snapshot(a), snapshot(b));
}
@Test
public void noLeavesDeletedOrAdded() {
IrisObject obj = emptyObject();
trunk(obj, 8);
blob(obj, 10, 8, 0, 2, OAK_LEAVES);
Map<Pos, String> before = snapshot(obj);
int initialLeaves = leafPositions(before).size();
int initialSize = before.size();
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
Map<Pos, String> after = snapshot(obj);
assertEquals(initialLeaves - r.leavesConvertedToWood(), leafPositions(after).size());
assertEquals(initialSize + r.woodPlaced(), after.size());
assertTrue(after.keySet().containsAll(before.keySet()));
}
@Test
public void analyzeDoesNotMutate() {
IrisObject a = emptyObject();
trunk(a, 8);
blob(a, 10, 8, 0, 2, OAK_LEAVES);
IrisObject b = emptyObject();
trunk(b, 8);
blob(b, 10, 8, 0, 2, OAK_LEAVES);
Map<Pos, String> before = snapshot(a);
TreePlausibilizer.Result analyzed = TreePlausibilizer.analyze(a, 42L, TreePlausibilizer.DEFAULT_REACH);
TreePlausibilizer.Result applied = TreePlausibilizer.apply(b, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(before, snapshot(a));
assertEquals(applied, analyzed);
assertTrue(analyzed.mutated());
}
@Test
public void loglessHedgePinsEverything() {
IrisObject obj = emptyObject();
blob(obj, 0, 0, 0, 1, OAK_LEAVES);
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(0, r.woodPlaced());
assertEquals(27, r.leavesPinnedPersistent());
Map<Pos, String> snap = snapshot(obj);
for (Pos leaf : leafPositions(snap)) {
String key = snap.get(leaf);
assertEquals("persistent at " + leaf, "true", propertyOf(key, "persistent"));
assertEquals("distance at " + leaf, "7", propertyOf(key, "distance"));
}
}
@Test
public void mushroomAndNetherCanopyUntouched() {
IrisObject obj = emptyObject();
for (int y = 0; y < 5; y++) {
put(obj, 0, y, 0, MUSHROOM_STEM);
}
blob(obj, 0, 6, 0, 2, MUSHROOM_BLOCK);
Map<Pos, String> before = snapshot(obj);
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(0, r.totalLeaves());
assertFalse(r.mutated());
assertEquals(before, snapshot(obj));
}
@Test
public void branchNeverOverwritesForeignBlocks() {
IrisObject obj = emptyObject();
trunk(obj, 9);
for (int y = 0; y <= 8; y++) {
for (int z = -2; z <= 2; z++) {
put(obj, 5, y, z, DIRT);
}
}
blob(obj, 10, 4, 0, 1, OAK_LEAVES);
TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
Map<Pos, String> snap = snapshot(obj);
for (int y = 0; y <= 8; y++) {
for (int z = -2; z <= 2; z++) {
assertEquals("dirt intact at 5," + y + "," + z, DIRT, snap.get(new Pos(5, y, z)));
}
}
}
@Test
public void exactDistancesWritten() {
IrisObject obj = emptyObject();
trunk(obj, 4);
for (int x = 1; x <= 6; x++) {
put(obj, x, 2, 0, OAK_LEAVES);
}
TreePlausibilizer.Result r = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertEquals(0, r.woodPlaced());
assertEquals(0, r.leavesConvertedToWood());
assertEquals(6, r.distancesRewritten());
Map<Pos, String> snap = snapshot(obj);
for (int x = 1; x <= 6; x++) {
String key = snap.get(new Pos(x, 2, 0));
assertEquals("distance at x=" + x, String.valueOf(x), propertyOf(key, "distance"));
assertEquals("persistent at x=" + x, "false", propertyOf(key, "persistent"));
}
}
@Test
public void applyIsIdempotent() {
IrisObject obj = emptyObject();
trunk(obj, 8);
blob(obj, 10, 8, 0, 2, OAK_LEAVES);
TreePlausibilizer.Result first = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
Map<Pos, String> afterFirst = snapshot(obj);
TreePlausibilizer.Result second = TreePlausibilizer.apply(obj, 42L, TreePlausibilizer.DEFAULT_REACH);
assertTrue(first.mutated());
assertFalse(second.mutated());
assertEquals(afterFirst, snapshot(obj));
}
@Test
public void seedOfIsStable() {
assertEquals(TreePlausibilizer.seedOf("trees/oak/oakFancy6"), TreePlausibilizer.seedOf("trees/oak/oakFancy6"));
assertTrue(TreePlausibilizer.seedOf("trees/oak/oakFancy6") != TreePlausibilizer.seedOf("trees/oak/oakFancy5"));
}
}
@@ -11,4 +11,11 @@ public class IrisWorldManagerMarkerTest {
assertEquals(319, IrisWorldManager.toMantleY(255, -64));
assertEquals(42, IrisWorldManager.toMantleY(42, 0));
}
@Test
public void mantleHeightIsTranslatedToWorldBlockHeight() {
assertEquals(0, IrisWorldManager.toWorldY(64, -64));
assertEquals(255, IrisWorldManager.toWorldY(319, -64));
assertEquals(42, IrisWorldManager.toWorldY(42, 0));
}
}
@@ -4,7 +4,11 @@ import art.arcane.iris.core.loader.IrisData;
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.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -16,6 +20,7 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -134,4 +139,268 @@ public class DecoratorCoreTest {
+ " (deviation " + String.format("%.0f%%", deviation * 100) + ")", deviation < 0.20);
}
}
@Test
public void singleStackTargetsBlockAboveSupport() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState air = mock(PlatformBlockState.class);
PlatformBlockState decorant = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true);
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(1);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 1, 0, support);
output.set(0, 2, 0, air);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 1, 3, output, new RNG(1L), data, opts);
assertSame(support, output.get(0, 1, 0));
assertSame(decorant, output.get(0, 2, 0));
}
@Test
public void singleStackDoesNotOverwriteOccupiedTarget() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState occupied = mock(PlatformBlockState.class);
PlatformBlockState decorant = mock(PlatformBlockState.class);
when(occupied.isAir()).thenReturn(false);
when(occupied.isFluid()).thenReturn(false);
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(1);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 1, 0, support);
output.set(0, 2, 0, occupied);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 1, 3, output, new RNG(1L), data, opts);
assertSame(support, output.get(0, 1, 0));
assertSame(occupied, output.get(0, 2, 0));
}
@Test
public void multiStackStopsBeforeOccupiedTarget() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState air = mock(PlatformBlockState.class);
PlatformBlockState occupied = mock(PlatformBlockState.class);
PlatformBlockState decorant = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true);
when(occupied.isAir()).thenReturn(false);
when(occupied.isFluid()).thenReturn(false);
when(decorant.key()).thenReturn("minecraft:tall_grass");
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3);
when(decorator.getTopThreshold()).thenReturn(0.75);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 5, 1);
output.set(0, 1, 0, support);
output.set(0, 2, 0, air);
output.set(0, 3, 0, occupied);
output.set(0, 4, 0, air);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackUp(decorator, 0, 0, 0, 0, 1, 4, output, new RNG(1L), data, opts);
assertSame(decorant, output.get(0, 2, 0));
assertSame(occupied, output.get(0, 3, 0));
assertSame(air, output.get(0, 4, 0));
}
@Test
public void descendingStackStopsAtLocalWorldFloor() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState decorant = mock(PlatformBlockState.class);
when(decorant.key()).thenReturn("minecraft:stone");
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(2);
when(decorator.getTopThreshold()).thenReturn(1.0);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackDown(
decorator, 0, 0, 0, 0, 0, -64, output, new RNG(1L), data, 2, opts, null);
assertSame(decorant, output.get(0, 0, 0));
}
@Test
public void singleDescendingStackPreservesTargetWhenPickIsNull() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState existing = mock(PlatformBlockState.class);
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(1);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
output.set(0, 0, 0, existing);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackDown(
decorator, 0, 0, 0, 0, 0, 0, output, new RNG(1L), data, 1, opts, null);
assertSame(existing, output.get(0, 0, 0));
}
@Test
public void multiDescendingStackStopsWhenPickIsNull() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState lowerExisting = mock(PlatformBlockState.class);
PlatformBlockState upperExisting = mock(PlatformBlockState.class);
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(2);
when(decorator.getTopThreshold()).thenReturn(1.0);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
output.set(0, 0, 0, lowerExisting);
output.set(0, 1, 0, upperExisting);
DecoratorCore.PlaceOpts opts = new DecoratorCore.PlaceOpts();
DecoratorCore.placeStackDown(
decorator, 0, 0, 0, 0, 1, 0, output, new RNG(1L), data, 2, opts, null);
assertSame(lowerExisting, output.get(0, 0, 0));
assertSame(upperExisting, output.get(0, 1, 0));
}
@Test
public void floatingStackStopsAtHunkCeiling() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState decorant = mock(PlatformBlockState.class);
when(decorator.getHeight(any(RNG.class), anyDouble(), anyDouble(), eq(data))).thenReturn(3);
when(decorator.getTopThreshold()).thenReturn(1.0);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
when(decorator.pickBlockDataTop(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(decorant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
int placed = DecoratorCore.placeFloatingStacked(
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
assertEquals(1, placed);
assertSame(decorant, output.get(0, 1, 0));
}
@Test
public void tallSurfacePlantDoesNotPlaceWhenUpperTargetIsOccupied() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState air = airState();
PlatformBlockState occupied = mock(PlatformBlockState.class);
PlatformBlockState plant = tallPlantState();
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 0, 0, support);
output.set(0, 1, 0, air);
output.set(0, 2, 0, occupied);
DecoratorCore.placeSurfaceSingle(
decorator, 0, 0, 0, 0, 0, output, new RNG(1L), data, false, false, null);
assertSame(air, output.get(0, 1, 0));
assertSame(occupied, output.get(0, 2, 0));
}
@Test
public void tallSinglePlantDoesNotPlaceWhenLowerTargetIsOccupied() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState occupied = mock(PlatformBlockState.class);
PlatformBlockState air = airState();
PlatformBlockState plant = tallPlantState();
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 1, 0, occupied);
output.set(0, 2, 0, air);
DecoratorCore.placeSingleUp(
decorator, 0, 0, 0, 0, 0, output, new RNG(1L), data, false, null);
assertSame(occupied, output.get(0, 1, 0));
assertSame(air, output.get(0, 2, 0));
}
@Test
public void tallFloatingPlantDoesNotPlaceWhenUpperTargetIsOccupied() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState air = airState();
PlatformBlockState occupied = mock(PlatformBlockState.class);
PlatformBlockState plant = tallPlantState();
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 1, 0, air);
output.set(0, 2, 0, occupied);
DecoratorCore.placeFloatingSimple(
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
assertSame(air, output.get(0, 1, 0));
assertSame(occupied, output.get(0, 2, 0));
}
@Test
public void tallSurfacePlantPlacesBothHalvesTogether() {
IrisDecorator decorator = mock(IrisDecorator.class);
IrisData data = mock(IrisData.class);
PlatformBlockState support = sturdyState();
PlatformBlockState lowerAir = airState();
PlatformBlockState upperAir = airState();
PlatformBlockState lower = mock(PlatformBlockState.class);
PlatformBlockState upper = mock(PlatformBlockState.class);
PlatformBlockState plant = tallPlantState(lower, upper);
when(decorator.pickBlockData(any(RNG.class), eq(data), anyDouble(), anyDouble())).thenReturn(plant);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 4, 1);
output.set(0, 0, 0, support);
output.set(0, 1, 0, lowerAir);
output.set(0, 2, 0, upperAir);
DecoratorCore.placeSurfaceSingle(
decorator, 0, 0, 0, 0, 0, output, new RNG(1L), data, false, false, null);
assertSame(lower, output.get(0, 1, 0));
assertSame(upper, output.get(0, 2, 0));
}
private PlatformBlockState airState() {
PlatformBlockState air = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true);
return air;
}
private PlatformBlockState tallPlantState() {
return tallPlantState(mock(PlatformBlockState.class), mock(PlatformBlockState.class));
}
private PlatformBlockState tallPlantState(PlatformBlockState lower, PlatformBlockState upper) {
PlatformBlockState plant = mock(PlatformBlockState.class);
when(plant.key()).thenReturn("minecraft:tall_grass[half=lower]");
when(plant.withProperty("half", "lower")).thenReturn(lower);
when(plant.withProperty("half", "upper")).thenReturn(upper);
return plant;
}
private PlatformBlockState sturdyState() {
PlatformBlockState support = mock(PlatformBlockState.class);
BlockData blockData = mock(BlockData.class);
when(support.nativeHandle()).thenReturn(blockData);
when(blockData.isFaceSturdy(any(), eq(BlockSupport.FULL))).thenReturn(true);
return support;
}
}
@@ -0,0 +1,407 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.IrisComplex;
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.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Position2;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
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.mock;
import static org.mockito.Mockito.when;
public class HintedLocatorTest {
private static boolean boundPlatform;
@BeforeClass
public static void bindPlatform() throws Exception {
if (IrisPlatforms.isBound()) {
return;
}
File dataDirectory = Files.createTempDirectory("iris-hinted-locator-test").toFile();
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
Object[] arguments = invocation.getArguments();
File file = dataDirectory;
for (Object argument : arguments) {
file = new File(file, String.valueOf(argument));
}
return file;
});
IrisPlatforms.bind(platform);
boundPlatform = true;
}
@AfterClass
public static void unbindPlatform() {
if (boundPlatform) {
IrisPlatforms.unbind();
boundPlatform = false;
}
}
private Engine engine;
private IrisDimension dimension;
private IrisComplex complex;
private IrisData data;
private ResourceLoader<IrisBiome> biomeLoader;
@Before
@SuppressWarnings("unchecked")
public void setup() {
engine = mock(Engine.class);
dimension = mock(IrisDimension.class);
complex = mock(IrisComplex.class);
data = mock(IrisData.class);
biomeLoader = (ResourceLoader<IrisBiome>) mock(ResourceLoader.class);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getData()).thenReturn(data);
when(engine.getFocus()).thenReturn(null);
when(engine.getFocusRegion()).thenReturn(null);
when(engine.isClosed()).thenReturn(false);
when(data.getBiomeLoader()).thenReturn(biomeLoader);
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static IrisRegion region(String key) {
IrisRegion region = new IrisRegion();
region.setLoadKey(key);
return region;
}
private static KList<String> keys(String... values) {
KList<String> list = new KList<>();
for (String value : values) {
list.add(value);
}
return list;
}
private static <T> ProceduralStream<T> constantStream(T value) {
return ProceduralStream.of((x, z) -> value, Interpolated.of(a -> 0D, a -> value));
}
private static <T> ProceduralStream<T> positionalStream(BiFunction<Double, Double, T> function, T fallback) {
return ProceduralStream.of(function::apply, Interpolated.of(a -> 0D, a -> fallback));
}
private void stubRegions(IrisRegion... regions) {
KList<IrisRegion> list = new KList<>();
KList<String> regionKeys = new KList<>();
for (IrisRegion region : regions) {
list.add(region);
regionKeys.add(region.getLoadKey());
}
when(dimension.getAllRegions(engine)).thenReturn(list);
when(dimension.getRegions()).thenReturn(regionKeys);
}
private void stubBiome(IrisBiome biome) {
when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome);
}
@Test
public void appendRingZeroAddsOnlyOrigin() {
KList<Position2> batch = new KList<>();
HintedLocator.appendRing(batch, new Position2(7, -3), 0, 4);
assertEquals(1, batch.size());
assertEquals(new Position2(7, -3), batch.get(0));
}
@Test
public void appendRingProducesFullPerimeterAtStride() {
KList<Position2> batch = new KList<>();
HintedLocator.appendRing(batch, new Position2(0, 0), 3, 4);
assertEquals(24, batch.size());
Set<Position2> unique = new HashSet<>(batch);
assertEquals(24, unique.size());
for (Position2 cell : batch) {
int chebyshev = Math.max(Math.abs(cell.getX()), Math.abs(cell.getZ()));
assertEquals(12, chebyshev);
assertEquals(0, cell.getX() % 4);
assertEquals(0, cell.getZ() % 4);
}
}
@Test
public void appendRingsAtStrideOneTileThePlaneWithoutDuplicates() {
Set<Position2> all = new HashSet<>();
int total = 0;
for (int ring = 0; ring <= 5; ring++) {
KList<Position2> batch = new KList<>();
HintedLocator.appendRing(batch, new Position2(2, 2), ring, 1);
total += batch.size();
all.addAll(batch);
}
assertEquals(121, total);
assertEquals(121, all.size());
}
@Test
public void biomePlanFindsChildBiomeUnderLandRoot() {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("root_land"));
IrisBiome root = biome("root_land");
root.setChildren(keys("target"));
IrisBiome target = biome("target");
stubBiome(root);
stubBiome(target);
stubRegions(regionA);
IrisRegion foreign = region("reg_other");
when(complex.getRegionStream()).thenReturn(positionalStream((x, z) -> x < 0D ? foreign : regionA, regionA));
when(complex.getLandBiomeStream()).thenReturn(constantStream(root));
when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root")));
when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root")));
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "target");
assertTrue(plan.isPossible());
assertNotNull(plan.getCoarse());
assertNull(plan.getExactOverride());
assertTrue(plan.getCoarse().test(100, 100));
assertFalse(plan.getCoarse().test(-100, 100));
}
@Test
public void biomePlanIsImpossibleWhenBiomeIsUnreferenced() {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("root_land"));
stubBiome(biome("root_land"));
stubRegions(regionA);
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "missing");
assertFalse(plan.isPossible());
}
@Test
public void biomePlanFallsBackToCaveStreamForCaveOnlyBiome() {
IrisRegion regionA = region("reg_a");
regionA.setCaveBiomes(keys("cavey"));
IrisBiome cavey = biome("cavey");
stubBiome(cavey);
stubRegions(regionA);
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
when(complex.getCaveBiomeStream()).thenReturn(positionalStream((x, z) -> x >= 512D ? cavey : biome("other_cave"), cavey));
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "cavey");
assertTrue(plan.isPossible());
assertNotNull(plan.getExactOverride());
assertTrue(plan.getCoarse().test(600, 0));
assertFalse(plan.getCoarse().test(0, 0));
when(engine.getCaveBiome(anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
return x >= 512 ? cavey : biome("other_cave");
});
assertTrue(plan.getExactOverride().matches(engine, new Position2(40, 0)));
assertFalse(plan.getExactOverride().matches(engine, new Position2(0, 0)));
}
@Test
public void biomePlanIsUnprunedInFocusMode() {
when(engine.getFocus()).thenReturn(biome("focus"));
HintedLocator.SearchPlan plan = HintedLocator.biomePlan(engine, "anything");
assertTrue(plan.isPossible());
assertNull(plan.getCoarse());
assertEquals(1, plan.getStrideChunks());
}
@Test
public void regionPlanMatchesOnlyTargetRegionCells() {
IrisRegion regionA = region("reg_a");
IrisRegion regionB = region("reg_b");
stubRegions(regionA, regionB);
when(complex.getRegionStream()).thenReturn(positionalStream((x, z) -> x >= 4096D ? regionB : regionA, regionA));
HintedLocator.SearchPlan plan = HintedLocator.regionPlan(engine, "reg_b");
assertTrue(plan.isPossible());
assertTrue(plan.getStrideChunks() > 1);
assertTrue(plan.getCoarse().test(5000, 0));
assertFalse(plan.getCoarse().test(0, 0));
}
@Test
public void regionPlanIsImpossibleForUnknownRegion() {
stubRegions(region("reg_a"));
HintedLocator.SearchPlan plan = HintedLocator.regionPlan(engine, "reg_missing");
assertFalse(plan.isPossible());
}
@Test
public void objectPlanPrunesToHostBiomes() {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("host_biome"));
IrisBiome host = biome("host_biome");
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setPlace(keys("trees/oak"));
KList<IrisObjectPlacement> placements = new KList<>();
placements.add(placement);
host.setObjects(placements);
stubBiome(host);
stubRegions(regionA);
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
when(complex.getLandBiomeStream()).thenReturn(positionalStream((x, z) -> x >= 1024D ? host : biome("plains"), host));
when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root")));
when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root")));
HintedLocator.SearchPlan plan = HintedLocator.objectPlan(engine, "trees/oak");
assertTrue(plan.isPossible());
assertEquals(1, plan.getStrideChunks());
assertTrue(plan.getCoarse().test(2000, 0));
assertFalse(plan.getCoarse().test(0, 0));
}
@Test
public void objectPlanAcceptsRegionLevelPlacements() {
IrisRegion regionA = region("reg_a");
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setPlace(keys("ruins/tower"));
KList<IrisObjectPlacement> placements = new KList<>();
placements.add(placement);
regionA.setObjects(placements);
stubRegions(regionA);
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
when(complex.getLandBiomeStream()).thenReturn(constantStream(biome("plains")));
when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root")));
when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root")));
HintedLocator.SearchPlan plan = HintedLocator.objectPlan(engine, "ruins/tower");
assertTrue(plan.isPossible());
assertTrue(plan.getCoarse().test(0, 0));
}
@Test
public void objectPlanIsImpossibleForUnplacedObject() {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("plains"));
stubBiome(biome("plains"));
stubRegions(regionA);
HintedLocator.SearchPlan plan = HintedLocator.objectPlan(engine, "missing/object");
assertFalse(plan.isPossible());
}
@Test
public void findLocatesDistantBiomeBandWithoutChunkScan() throws Exception {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("target"));
IrisBiome target = biome("target");
IrisBiome plains = biome("plains");
stubBiome(target);
stubRegions(regionA);
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
when(complex.getLandBiomeStream()).thenReturn(positionalStream((x, z) -> x >= 4096D ? target : plains, plains));
when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root")));
when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root")));
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
return x >= 4096 ? target : plains;
});
Locator<IrisBiome> locator = Locator.surfaceBiome("target");
AtomicInteger checks = new AtomicInteger();
Position2 result = locator.find(engine, new Position2(0, 0), 60_000, checks::set).get();
assertNotNull(result);
assertTrue(((result.getX() << 4) + 8) >= 4096);
}
@Test
public void findReturnsNullFastForImpossibleTarget() throws Exception {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("plains"));
stubBiome(biome("plains"));
stubRegions(regionA);
Locator<IrisBiome> locator = Locator.surfaceBiome("missing");
long start = System.currentTimeMillis();
Position2 result = locator.find(engine, new Position2(0, 0), 120_000, (Integer count) -> {
}).get();
long elapsed = System.currentTimeMillis() - start;
assertNull(result);
assertTrue(elapsed < 10_000);
}
@Test
public void findLocatesObjectThroughCoarseCascade() throws Exception {
IrisRegion regionA = region("reg_a");
regionA.setLandBiomes(keys("host_biome"));
IrisBiome host = biome("host_biome");
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setPlace(keys("trees/oak"));
KList<IrisObjectPlacement> placements = new KList<>();
placements.add(placement);
host.setObjects(placements);
stubBiome(host);
stubRegions(regionA);
when(complex.getRegionStream()).thenReturn(constantStream(regionA));
when(complex.getLandBiomeStream()).thenReturn(positionalStream((x, z) -> x >= 1024D ? host : biome("plains"), host));
when(complex.getSeaBiomeStream()).thenReturn(constantStream(biome("sea_root")));
when(complex.getShoreBiomeStream()).thenReturn(constantStream(biome("shore_root")));
when(engine.getObjectsAt(anyInt(), anyInt())).thenAnswer(invocation -> {
int chunkX = invocation.getArgument(0);
Set<String> found = new HashSet<>();
if (chunkX >= 64) {
found.add("trees/oak");
}
return found;
});
Locator<art.arcane.iris.engine.object.IrisObject> locator = Locator.object("trees/oak");
Position2 result = locator.find(engine, new Position2(0, 0), 60_000, (Integer count) -> {
}).get();
assertNotNull(result);
assertTrue(result.getX() >= 64);
}
}
@@ -19,9 +19,19 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.engine.object.StructureDistribution;
import art.arcane.volmlib.util.collection.KList;
@@ -35,6 +45,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -71,6 +84,196 @@ public class IrisStructureLocatorContractTest {
assertFalse(IrisStructureLocator.suppressesVanilla(engine, ""));
}
@Test
public void vanillaSourceRequiresExplicitDimensionSuppression() {
IrisData data = mock(IrisData.class);
IrisStructure structure = new IrisStructure();
structure.setLoadKey("test:city");
structure.setVanillaSource("minecraft:ancient_city");
when(data.load(IrisStructure.class, "test:city", false)).thenReturn(structure);
registerSinglePieceGraph(data, structure, "test:city");
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getStructures().add("test:city");
IrisDimension dimension = mock(IrisDimension.class);
KList<IrisStructurePlacement> placements = new KList<>();
placements.add(placement);
when(dimension.getStructures()).thenReturn(placements);
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
assertTrue(IrisStructureLocator.isPlaced(engine, "minecraft:ancient_city"));
assertFalse(IrisStructureLocator.suppressesVanilla(engine, "minecraft:ancient_city"));
placement.setNativeSuppression(NativeStructureSuppression.REPLACE_SOURCE);
IrisStructureLocator.invalidate(engine);
assertTrue(IrisStructureLocator.suppressesVanilla(engine, "minecraft:ancient_city"));
}
@Test
public void regionReplacementFailsInsteadOfFallingBackToNative() {
IrisData data = mock(IrisData.class);
IrisStructure structure = new IrisStructure();
structure.setLoadKey("test:city");
structure.setVanillaSource("minecraft:ancient_city");
when(data.load(IrisStructure.class, "test:city", false)).thenReturn(structure);
registerSinglePieceGraph(data, structure, "test:city");
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getStructures().add("test:city");
placement.setNativeSuppression(NativeStructureSuppression.REPLACE_SOURCE);
IrisRegion region = mock(IrisRegion.class);
when(region.getStructures()).thenReturn(new KList<IrisStructurePlacement>().qadd(placement));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getStructures()).thenReturn(new KList<>());
when(dimension.getAllRegions(any())).thenReturn(new KList<IrisRegion>().qadd(region));
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
assertReplacementFailure(
() -> IrisStructureLocator.suppressesVanilla(engine, "minecraft:ancient_city"),
"dimension-level");
}
@Test
public void geometryFailureFailsInsteadOfFallingBackToNative() {
IrisData data = mock(IrisData.class);
IrisStructure structure = new IrisStructure()
.setStartPool("test:start")
.setMaxDepth(1)
.setMaxSizeChunks(1)
.setVanillaSource("minecraft:ancient_city");
structure.setLoadKey("test:city");
IrisJigsawConnector source = new IrisJigsawConnector()
.setPosition(new IrisPosition(0, 0, 0))
.setDirection(IrisDirection.EAST_POSITIVE_X)
.setPool("test:target")
.setName("source")
.setTargetName("door");
IrisJigsawConnector target = new IrisJigsawConnector()
.setPosition(new IrisPosition(50, 0, 0))
.setDirection(IrisDirection.WEST_NEGATIVE_X)
.setPool("test:target")
.setName("door")
.setTargetName("unused");
IrisJigsawPiece startPiece = new IrisJigsawPiece().setObject("test:start-object").setRotatable(false);
startPiece.getConnectors().add(source);
IrisJigsawPiece targetPiece = new IrisJigsawPiece().setObject("test:target-object").setRotatable(false);
targetPiece.getConnectors().add(target);
IrisJigsawPool startPool = new IrisJigsawPool();
startPool.getPieces().add(new IrisJigsawPieceEntry("test:start-piece", 1));
IrisJigsawPool targetPool = new IrisJigsawPool();
targetPool.getPieces().add(new IrisJigsawPieceEntry("test:target-piece", 1));
IrisObject startObject = mock(IrisObject.class);
IrisObject targetObject = mock(IrisObject.class);
when(startObject.getW()).thenReturn(1);
when(startObject.getH()).thenReturn(1);
when(startObject.getD()).thenReturn(1);
when(targetObject.getW()).thenReturn(100);
when(targetObject.getH()).thenReturn(1);
when(targetObject.getD()).thenReturn(1);
when(data.load(IrisStructure.class, "test:city", false)).thenReturn(structure);
when(data.load(IrisJigsawPool.class, "test:start", false)).thenReturn(startPool);
when(data.load(IrisJigsawPool.class, "test:target", false)).thenReturn(targetPool);
when(data.load(IrisJigsawPiece.class, "test:start-piece", false)).thenReturn(startPiece);
when(data.load(IrisJigsawPiece.class, "test:target-piece", false)).thenReturn(targetPiece);
when(data.load(IrisObject.class, "test:start-object", false)).thenReturn(startObject);
when(data.load(IrisObject.class, "test:target-object", false)).thenReturn(targetObject);
assertTrue(StructureGraphCatalog.compile(data, structure).isAssemblyViable());
IrisStructurePlacement placement = new IrisStructurePlacement()
.setNativeSuppression(NativeStructureSuppression.REPLACE_SOURCE);
placement.getStructures().add("test:city");
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getStructures()).thenReturn(new KList<IrisStructurePlacement>().qadd(placement));
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
assertReplacementFailure(
() -> IrisStructureLocator.suppressesVanilla(engine, "minecraft:ancient_city"),
"not runtime-viable");
}
@Test
public void missingVanillaSourceFailsInsteadOfFallingBackToNative() {
IrisData data = mock(IrisData.class);
IrisStructure structure = new IrisStructure();
structure.setLoadKey("test:city");
when(data.load(IrisStructure.class, "test:city", false)).thenReturn(structure);
registerSinglePieceGraph(data, structure, "test:city");
IrisStructurePlacement placement = new IrisStructurePlacement()
.setNativeSuppression(NativeStructureSuppression.REPLACE_SOURCE);
placement.getStructures().add("test:city");
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getStructures()).thenReturn(new KList<IrisStructurePlacement>().qadd(placement));
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
assertReplacementFailure(
() -> IrisStructureLocator.suppressesVanilla(engine, "minecraft:ancient_city"),
"valid namespaced vanillaSource");
}
@Test
public void missingNonReplacementStructureFailsInsteadOfBeingSkipped() {
IrisData data = mock(IrisData.class);
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getStructures().add("test:missing");
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getStructures()).thenReturn(new KList<IrisStructurePlacement>().qadd(placement));
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getData()).thenReturn(data);
when(engine.getDimension()).thenReturn(dimension);
assertReplacementFailure(
() -> IrisStructureLocator.isPlaced(engine, "test:missing"),
"references missing structure 'test:missing'");
}
@Test
public void placementResolutionRejectsAnUnboundEngine() {
assertReplacementFailure(
() -> IrisStructureLocator.resolvePlacement(null, new IrisStructurePlacement(), 0, 0),
"fully bound engine");
}
@Test
public void replacementOutputFailureIncludesStructureAndChunkContext() {
IrisStructurePlacement placement = new IrisStructurePlacement()
.setNativeSuppression(NativeStructureSuppression.REPLACE_SOURCE);
placement.getStructures().add("test:city");
assertReplacementFailure(
() -> IrisStructureLocator.requirePlacementOutput(
placement, "test:city", 12, -7, false, "runtime assembly produced no pieces"),
"structure 'test:city' failed in chunk 12,-7: runtime assembly produced no pieces");
}
@Test
public void nonReplacementOutputAbsenceRemainsSkippable() {
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getStructures().add("test:city");
assertFalse(IrisStructureLocator.requirePlacementOutput(
placement, "test:city", 12, -7, false, "runtime assembly produced no pieces"));
assertTrue(IrisStructureLocator.requirePlacementOutput(
placement, "test:city", 12, -7, true, "runtime assembly produced no pieces"));
}
@Test
public void startsInChunkIsFalseForNullEngine() {
assertFalse(IrisStructureLocator.startsInChunk(null, "minecraft:ancient_city", 0, 0));
@@ -229,6 +432,47 @@ public class IrisStructureLocatorContractTest {
assertNull(IrisStructureLocator.resolveVerticalShift(clippedSurface, placement, 300, -64, 319));
}
@Test
public void undergroundAssemblyIsShiftedBelowEveryTerrainColumn() {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt(), eq(true)))
.thenAnswer(invocation -> invocation.<Integer>getArgument(0) == 1 ? 140 : 164);
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.setUnderground(true);
placement.setMinHeight(-64);
placement.setMaxHeight(100);
KList<PlacedStructurePiece> pieces = new KList<>();
pieces.add(piece(0, 60, 0, 1, 80, 0));
assertEquals(Integer.valueOf(-5), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
placement.setMinHeight(60);
placement.setMaxHeight(60);
assertNull(IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
}
@Test
public void undergroundBurialIncludesOverboreCeiling() {
Engine engine = mock(Engine.class);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(164);
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.setUnderground(true);
placement.setMinHeight(-64);
placement.setMaxHeight(100);
placement.setOverbore(true);
placement.setOverboreRadius(2);
placement.setOverboreHeight(20);
KList<PlacedStructurePiece> pieces = new KList<>();
pieces.add(piece(0, 60, 0, 1, 80, 1));
assertEquals(Integer.valueOf(-17), IrisStructureLocator.resolveUndergroundBurialShift(
engine, pieces, placement, 60, -63, 319));
}
@Test
public void exactYOnlyCoversPlacementPathsWithResolvedAbsoluteAnchors() {
IrisStructurePlacement placement = new IrisStructurePlacement();
@@ -280,6 +524,7 @@ public class IrisStructureLocatorContractTest {
IrisStructure structure = new IrisStructure();
structure.setLoadKey("test:density");
when(data.load(IrisStructure.class, "test:density", false)).thenReturn(structure);
registerSinglePieceGraph(data, structure, "test:density");
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.getStructures().add("test:density");
@@ -303,4 +548,32 @@ public class IrisStructureLocatorContractTest {
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
return engine;
}
private void registerSinglePieceGraph(IrisData data, IrisStructure structure, String prefix) {
String poolKey = prefix + "/start";
String pieceKey = prefix + "/piece";
String objectKey = prefix + "/object";
structure.setStartPool(poolKey);
IrisJigsawPool pool = new IrisJigsawPool();
pool.getPieces().add(new IrisJigsawPieceEntry().setPiece(pieceKey).setWeight(1));
IrisJigsawPiece piece = new IrisJigsawPiece();
piece.setObject(objectKey);
IrisObject object = mock(IrisObject.class);
when(object.getW()).thenReturn(1);
when(object.getH()).thenReturn(1);
when(object.getD()).thenReturn(1);
when(data.load(IrisJigsawPool.class, poolKey, false)).thenReturn(pool);
when(data.load(IrisJigsawPiece.class, pieceKey, false)).thenReturn(piece);
when(data.load(IrisObject.class, objectKey, false)).thenReturn(object);
}
private void assertReplacementFailure(Runnable operation, String expectedMessage) {
try {
operation.run();
} catch (IllegalStateException e) {
assertTrue(e.getMessage(), e.getMessage().contains(expectedMessage));
return;
}
throw new AssertionError("Expected strict native replacement failure containing '" + expectedMessage + "'");
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import java.util.Set;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class LocatorPoiCoordinateTest {
@Test
public void poiLocatorQueriesCandidateChunkCoordinates() {
Engine engine = mock(Engine.class);
BlockPos position = new BlockPos(50, 64, -30);
when(engine.getPOIsAt(3, -2)).thenReturn(Set.of(new Pair<>("buried_treasure", position)));
boolean matched = Locator.poi("buried_treasure").matches(engine, new Position2(3, -2));
assertTrue(matched);
verify(engine).getPOIsAt(3, -2);
}
}
@@ -0,0 +1,318 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.InventorySlotType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.iris.engine.object.IrisLootReference;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
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.assertNotEquals;
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.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class LootResolverTest {
@Test
public void inclusiveRollCanReachBothConfiguredBounds() {
RNG rng = new RNG(417L);
Set<Integer> results = new HashSet<>();
for (int i = 0; i < 256; i++) {
results.add(LootResolver.inclusive(rng, 1, 3));
}
assertEquals(Set.of(1, 2, 3), results);
}
@Test
public void inclusiveRollNormalizesReversedBounds() {
RNG rng = new RNG(912L);
for (int i = 0; i < 64; i++) {
int value = LootResolver.inclusive(rng, 7, 3);
assertTrue(value >= 3 && value <= 7);
}
}
@Test
public void oneInUsesTheWholeConfiguredRange() {
RNG success = new FixedRollRng(0);
RNG failure = new FixedRollRng(7);
assertTrue(LootResolver.oneIn(success, 8));
assertFalse(LootResolver.oneIn(failure, 8));
assertTrue(LootResolver.oneIn(failure, 1));
}
@Test
public void spatialSeedsIncludeWorldTableAndAbsolutePosition() {
IrisLootTable first = new IrisLootTable().setName("first");
IrisLootTable second = new IrisLootTable().setName("second");
long baseline = LootResolver.tableSeed(41L, first, 0, 64, 0);
assertEquals(baseline, LootResolver.tableSeed(41L, first, 0, 64, 0));
assertNotEquals(baseline, LootResolver.tableSeed(42L, first, 0, 64, 0));
assertNotEquals(baseline, LootResolver.tableSeed(41L, second, 0, 64, 0));
assertNotEquals(baseline, LootResolver.tableSeed(41L, first, 16, 64, 0));
}
@Test
public void spatialRarityIsStableAndDistributed() {
IrisLootTable table = new IrisLootTable().setName("distribution");
int successes = 0;
for (int x = 0; x < 65_536; x++) {
if (LootResolver.spatialOneIn(91L, table, 3, x, 40, x * 31, 8L)) {
successes++;
}
}
assertTrue(successes > 7_600);
assertTrue(successes < 8_800);
assertEquals(
LootResolver.spatialOneIn(91L, table, 3, 112, 40, 992, 8L),
LootResolver.spatialOneIn(91L, table, 3, 112, 40, 992, 8L)
);
}
@Test
public void nativeOrObjectLootSuppressesFallbackReferences() {
ResolutionFixture fixture = new ResolutionFixture();
List<IrisLootTable> sources = new ArrayList<>();
LootResolver.resolveEnvironmentSources(
sources,
fixture.engine,
new RNG(19L),
10,
70,
20,
true,
table -> table
);
assertTrue(sources.isEmpty());
}
@Test
public void emptyPlacementUsesEachFallbackScopeOnce() {
ResolutionFixture fixture = new ResolutionFixture();
List<IrisLootTable> sources = new ArrayList<>();
LootResolver.resolveEnvironmentSources(
sources,
fixture.engine,
new RNG(19L),
10,
70,
20,
false,
table -> table
);
assertEquals(List.of(fixture.dimensionTable, fixture.regionTable, fixture.surfaceTable), sources);
}
@Test
public void sourceScalingCanSelectTheLastSource() {
List<String> sources = new ArrayList<>(List.of("first", "last"));
RNG rng = new FixedRollRng(1);
LootResolver.scaleSources(sources, 1.5D, rng);
assertEquals(List.of("first", "last", "last"), sources);
}
@Test
public void sourceScalingRejectsUnsafeMultipliersBeforeMutation() {
List<String> sources = new ArrayList<>(List.of("first", "second"));
IllegalArgumentException nonFinite = assertThrows(IllegalArgumentException.class,
() -> LootResolver.scaleSources(sources, Double.POSITIVE_INFINITY, new RNG(17L)));
IllegalArgumentException negative = assertThrows(IllegalArgumentException.class,
() -> LootResolver.scaleSources(sources, -1D, new RNG(17L)));
assertTrue(nonFinite.getMessage().contains("must be finite"));
assertTrue(negative.getMessage().contains("non-negative"));
assertEquals(List.of("first", "second"), sources);
}
@Test
public void sourceScalingRejectsExcessiveTargetAllocationBeforeMutation() {
List<String> sources = new ArrayList<>();
for (int index = 0; index < 17; index++) {
sources.add("table-" + index);
}
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> LootResolver.scaleSources(sources, IrisLootReference.MAX_MULTIPLIER, new RNG(17L)));
assertTrue(failure.getMessage().contains("maximum of 256 sources"));
assertEquals(17, sources.size());
}
@Test
public void sourceScalingAllowsExactResolvedSourceBoundary() {
List<String> sources = new ArrayList<>();
for (int index = 0; index < 16; index++) {
sources.add("table-" + index);
}
LootResolver.scaleSources(sources, IrisLootReference.MAX_MULTIPLIER, new RNG(17L));
assertEquals(256, sources.size());
}
@Test
public void sourceScalingRejectsNumericOverflowBeforeMutation() {
List<String> sources = new ArrayList<>(List.of("first", "second"));
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> LootResolver.scaleSources(sources, Double.MAX_VALUE, new RNG(17L)));
assertTrue(failure.getMessage().contains("maximum of 256 sources"));
assertEquals(List.of("first", "second"), sources);
}
@Test
public void sourceScalingRejectsOversizedInputBeforeSnapshotAllocation() {
List<String> sources = new ArrayList<>();
for (int index = 0; index < 257; index++) {
sources.add("table-" + index);
}
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> LootResolver.scaleSources(sources, 0D, new RNG(17L)));
assertTrue(failure.getMessage().contains("starts above the maximum of 256"));
assertEquals(257, sources.size());
}
@Test
public void wholeNumberScalingPreservesTableBalanceAndIsDeterministic() {
List<String> first = new ArrayList<>(List.of("alpha", "beta", "gamma"));
List<String> second = new ArrayList<>(first);
LootResolver.scaleSources(first, 4D, new RNG(817L));
LootResolver.scaleSources(second, 4D, new RNG(817L));
assertEquals(first, second);
assertEquals(4, Collections.frequency(first, "alpha"));
assertEquals(4, Collections.frequency(first, "beta"));
assertEquals(4, Collections.frequency(first, "gamma"));
}
@Test
public void combinedMultiplierAboveReferenceCapRemainsUsableUnderResolvedSourceCap() {
List<String> sources = new ArrayList<>(List.of("table"));
LootResolver.scaleSources(sources, IrisLootReference.MAX_MULTIPLIER * 2D, new RNG(17L));
assertEquals(32, sources.size());
}
@Test
public void lootMultiplierCapIsPublishedInSchemaMetadata() throws Exception {
Field multiplier = IrisLootReference.class.getDeclaredField("multiplier");
MaxNumber maximum = multiplier.getAnnotation(MaxNumber.class);
Desc description = multiplier.getAnnotation(Desc.class);
assertEquals(IrisLootReference.MAX_MULTIPLIER, maximum.value(), 0D);
assertTrue(description.value().contains("0 to 16"));
}
@Test
public void emptyLootTableReturnsNoItems() {
IrisLootTable table = new IrisLootTable().setName("empty");
assertTrue(table.getLoot(false, 7L, InventorySlotType.STORAGE, null, 0, 64, 0).isEmpty());
}
@Test
public void weightedSelectionRejectsNonPositivePoolsAndCanReachLastEntry() {
List<WeightedChoice> invalid = List.of(new WeightedChoice("zero", 0), new WeightedChoice("negative", -4));
List<WeightedChoice> valid = List.of(new WeightedChoice("first", 1), new WeightedChoice("last", 1));
assertNull(LootResolver.pickWeighted(invalid, WeightedChoice::weight, new FixedRollRng(0)));
assertEquals("last", LootResolver.pickWeighted(valid, WeightedChoice::weight, new FixedRollRng(1)).name());
}
private static final class ResolutionFixture {
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
private final IrisLootTable dimensionTable = new IrisLootTable().setName("dimension");
private final IrisLootTable regionTable = new IrisLootTable().setName("region");
private final IrisLootTable surfaceTable = new IrisLootTable().setName("surface");
private ResolutionFixture() {
IrisDimension dimension = mock(IrisDimension.class);
IrisRegion region = mock(IrisRegion.class);
IrisBiome surface = mock(IrisBiome.class);
IrisComplex complex = engine.getComplex();
IrisLootReference dimensionReference = reference(dimensionTable);
IrisLootReference regionReference = reference(regionTable);
IrisLootReference surfaceReference = reference(surfaceTable);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getLoot()).thenReturn(dimensionReference);
when(region.getLoot()).thenReturn(regionReference);
when(surface.getLoot()).thenReturn(surfaceReference);
when(surface.getLoadKey()).thenReturn("surface");
when(complex.getRegionStream().get(10, 20)).thenReturn(region);
when(complex.getTrueBiomeStream().get(10, 20)).thenReturn(surface);
when(complex.getHeightStream().get(10, 20)).thenReturn(64D);
when(engine.getCaveBiome(10, 70, 20)).thenReturn(surface);
}
private IrisLootReference reference(IrisLootTable table) {
IrisLootReference reference = mock(IrisLootReference.class);
when(reference.getMode()).thenReturn(IrisLootMode.FALLBACK);
when(reference.getMultiplier()).thenReturn(1D);
when(reference.getLootTables(any())).thenReturn(new KList<>(table));
return reference;
}
}
private static final class FixedRollRng extends RNG {
private final int roll;
private FixedRollRng(int roll) {
super(1L);
this.roll = roll;
}
@Override
public int nextInt(int bound) {
return Math.min(roll, bound - 1);
}
@Override
public long nextLong(long bound) {
return Math.min(roll, bound - 1L);
}
}
private record WeightedChoice(String name, int weight) {
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class NativeStructureGenerationPolicyTest {
@Test
public void generationStatusMessagesAreSharedAcrossPlatforms() {
assertEquals(
"Native structure minecraft:village_plains is disabled by this dimension's importedStructures settings.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:village_plains", NativeStructureGenerationStatus.DISABLED_BY_PACK));
assertEquals(
"Native structure minecraft:ancient_city is replaced by an Iris placement in this pack and locates through that explicit replacement.",
NativeStructureGenerationPolicy.generationStatusMessage(
"minecraft:ancient_city", NativeStructureGenerationStatus.REPLACED_BY_IRIS));
}
}
@@ -0,0 +1,455 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.JigsawJoint;
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.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.AfterClass;
import org.junit.BeforeClass;
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.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureAssemblerTest {
@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 attachedNonRotatablePieceKeepsItsAuthoredFacing() {
Fixture fixture = new Fixture();
IrisJigsawConnector startConnector = connector(IrisDirection.EAST_POSITIVE_X, "target", "start", "door");
IrisJigsawPiece startPiece = piece("start-object", false, startConnector);
IrisJigsawConnector candidateConnector = connector(IrisDirection.NORTH_NEGATIVE_Z, "", "door", "");
IrisJigsawPiece candidatePiece = piece("candidate-object", false, candidateConnector);
fixture.pool("start", pool("", "start-piece"));
fixture.pool("target", pool("", "candidate-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("candidate-piece", candidatePiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("candidate-object", new IrisObject(1, 1, 1));
KList<PlacedStructurePiece> fixed = fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(11L));
assertNull(fixed);
candidatePiece.setRotatable(true);
KList<PlacedStructurePiece> rotatable = fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(11L));
assertEquals(2, rotatable.size());
assertSame(candidatePiece, rotatable.get(1).getPiece());
}
@Test
public void fallbackCandidatesAreTriedAfterPrimaryCandidatesFail() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "branch", "start", "door"));
IrisJigsawPiece incompatiblePiece = piece("incompatible-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "wrong", ""));
IrisJigsawPiece fallbackPiece = piece("fallback-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("branch", pool("fallback", "incompatible-piece"));
fixture.pool("fallback", pool("", "fallback-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("incompatible-piece", incompatiblePiece);
fixture.piece("fallback-piece", fallbackPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("incompatible-object", new IrisObject(1, 1, 1));
fixture.object("fallback-object", new IrisObject(1, 1, 1));
KList<PlacedStructurePiece> placed = fixture.assembler(structure("start", 2), 0, 0, 0).assemble(new RNG(29L));
assertEquals(2, placed.size());
assertSame(fallbackPiece, placed.get(1).getPiece());
}
@Test
public void alignedRuntimeConnectorsPreserveTopOrientation() {
Fixture fixture = new Fixture();
IrisJigsawConnector source = connector(IrisDirection.EAST_POSITIVE_X, "target", "start", "door")
.setJoint(JigsawJoint.ALIGNED)
.setTop(IrisDirection.UP_POSITIVE_Y);
IrisJigsawConnector target = connector(IrisDirection.WEST_NEGATIVE_X, "target", "door", "unused")
.setTop(IrisDirection.DOWN_NEGATIVE_Y);
IrisJigsawPiece startPiece = piece("start-object", false, source);
IrisJigsawPiece targetPiece = piece("target-object", false, target);
fixture.pool("start", pool("", "start-piece"));
fixture.pool("target", pool("", "target-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("target-piece", targetPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("target-object", new IrisObject(1, 1, 1));
assertNull(fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(13L)));
target.setTop(IrisDirection.UP_POSITIVE_Y);
KList<PlacedStructurePiece> placed = fixture.assembler(structure("start", 1), 0, 0, 0)
.assemble(new RNG(13L));
assertEquals(2, placed.size());
}
@Test
public void fallbackAtMaximumDepthDoesNotExpandAgain() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "branch", "start", "door"));
IrisJigsawPiece branchPiece = piece("branch-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""),
connector(IrisDirection.EAST_POSITIVE_X, "terminal", "branch", "door"));
IrisJigsawPiece primaryTerminal = piece("primary-terminal-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""));
IrisJigsawPiece fallbackTerminal = piece("fallback-terminal-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""),
connector(IrisDirection.EAST_POSITIVE_X, "terminal", "terminal", "door"));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("branch", pool("", "branch-piece"));
fixture.pool("terminal", pool("terminal-fallback", "primary-terminal-piece"));
fixture.pool("terminal-fallback", pool("", "fallback-terminal-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("branch-piece", branchPiece);
fixture.piece("primary-terminal-piece", primaryTerminal);
fixture.piece("fallback-terminal-piece", fallbackTerminal);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("branch-object", new IrisObject(1, 1, 1));
fixture.object("primary-terminal-object", new IrisObject(1, 1, 1));
fixture.object("fallback-terminal-object", new IrisObject(1, 1, 1));
IrisStructure structure = structure("start", 1);
structure.setMaxSizeChunks(1);
KList<PlacedStructurePiece> placed = fixture.assembler(structure, 0, 0, 0).assemble(new RNG(41L));
assertEquals(3, placed.size());
assertSame(branchPiece, placed.get(1).getPiece());
assertSame(fallbackTerminal, placed.get(2).getPiece());
}
@Test
public void fallbackOfFallbackIsNotSelected() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "branch", "start", "door"));
IrisJigsawPiece incompatiblePiece = piece("incompatible-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "wrong", ""));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("branch", pool("terminators", "incompatible-piece"));
fixture.pool("terminators", pool("empty", "incompatible-piece"));
fixture.pool("empty", pool(""));
fixture.piece("start-piece", startPiece);
fixture.piece("incompatible-piece", incompatiblePiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("incompatible-object", new IrisObject(1, 1, 1));
assertNull(fixture.assembler(structure("start", 2), 0, 0, 0).assemble(new RNG(31L)));
}
@Test
public void evenSizedBoundsContainExactlyTheObjectFootprint() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false);
fixture.pool("start", pool("", "start-piece"));
fixture.piece("start-piece", startPiece);
fixture.object("start-object", new IrisObject(4, 2, 6));
PlacedStructurePiece placed = fixture.assembler(structure("start", 1), 10, 20, 30)
.assemble(new RNG(53L)).getFirst();
assertBounds(placed, 8, 19, 27, 11, 20, 32);
}
@Test
public void rotatedEvenSizedBoundsPreserveAsymmetricCentering() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", true);
fixture.pool("start", pool("", "start-piece"));
fixture.piece("start-piece", startPiece);
fixture.object("start-object", new IrisObject(4, 2, 6));
StructureAssembler assembler = fixture.assembler(structure("start", 1), 10, 20, 30);
assertBounds(assembler.assemble(rotationIndex(1)).getFirst(), 7, 19, 29, 12, 20, 32);
assertBounds(assembler.assemble(rotationIndex(2)).getFirst(), 9, 19, 28, 12, 20, 33);
assertBounds(assembler.assemble(rotationIndex(3)).getFirst(), 8, 19, 28, 13, 20, 31);
}
@Test
public void collisionUsesHalfOpenEdgesForInclusivePieceBounds() {
PlacedStructurePiece origin = bounds(0, 0, 0, 0, 0, 0);
PlacedStructurePiece overlap = bounds(0, 0, 0, 0, 0, 0);
PlacedStructurePiece adjacent = bounds(1, 0, 0, 1, 0, 0);
assertTrue(origin.intersects(overlap));
assertFalse(origin.intersects(adjacent));
assertFalse(adjacent.intersects(origin));
}
@Test
public void maximumWeightsCannotOverflowRuntimeSelection() {
Fixture fixture = new Fixture();
IrisJigsawPiece first = piece("first-object", false);
IrisJigsawPiece second = piece("second-object", false);
IrisJigsawPool start = new IrisJigsawPool();
start.getPieces().add(new IrisJigsawPieceEntry("first-piece", Integer.MAX_VALUE));
start.getPieces().add(new IrisJigsawPieceEntry("second-piece", Integer.MAX_VALUE));
fixture.pool("start", start);
fixture.piece("first-piece", first);
fixture.piece("second-piece", second);
fixture.object("first-object", new IrisObject(1, 1, 1));
fixture.object("second-object", new IrisObject(1, 1, 1));
for (long seed = 0; seed < 32; seed++) {
KList<PlacedStructurePiece> placed = fixture.assembler(structure("start", 1), 0, 0, 0)
.assemble(new RNG(seed));
assertEquals(1, placed.size());
}
}
@Test
public void malformedStartConnectorDataFailsLoudly() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false);
startPiece.setConnectors(null);
fixture.pool("start", pool("", "start-piece"));
fixture.piece("start-piece", startPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
assertThrows(IllegalStateException.class,
() -> fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(3L)));
}
@Test
public void corruptWeightedPeerFailsBeforeCandidateSelection() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "branch", "start", "door"));
IrisJigsawPiece validPiece = piece("valid-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""));
IrisJigsawPiece fallbackPiece = piece("fallback-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("branch", pool("fallback", "valid-piece", "missing-piece"));
fixture.pool("fallback", pool("", "fallback-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("valid-piece", validPiece);
fixture.piece("fallback-piece", fallbackPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("valid-object", new IrisObject(1, 1, 1));
fixture.object("fallback-object", new IrisObject(1, 1, 1));
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> fixture.assembler(structure("start", 2), 0, 0, 0).assemble(new RNG(17L)));
assertTrue(failure.getMessage().contains("branch"));
assertTrue(failure.getMessage().contains("missing-piece"));
}
@Test
public void corruptUnselectedStartEntryFailsBeforeCandidateSelection() {
Fixture fixture = new Fixture();
IrisJigsawPiece validPiece = piece("valid-object", false);
fixture.pool("start", pool("", "valid-piece", "missing-piece"));
fixture.piece("valid-piece", validPiece);
fixture.object("valid-object", new IrisObject(1, 1, 1));
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> fixture.assembler(structure("start", 1), 0, 0, 0).assemble(new RNG(18L)));
assertTrue(failure.getMessage().contains("start"));
assertTrue(failure.getMessage().contains("missing-piece"));
}
@Test
public void malformedNestedConnectorDoesNotFallThroughToAuthoredFallback() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "branch", "start", "door"));
IrisJigsawPiece malformedPiece = piece("malformed-object", false);
malformedPiece.getConnectors().add((IrisJigsawConnector) null);
IrisJigsawPiece fallbackPiece = piece("fallback-object", false,
connector(IrisDirection.WEST_NEGATIVE_X, "", "door", ""));
fixture.pool("start", pool("", "start-piece"));
fixture.pool("branch", pool("fallback", "malformed-piece"));
fixture.pool("fallback", pool("", "fallback-piece"));
fixture.piece("start-piece", startPiece);
fixture.piece("malformed-piece", malformedPiece);
fixture.piece("fallback-piece", fallbackPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
fixture.object("malformed-object", new IrisObject(1, 1, 1));
fixture.object("fallback-object", new IrisObject(1, 1, 1));
assertThrows(IllegalStateException.class,
() -> fixture.assembler(structure("start", 2), 0, 0, 0).assemble(new RNG(19L)));
}
@Test
public void emptyPoolChoiceTerminatesTheBranchWithoutPlacingAir() {
Fixture fixture = new Fixture();
IrisJigsawPiece startPiece = piece("start-object", false,
connector(IrisDirection.EAST_POSITIVE_X, "terminal", "start", "door"));
fixture.pool("start", pool("", "start-piece"));
IrisJigsawPool terminal = new IrisJigsawPool();
terminal.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true).setWeight(1));
fixture.pool("terminal", terminal);
fixture.piece("start-piece", startPiece);
fixture.object("start-object", new IrisObject(1, 1, 1));
KList<PlacedStructurePiece> placed = fixture.assembler(structure("start", 2), 0, 0, 0)
.assemble(new RNG(7L));
assertEquals(1, placed.size());
assertSame(startPiece, placed.getFirst().getPiece());
}
@Test
public void emptyStartChoiceProducesAnIntentionalEmptyAssembly() {
Fixture fixture = new Fixture();
IrisJigsawPool start = new IrisJigsawPool();
start.getPieces().add(new IrisJigsawPieceEntry().setEmpty(true).setWeight(1));
fixture.pool("start", start);
KList<PlacedStructurePiece> placed = fixture.assembler(structure("start", 1), 0, 0, 0)
.assemble(new RNG(9L));
assertTrue(placed.isEmpty());
}
@Test
public void assemblerRejectsLimitsOutsideTheDeclaredContract() {
Fixture fixture = new Fixture();
IrisStructure deep = structure("start", 31);
IrisStructure wide = structure("start", 1).setMaxSizeChunks(33);
IrisStructure overflowing = structure("start", 1).setMaxSizeChunks(Integer.MAX_VALUE);
assertThrows(IllegalStateException.class,
() -> fixture.assembler(deep, 0, 0, 0).assemble(new RNG(1L)));
assertThrows(IllegalStateException.class,
() -> fixture.assembler(wide, 0, 0, 0).assemble(new RNG(1L)));
assertThrows(IllegalStateException.class,
() -> fixture.assembler(overflowing, 0, 0, 0).assemble(new RNG(1L)));
}
private static IrisJigsawConnector connector(IrisDirection direction, String pool, String name, String targetName) {
return new IrisJigsawConnector()
.setPosition(new IrisPosition())
.setDirection(direction)
.setPool(pool == null || pool.isBlank() ? "terminal" : pool)
.setName(name)
.setTargetName(targetName)
.setJoint(JigsawJoint.ROLLABLE);
}
private static IrisJigsawPiece piece(String object, boolean rotatable, IrisJigsawConnector... connectors) {
IrisJigsawPiece piece = new IrisJigsawPiece().setObject(object).setRotatable(rotatable);
piece.getConnectors().add(connectors);
return piece;
}
private static IrisJigsawPool pool(String fallback, String... pieces) {
IrisJigsawPool pool = new IrisJigsawPool().setFallback(fallback);
for (String piece : pieces) {
pool.getPieces().add(new IrisJigsawPieceEntry().setPiece(piece).setWeight(1));
}
return pool;
}
private static IrisStructure structure(String startPool, int maxDepth) {
return new IrisStructure().setStartPool(startPool).setMaxDepth(maxDepth).setMaxSizeChunks(8);
}
private static RNG rotationIndex(int index) {
return new RNG(67L + index) {
@Override
public int i(int upperBound) {
return upperBound == 4 ? index : 0;
}
};
}
private static PlacedStructurePiece bounds(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return new PlacedStructurePiece(null, null, 0, 0, 0, null, minX, minY, minZ, maxX, maxY, maxZ);
}
private static void assertBounds(PlacedStructurePiece piece, int minX, int minY, int minZ,
int maxX, int maxY, int maxZ) {
assertEquals(minX, piece.getMinX());
assertEquals(minY, piece.getMinY());
assertEquals(minZ, piece.getMinZ());
assertEquals(maxX, piece.getMaxX());
assertEquals(maxY, piece.getMaxY());
assertEquals(maxZ, piece.getMaxZ());
}
private static final class Fixture {
private final IrisData data = mock(IrisData.class);
private Fixture() {
when(data.load(IrisJigsawPool.class, "terminal", false)).thenReturn(new IrisJigsawPool());
}
private void pool(String key, IrisJigsawPool pool) {
when(data.load(IrisJigsawPool.class, key, false)).thenReturn(pool);
}
private void piece(String key, IrisJigsawPiece piece) {
when(data.load(IrisJigsawPiece.class, key, false)).thenReturn(piece);
}
private void object(String key, IrisObject object) {
when(data.load(IrisObject.class, key, false)).thenReturn(object);
}
private StructureAssembler assembler(IrisStructure structure, int x, int y, int z) {
return StructureAssembler.forData(data, structure, new IrisPosition(x, y, z));
}
}
}
@@ -107,29 +107,32 @@ public class StructurePlacementGridTest {
}
@Test
public void densityStartIsDeterministicAndOrdinalScoped() {
public void densityStartIsDeterministicAndPlacementIdScoped() {
IrisStructurePlacement placement = placement(StructureDistribution.DENSITY);
placement.setDensity(0.35);
boolean differsByOrdinal = false;
IrisStructurePlacement secondPlacement = placement(StructureDistribution.DENSITY);
secondPlacement.setDensity(0.35);
secondPlacement.setPlacementId("second");
boolean differsById = false;
for (int cx = -32; cx <= 32; cx++) {
for (int cz = -32; cz <= 32; cz++) {
boolean first = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 0);
boolean repeated = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 0);
boolean secondPlacement = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L, 1);
boolean first = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L);
boolean repeated = StructurePlacementGrid.startsInChunk(placement, cx, cz, 912345L);
boolean second = StructurePlacementGrid.startsInChunk(secondPlacement, cx, cz, 912345L);
assertEquals(first, repeated);
differsByOrdinal |= first != secondPlacement;
differsById |= first != second;
}
}
assertTrue(differsByOrdinal);
assertTrue(differsById);
}
@Test
public void densityBoundariesNeverAndAlwaysStart() {
IrisStructurePlacement placement = placement(StructureDistribution.DENSITY);
placement.setDensity(0.0);
assertFalse(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L, 0));
assertFalse(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L));
placement.setDensity(1.0);
assertTrue(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L, 0));
assertTrue(StructurePlacementGrid.startsInChunk(placement, 4, -9, 77L));
}
@Test
@@ -144,7 +147,7 @@ public class StructurePlacementGridTest {
int starts = 0;
for (int cx = -scanRadius; cx <= scanRadius; cx++) {
for (int cz = -scanRadius; cz <= scanRadius; cz++) {
if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, 0)) {
if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed)) {
starts++;
}
}
@@ -154,15 +157,100 @@ public class StructurePlacementGridTest {
}
@Test
public void placementRngIsStableAndOrdinalScoped() {
public void placementRngIsStableAndPlacementIdScoped() {
IrisStructurePlacement placement = placement(StructureDistribution.RANDOM_SPREAD);
long first = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 0).getSeed();
long repeated = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 0).getSeed();
long secondPlacement = StructurePlacementGrid.placementRng(placement, 7, -11, 123L, 1).getSeed();
long first = StructurePlacementGrid.placementRng(placement, 7, -11, 123L).getSeed();
long repeated = StructurePlacementGrid.placementRng(placement, 7, -11, 123L).getSeed();
placement.setPlacementId("second");
long secondPlacement = StructurePlacementGrid.placementRng(placement, 7, -11, 123L).getSeed();
assertEquals(first, repeated);
assertNotEquals(first, secondPlacement);
}
@Test
public void authoredPlacementIdSeparatesRandomSpreadStarts() {
IrisStructurePlacement first = placement(StructureDistribution.RANDOM_SPREAD);
first.setPlacementId("first");
IrisStructurePlacement second = placement(StructureDistribution.RANDOM_SPREAD);
second.setPlacementId("second");
int[] firstStart = StructurePlacementGrid.randomSpreadCellChunk(
8, -3, first.getSpacing(), first.getSeparation(), StructurePlacementGrid.placementSalt(first), 31337L);
int[] secondStart = StructurePlacementGrid.randomSpreadCellChunk(
8, -3, second.getSpacing(), second.getSeparation(), StructurePlacementGrid.placementSalt(second), 31337L);
assertFalse(firstStart[0] == secondStart[0] && firstStart[1] == secondStart[1]);
}
@Test
public void blankIdsDeriveDistinctStableRandomSpreadGridsFromContent() {
IrisStructurePlacement first = placement(StructureDistribution.RANDOM_SPREAD);
IrisStructurePlacement recreatedFirst = placement(StructureDistribution.RANDOM_SPREAD);
IrisStructurePlacement second = placement(StructureDistribution.RANDOM_SPREAD);
second.getStructures().clear();
second.getStructures().add("test:other");
assertEquals(StructurePlacementGrid.placementSalt(first),
StructurePlacementGrid.placementSalt(recreatedFirst));
assertNotEquals(StructurePlacementGrid.placementSalt(first),
StructurePlacementGrid.placementSalt(second));
boolean differs = false;
for (int cell = -8; cell <= 8; cell++) {
int[] firstStart = StructurePlacementGrid.randomSpreadCellChunk(
cell, cell + 3, first.getSpacing(), first.getSeparation(),
StructurePlacementGrid.placementSalt(first), 31337L);
int[] secondStart = StructurePlacementGrid.randomSpreadCellChunk(
cell, cell + 3, second.getSpacing(), second.getSeparation(),
StructurePlacementGrid.placementSalt(second), 31337L);
differs |= firstStart[0] != secondStart[0] || firstStart[1] != secondStart[1];
}
assertTrue(differs);
}
@Test
public void blankIdsDeriveDistinctStableConcentricRingsFromContent() {
IrisStructurePlacement first = placement(StructureDistribution.CONCENTRIC_RINGS);
IrisStructurePlacement recreatedFirst = placement(StructureDistribution.CONCENTRIC_RINGS);
IrisStructurePlacement second = placement(StructureDistribution.CONCENTRIC_RINGS);
second.getStructures().clear();
second.getStructures().add("test:other");
first.setRingCount(12);
recreatedFirst.setRingCount(12);
second.setRingCount(12);
boolean differs = false;
for (int index = 0; index < first.getRingCount(); index++) {
int[] firstPoint = StructurePlacementGrid.concentricRingChunk(first, index, 77123L);
int[] recreatedPoint = StructurePlacementGrid.concentricRingChunk(recreatedFirst, index, 77123L);
int[] secondPoint = StructurePlacementGrid.concentricRingChunk(second, index, 77123L);
assertEquals(firstPoint[0], recreatedPoint[0]);
assertEquals(firstPoint[1], recreatedPoint[1]);
differs |= firstPoint[0] != secondPoint[0] || firstPoint[1] != secondPoint[1];
}
assertTrue(differs);
}
@Test
public void derivedPlacementIdentityIncludesPlacementSettings() {
IrisStructurePlacement first = placement(StructureDistribution.DENSITY);
IrisStructurePlacement second = placement(StructureDistribution.DENSITY);
second.setMinHeight(first.getMinHeight() + 1);
long firstSeed = StructurePlacementGrid.placementRng(first, 9, 14, 42L).getSeed();
long secondSeed = StructurePlacementGrid.placementRng(second, 9, 14, 42L).getSeed();
assertNotEquals(firstSeed, secondSeed);
}
@Test
public void explicitPlacementIdentityKeepsRngStableAcrossSettingEdits() {
IrisStructurePlacement first = placement(StructureDistribution.DENSITY);
first.setPlacementId("stable");
IrisStructurePlacement second = placement(StructureDistribution.DENSITY);
second.setPlacementId("stable");
second.setMinHeight(first.getMinHeight() + 1);
second.setDensity(0.75);
long firstSeed = StructurePlacementGrid.placementRng(first, 9, 14, 42L).getSeed();
long secondSeed = StructurePlacementGrid.placementRng(second, 9, 14, 42L).getSeed();
assertEquals(firstSeed, secondSeed);
}
private IrisStructurePlacement placement(StructureDistribution distribution) {
IrisStructurePlacement placement = new IrisStructurePlacement();
placement.setDistribution(distribution);
@@ -18,57 +18,73 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import java.util.List;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class StructureReachabilityContractTest {
@Test
public void reachableKeysIsEmptyForNullEngine() {
assertTrue(StructureReachability.reachableKeys(null).isEmpty());
public void reachableKeysRejectsNullEngine() {
assertThrows(NullPointerException.class, () -> StructureReachability.reachableKeys(null));
}
@Test
public void reachableKeysIsEmptyWhenEngineHasNoData() {
public void reachableKeysRejectsEngineWithoutData() {
Engine engine = mock(Engine.class);
assertTrue(StructureReachability.reachableKeys(engine).isEmpty());
assertThrows(IllegalStateException.class, () -> StructureReachability.reachableKeys(engine));
}
@Test
public void isReachableIsFalseForNullEngine() {
assertFalse(StructureReachability.isReachable(null, "minecraft:village_taiga"));
public void isReachableRejectsNullEngine() {
assertThrows(NullPointerException.class,
() -> StructureReachability.isReachable(null, "minecraft:village_taiga"));
}
@Test
public void isReachableIsFalseForNullOrEmptyKey() {
public void isReachableRejectsNullOrEmptyKey() {
Engine engine = mock(Engine.class);
assertFalse(StructureReachability.isReachable(engine, null));
assertFalse(StructureReachability.isReachable(engine, ""));
assertThrows(IllegalArgumentException.class, () -> StructureReachability.isReachable(engine, null));
assertThrows(IllegalArgumentException.class, () -> StructureReachability.isReachable(engine, ""));
}
@Test
public void isReachableIsFalseWhenNothingIsReachable() {
public void isReachableRejectsEngineWithoutData() {
Engine engine = mock(Engine.class);
assertFalse(StructureReachability.isReachable(engine, "minecraft:village_taiga"));
assertThrows(IllegalStateException.class,
() -> StructureReachability.isReachable(engine, "minecraft:village_taiga"));
}
@Test
public void missingBiomeKeysIsEmptyForNullEngine() {
assertTrue(StructureReachability.missingBiomeKeys(null, "minecraft:village_taiga").isEmpty());
public void missingBiomeKeysRejectsNullEngine() {
assertThrows(NullPointerException.class,
() -> StructureReachability.missingBiomeKeys(null, "minecraft:village_taiga"));
}
@Test
public void missingBiomeKeysIsEmptyForNullKey() {
public void missingBiomeKeysRejectsNullKey() {
Engine engine = mock(Engine.class);
assertTrue(StructureReachability.missingBiomeKeys(engine, null).isEmpty());
assertThrows(IllegalArgumentException.class,
() -> StructureReachability.missingBiomeKeys(engine, null));
}
@Test
public void missingBiomeKeysIsEmptyWhenWorldUnavailable() {
public void missingBiomeKeysRejectsUnavailableWorld() {
Engine engine = mock(Engine.class);
assertTrue(StructureReachability.missingBiomeKeys(engine, "minecraft:village_taiga").isEmpty());
when(engine.getData()).thenReturn(mock(IrisData.class));
assertThrows(IllegalStateException.class,
() -> StructureReachability.missingBiomeKeys(engine, "minecraft:village_taiga"));
}
@Test
@@ -76,4 +92,55 @@ public class StructureReachabilityContractTest {
StructureReachability.invalidate(null);
StructureReachability.invalidate(mock(Engine.class));
}
@Test
public void failedReachabilityBuildIsNotCachedAsEmpty() {
Engine engine = mock(Engine.class);
IrisWorld world = mock(IrisWorld.class);
PlatformWorld platformWorld = mock(PlatformWorld.class);
IrisPlatform platform = mock(IrisPlatform.class);
PlatformStructureHooks hooks = mock(PlatformStructureHooks.class);
IllegalStateException cause = new IllegalStateException("registry unavailable");
when(engine.getData()).thenReturn(mock(IrisData.class));
when(engine.getWorld()).thenReturn(world);
when(world.platformWorld()).thenReturn(platformWorld);
when(platform.structureHooks()).thenReturn(hooks);
when(hooks.reachableStructureKeys(platformWorld))
.thenThrow(cause)
.thenReturn(List.of("minecraft:monument"));
IrisPlatforms.unbind();
IrisPlatforms.bind(platform);
try {
assertThrows(IllegalStateException.class, () -> StructureReachability.reachableKeys(engine));
assertTrue(StructureReachability.reachableKeys(engine).contains("minecraft:monument"));
} finally {
StructureReachability.invalidate(engine);
IrisPlatforms.unbind();
}
}
@Test
public void missingBiomeKeysForwardsCanonicalStructureKey() {
Engine engine = mock(Engine.class);
IrisWorld world = mock(IrisWorld.class);
PlatformWorld platformWorld = mock(PlatformWorld.class);
IrisPlatform platform = mock(IrisPlatform.class);
PlatformStructureHooks hooks = mock(PlatformStructureHooks.class);
when(engine.getWorld()).thenReturn(world);
when(world.platformWorld()).thenReturn(platformWorld);
when(platform.structureHooks()).thenReturn(hooks);
when(hooks.possibleBiomeKeys(platformWorld)).thenReturn(List.of("minecraft:deep_ocean"));
when(hooks.structureBiomeKeys("minecraft:monument")).thenReturn(List.of("minecraft:deep_ocean"));
IrisPlatforms.unbind();
IrisPlatforms.bind(platform);
try {
assertTrue(StructureReachability.missingBiomeKeys(
engine, " MINECRAFT:MONUMENT ").isEmpty());
verify(hooks).structureBiomeKeys("minecraft:monument");
} finally {
IrisPlatforms.unbind();
}
}
}
@@ -0,0 +1,196 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.engine.object.IrisObject;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisObjectFrameReaderTest {
@Test
public void acceptsModernTileFrame() throws Exception {
IrisObject object = readObject(tile -> {
tile.writeUTF("minecraft:chest");
tile.writeUTF("{}");
});
assertEquals(1, object.getW());
assertEquals(1, object.getH());
assertEquals(1, object.getD());
}
@Test
public void acceptsEveryLegacyTileFrame() throws Exception {
readObject(tile -> {
tile.writeShort(0);
tile.writeUTF("one");
tile.writeUTF("two");
tile.writeUTF("three");
tile.writeUTF("four");
tile.writeByte(0);
});
readObject(tile -> {
tile.writeShort(1);
tile.writeUTF("minecraft:pig");
});
readObject(tile -> {
tile.writeShort(1);
tile.writeShort(0);
});
readObject(tile -> {
tile.writeShort(2);
tile.writeByte(0);
tile.writeByte(1);
tile.writeByte(0);
tile.writeUTF("minecraft:base");
});
readObject(tile -> {
tile.writeShort(2);
tile.writeByte(0);
tile.writeByte(1);
tile.writeByte(0);
tile.writeByte(0);
});
readObject(tile -> {
tile.writeShort(3);
tile.writeUTF("minecraft:chest");
tile.writeUTF("minecraft:chests/simple_dungeon");
tile.writeLong(42L);
});
}
@Test
public void acceptsLegacySignColorBoundary() throws Exception {
readObject(tile -> writeLegacySign(tile, 15));
}
@Test
public void rejectsBlankModernTileMaterialWithResourceContext() throws Exception {
byte[] content = objectBytes(tile -> {
tile.writeUTF("");
tile.writeUTF("{}");
});
IOException exception = assertThrows(IOException.class, () -> IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(content), "objects/blank-material.iob"));
assertEquals("Malformed Iris object resource objects/blank-material.iob: "
+ "tile 0 modern material is blank", exception.getMessage());
}
@Test
public void rejectsLegacySignColorsOutsideRuntimeRange() throws Exception {
IOException negative = readFailure(tile -> writeLegacySign(tile, -1), "objects/sign-negative.iob");
IOException overflow = readFailure(tile -> writeLegacySign(tile, 16), "objects/sign-overflow.iob");
assertTrue(negative.getMessage(), negative.getMessage().endsWith(
"tile 0 legacy sign color index -1 is outside 0..15"));
assertTrue(overflow.getMessage(), overflow.getMessage().endsWith(
"tile 0 legacy sign color index 16 is outside 0..15"));
}
@Test
public void rejectsTileCountsAboveValidationLimit() throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeInt(257);
output.writeInt(257);
output.writeInt(1);
output.writeUTF("Iris V2 IOB;");
output.writeShort(0);
output.writeInt(0);
output.writeInt(65_537);
}
IOException exception = assertThrows(IOException.class, () -> IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(bytes.toByteArray()), "objects/too-many-tiles.iob"));
assertTrue(exception.getMessage(), exception.getMessage().endsWith(
"tile count 65537 exceeds limit 65536"));
}
@Test
public void boundsObjectInputBeforeUnboundedAllocation() throws Exception {
byte[] content = new byte[33];
IOException exception = assertThrows(IOException.class, () -> IrisObjectFrameReader.readLimitedContent(
new ByteArrayInputStream(content), "objects/oversized.iob", 32));
assertEquals("Malformed Iris object resource objects/oversized.iob: "
+ "file exceeds 32-byte limit", exception.getMessage());
}
@Test
public void rejectsTruncatedTileFrame() throws Exception {
byte[] content = objectBytes(tile -> tile.writeUTF("minecraft:chest"));
assertThrows(IOException.class, () -> IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(content), "objects/truncated.iob"));
}
@Test
public void rejectsTrailingBytes() throws Exception {
byte[] content = objectBytes(tile -> {
tile.writeUTF("minecraft:chest");
tile.writeUTF("{}");
tile.writeByte(1);
});
assertThrows(IOException.class, () -> IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(content), "objects/trailing.iob"));
}
private IrisObject readObject(TileWriter tileWriter) throws Exception {
return IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(objectBytes(tileWriter)), "objects/test.iob");
}
private IOException readFailure(TileWriter tileWriter, String resourceName) throws Exception {
byte[] content = objectBytes(tileWriter);
return assertThrows(IOException.class, () -> IrisObjectFrameReader.readBounds(
new ByteArrayInputStream(content), resourceName));
}
private void writeLegacySign(DataOutputStream tile, int colorIndex) throws IOException {
tile.writeShort(0);
tile.writeUTF("one");
tile.writeUTF("two");
tile.writeUTF("three");
tile.writeUTF("four");
tile.writeByte(colorIndex);
}
private byte[] objectBytes(TileWriter tileWriter) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
output.writeUTF("Iris V2 IOB;");
output.writeShort(1);
output.writeUTF("minecraft:chest");
output.writeInt(1);
output.writeShort(0);
output.writeShort(0);
output.writeShort(0);
output.writeShort(0);
output.writeInt(1);
output.writeShort(0);
output.writeShort(0);
output.writeShort(0);
tileWriter.write(output);
}
return bytes.toByteArray();
}
@FunctionalInterface
private interface TileWriter {
void write(DataOutputStream output) throws IOException;
}
}
@@ -0,0 +1,576 @@
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.engine.object.IrisDirection;
import art.arcane.iris.engine.object.IrisJigsawConnector;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
import art.arcane.iris.engine.object.IrisJigsawPool;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.JigsawJoint;
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.volmlib.util.collection.KList;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.LinkedHashMap;
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.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureGraphCompilerTest {
@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 compilesACompleteTerminalGraphAndSamplesDeterministically() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/start", object());
resolver.pieces.put("pieces/start", piece("objects/start"));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
IrisStructure structure = structure("pools/start");
StructureGraphCompilation first = StructureGraphCompiler.compile(structure, resolver);
StructureGraphCompilation second = StructureGraphCompiler.compile(structure, resolver);
assertTrue(first.getDiagnostics().isEmpty());
assertTrue(first.isAssemblyViable());
assertEquals(List.of("pools/start"), new ArrayList<>(first.getGraph().getPools().keySet()));
assertEquals(List.of("pieces/start"), new ArrayList<>(first.getGraph().getPieces().keySet()));
assertEquals(List.of("objects/start"), new ArrayList<>(first.getGraph().getObjects().keySet()));
assertEquals(first.getAssemblySamples(), second.getAssemblySamples());
for (StructureGraphAssemblySample sample : first.getAssemblySamples()) {
assertEquals(List.of("pieces/start"), sample.outcome().pieceKeys());
assertTrue(sample.outcome().isComplete());
}
}
@Test
public void reportsMissingReferencesInvalidWeightsAndNoViableStart() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawPool start = pool(entry("pieces/missing", 0));
start.setFallback("pools/missing-fallback");
resolver.pools.put("pools/start", start);
StructureGraphCompilation result = StructureGraphCompiler.compile(structure("pools/start"), resolver);
assertCodes(result,
StructureGraphDiagnostic.Code.INVALID_WEIGHT,
StructureGraphDiagnostic.Code.MISSING_PIECE,
StructureGraphDiagnostic.Code.MISSING_POOL,
StructureGraphDiagnostic.Code.NO_VIABLE_START_PIECE);
assertTrue(result.hasErrors());
assertFalse(result.isAssemblyViable());
}
@Test
public void reportsEmptyPoolsAndConnectorFieldFailures() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector connector = connector(new IrisPosition(4, 1, 1), null, "", "path", "path");
resolver.objects.put("objects/start", object());
resolver.pieces.put("pieces/start", piece("objects/start", connector));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
resolver.pools.put("pools/empty", pool());
StructureGraphCompilation invalidConnector = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
StructureGraphCompilation emptyStart = StructureGraphCompiler.compile(
structure("pools/empty"), resolver);
assertCodes(invalidConnector,
StructureGraphDiagnostic.Code.INVALID_CONNECTOR_POSITION,
StructureGraphDiagnostic.Code.INVALID_CONNECTOR_DIRECTION,
StructureGraphDiagnostic.Code.MISSING_CONNECTOR_POOL);
assertCodes(emptyStart,
StructureGraphDiagnostic.Code.EMPTY_START_POOL,
StructureGraphDiagnostic.Code.NO_VIABLE_START_PIECE);
}
@Test
public void reportsIncompatibleDirectionsAndUnreachablePieces() {
InMemoryResolver resolver = connectorGraph(false, IrisDirection.NORTH_NEGATIVE_Z);
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertCodes(result,
StructureGraphDiagnostic.Code.NO_COMPATIBLE_CONNECTOR,
StructureGraphDiagnostic.Code.UNREACHABLE_PIECE);
assertFalse(result.getGraph().getReachablePieces().contains("pieces/target"));
assertFalse(result.isAssemblyViable());
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertFalse(sample.outcome().isComplete());
}
}
@Test
public void acceptsHorizontalRotationWithVanillaOneWayTargetMatching() {
InMemoryResolver resolver = connectorGraph(true, IrisDirection.NORTH_NEGATIVE_Z);
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(result.getDiagnostics().isEmpty());
assertTrue(result.getGraph().getReachablePieces().contains("pieces/target"));
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertEquals(List.of("pieces/start", "pieces/target"), sample.outcome().pieceKeys());
assertTrue(sample.outcome().isComplete());
}
}
@Test
public void rejectsVerticalConnectorsThatYRotationCannotAlign() {
InMemoryResolver resolver = connectorGraph(true, IrisDirection.UP_POSITIVE_Y);
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertCodes(result,
StructureGraphDiagnostic.Code.NO_COMPATIBLE_CONNECTOR,
StructureGraphDiagnostic.Code.UNREACHABLE_PIECE);
}
@Test
public void alignedConnectorsRequireMatchingTopOrientation() {
InMemoryResolver resolver = connectorGraph(false, IrisDirection.SOUTH_POSITIVE_Z);
IrisJigsawConnector source = resolver.pieces.get("pieces/start").getConnectors().getFirst();
IrisJigsawConnector target = resolver.pieces.get("pieces/target").getConnectors().getFirst();
source.setJoint(JigsawJoint.ALIGNED);
source.setTop(IrisDirection.UP_POSITIVE_Y);
target.setTop(IrisDirection.DOWN_NEGATIVE_Y);
StructureGraphCompilation aligned = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertCodes(aligned,
StructureGraphDiagnostic.Code.NO_COMPATIBLE_CONNECTOR,
StructureGraphDiagnostic.Code.UNREACHABLE_PIECE);
source.setJoint(JigsawJoint.ROLLABLE);
StructureGraphCompilation rollable = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(rollable.isAssemblyViable());
}
@Test
public void deterministicSamplingUsesFallbackAtTheMaximumDepth() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector source = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/primary", "source", "door");
IrisJigsawConnector primaryTarget = connector(
new IrisPosition(1, 1, 2), IrisDirection.SOUTH_POSITIVE_Z,
"pools/primary", "door", "unused");
IrisJigsawConnector primarySource = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/primary", "source", "door");
IrisJigsawConnector fallbackTarget = connector(
new IrisPosition(1, 1, 2), IrisDirection.SOUTH_POSITIVE_Z,
"pools/primary", "door", "unused");
resolver.objects.put("objects/start", object());
resolver.objects.put("objects/primary", object());
resolver.objects.put("objects/fallback", object());
resolver.pieces.put("pieces/start", piece("objects/start", source));
resolver.pieces.put("pieces/primary", piece("objects/primary", primaryTarget, primarySource));
resolver.pieces.put("pieces/fallback", piece("objects/fallback", fallbackTarget));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
IrisJigsawPool primary = pool(entry("pieces/primary", 1));
primary.setFallback("pools/fallback");
resolver.pools.put("pools/primary", primary);
resolver.pools.put("pools/fallback", pool(entry("pieces/fallback", 1)));
IrisStructure structure = structure("pools/start");
structure.setMaxDepth(1);
StructureGraphCompilation result = StructureGraphCompiler.compile(structure, resolver);
assertTrue(result.getDiagnostics().isEmpty());
assertTrue(result.isAssemblyViable());
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertEquals(List.of("pieces/start", "pieces/primary", "pieces/fallback"),
sample.outcome().pieceKeys());
assertTrue(sample.outcome().isComplete());
}
}
@Test
public void deterministicSamplingDoesNotSelectFallbackOfFallback() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector source = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/primary", "source", "door");
IrisJigsawConnector incompatible = connector(
new IrisPosition(1, 1, 2), IrisDirection.SOUTH_POSITIVE_Z,
"pools/primary", "wrong", "unused");
IrisJigsawConnector nestedTarget = connector(
new IrisPosition(1, 1, 2), IrisDirection.SOUTH_POSITIVE_Z,
"pools/primary", "door", "unused");
resolver.objects.put("objects/start", object());
resolver.objects.put("objects/incompatible", object());
resolver.objects.put("objects/nested", object());
resolver.pieces.put("pieces/start", piece("objects/start", source));
resolver.pieces.put("pieces/incompatible", piece("objects/incompatible", incompatible));
resolver.pieces.put("pieces/nested", piece("objects/nested", nestedTarget));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
IrisJigsawPool primary = pool(entry("pieces/incompatible", 1));
primary.setFallback("pools/direct");
resolver.pools.put("pools/primary", primary);
IrisJigsawPool direct = pool(entry("pieces/incompatible", 1));
direct.setFallback("pools/nested");
resolver.pools.put("pools/direct", direct);
resolver.pools.put("pools/nested", pool(entry("pieces/nested", 1)));
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertFalse(result.getGraph().getReachablePools().contains("pools/nested"));
assertFalse(result.getGraph().getReachablePieces().contains("pieces/nested"));
assertFalse(result.isAssemblyViable());
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertEquals(List.of("pieces/start"), sample.outcome().pieceKeys());
assertEquals(1, sample.outcome().unresolvedConnectorCount());
}
}
@Test
public void reportsFallbackCyclesAndTheirUnreachableClosure() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/terminal", object());
resolver.pieces.put("pieces/terminal", piece("objects/terminal"));
IrisJigsawPool start = pool(entry("pieces/terminal", 1));
IrisJigsawPool first = pool(entry("pieces/terminal", 1));
IrisJigsawPool second = pool(entry("pieces/terminal", 1));
start.setFallback("pools/first");
first.setFallback("pools/second");
second.setFallback("pools/first");
resolver.pools.put("pools/start", start);
resolver.pools.put("pools/first", first);
resolver.pools.put("pools/second", second);
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertCodes(result,
StructureGraphDiagnostic.Code.FALLBACK_CYCLE,
StructureGraphDiagnostic.Code.UNREACHABLE_POOL);
assertTrue(result.hasErrors());
}
@Test
public void validatesLongFallbackChainsWithoutRecursiveTraversal() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/terminal", object());
resolver.pieces.put("pieces/terminal", piece("objects/terminal"));
int poolCount = 10_000;
for (int index = 0; index < poolCount; index++) {
IrisJigsawPool pool = pool(entry("pieces/terminal", 1));
if (index + 1 < poolCount) {
pool.setFallback("pools/chain-" + (index + 1));
}
resolver.pools.put("pools/chain-" + index, pool);
}
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/chain-0"), resolver);
assertTrue(result.getDiagnostics().stream().noneMatch(
diagnostic -> diagnostic.code() == StructureGraphDiagnostic.Code.FALLBACK_CYCLE));
assertTrue(result.isAssemblyViable());
}
@Test
public void reportsMissingObjectsAndInvalidObjectBounds() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/empty", new IrisObject());
resolver.pieces.put("pieces/missing-object", piece("objects/missing"));
resolver.pieces.put("pieces/empty-object", piece("objects/empty"));
resolver.pools.put("pools/start", pool(
entry("pieces/missing-object", 1),
entry("pieces/empty-object", 1)));
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertCodes(result,
StructureGraphDiagnostic.Code.MISSING_OBJECT,
StructureGraphDiagnostic.Code.INVALID_OBJECT_BOUNDS);
assertTrue(result.hasErrors());
}
@Test
public void rejectsNullConnectorCollectionsAndNames() {
InMemoryResolver nullListResolver = new InMemoryResolver();
nullListResolver.objects.put("objects/start", object());
IrisJigsawPiece nullListPiece = piece("objects/start");
nullListPiece.setConnectors(null);
nullListResolver.pieces.put("pieces/start", nullListPiece);
nullListResolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
StructureGraphCompilation nullList = StructureGraphCompiler.compile(
structure("pools/start"), nullListResolver);
assertCodes(nullList, StructureGraphDiagnostic.Code.INVALID_CONNECTOR);
assertTrue(nullList.hasErrors());
InMemoryResolver nullNamesResolver = connectorGraph(true, IrisDirection.SOUTH_POSITIVE_Z);
nullNamesResolver.pieces.get("pieces/start").getConnectors().getFirst().setTargetName(null);
StructureGraphCompilation nullNames = StructureGraphCompiler.compile(
structure("pools/start"), nullNamesResolver);
assertCodes(nullNames, StructureGraphDiagnostic.Code.INVALID_CONNECTOR);
assertTrue(nullNames.hasErrors());
}
@Test
public void rejectsStructureLimitsOutsideTheRuntimeContract() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/start", object());
resolver.pieces.put("pieces/start", piece("objects/start"));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
IrisStructure deep = structure("pools/start");
deep.setMaxDepth(31);
IrisStructure wide = structure("pools/start");
wide.setMaxSizeChunks(33);
IrisStructure overflowing = structure("pools/start");
overflowing.setMaxSizeChunks(Integer.MAX_VALUE);
assertCodes(StructureGraphCompiler.compile(deep, resolver),
StructureGraphDiagnostic.Code.INVALID_MAX_DEPTH);
assertCodes(StructureGraphCompiler.compile(wide, resolver),
StructureGraphDiagnostic.Code.INVALID_MAX_SIZE);
assertCodes(StructureGraphCompiler.compile(overflowing, resolver),
StructureGraphDiagnostic.Code.INVALID_MAX_SIZE);
}
@Test
public void emptyPoolChoiceTerminatesABranchWithoutAPlaceholderPiece() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector source = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/terminal", "source", "door");
resolver.objects.put("objects/start", object());
resolver.pieces.put("pieces/start", piece("objects/start", source));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
resolver.pools.put("pools/terminal", pool(emptyEntry(4)));
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(result.getDiagnostics().isEmpty());
assertTrue(result.isAssemblyViable());
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertEquals(List.of("pieces/start"), sample.outcome().pieceKeys());
assertTrue(sample.outcome().isComplete());
}
}
@Test
public void rareEmptyStartPreventsGuaranteedReplacementOutputWithoutDependingOnSamples() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/start", object());
resolver.pieces.put("pieces/start", piece("objects/start"));
resolver.pools.put("pools/start", pool(
entry("pieces/start", Integer.MAX_VALUE), emptyEntry(1)));
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(result.isAssemblyViable());
assertFalse(result.guaranteesAssemblyOutput());
assertTrue(result.getAssemblySamples().stream().noneMatch(
sample -> sample.outcome().intentionalEmpty()));
}
@Test
public void oversizedStartPreventsGuaranteedReplacementOutput() {
InMemoryResolver resolver = new InMemoryResolver();
resolver.objects.put("objects/start", new IrisObject(130, 3, 3));
resolver.pieces.put("pieces/start", piece("objects/start"));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
StructureGraphCompilation result = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(result.isAssemblyViable());
assertFalse(result.guaranteesAssemblyOutput());
}
@Test
public void reachableBranchRequiresAuthoredEmptyTerminationForReplacementOutput() {
InMemoryResolver resolver = connectorGraph(true, IrisDirection.NORTH_NEGATIVE_Z);
StructureGraphCompilation unbounded = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(unbounded.isAssemblyViable());
assertFalse(unbounded.guaranteesAssemblyOutput());
resolver.pools.get("pools/target").setFallback("pools/empty");
resolver.pools.put("pools/empty", pool());
StructureGraphCompilation terminated = StructureGraphCompiler.compile(
structure("pools/start"), resolver);
assertTrue(terminated.isAssemblyViable());
assertTrue(terminated.guaranteesAssemblyOutput());
}
@Test
public void defersTopologyPieceCapsToGeometryValidation() {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector startSource = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/recursive", "source", "door");
IrisJigsawConnector recursiveTarget = connector(
new IrisPosition(1, 1, 2), IrisDirection.SOUTH_POSITIVE_Z,
"pools/recursive", "door", "unused");
IrisJigsawConnector firstBranch = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/recursive", "source", "door");
IrisJigsawConnector secondBranch = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/recursive", "source", "door");
resolver.objects.put("objects/start", object());
resolver.objects.put("objects/recursive", object());
resolver.pieces.put("pieces/start", piece("objects/start", startSource));
resolver.pieces.put("pieces/recursive", piece(
"objects/recursive", recursiveTarget, firstBranch, secondBranch));
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
IrisJigsawPool recursive = pool(entry("pieces/recursive", 1));
recursive.setFallback("pools/empty");
resolver.pools.put("pools/recursive", recursive);
resolver.pools.put("pools/empty", pool());
IrisStructure structure = structure("pools/start");
structure.setMaxDepth(30);
StructureGraphCompilation result = StructureGraphCompiler.compile(structure, resolver);
assertCodes(result, StructureGraphDiagnostic.Code.ASSEMBLY_PIECE_CAP_REACHED);
assertTrue(result.isAssemblyViable());
for (StructureGraphAssemblySample sample : result.getAssemblySamples()) {
assertTrue(sample.outcome().pieceCapReached());
assertEquals(0, sample.outcome().unresolvedConnectorCount());
}
}
private static InMemoryResolver connectorGraph(boolean rotatable, IrisDirection targetDirection) {
InMemoryResolver resolver = new InMemoryResolver();
IrisJigsawConnector source = connector(
new IrisPosition(1, 1, 0), IrisDirection.NORTH_NEGATIVE_Z,
"pools/target", "source", "door");
IrisJigsawConnector target = connector(
new IrisPosition(1, 1, 0), targetDirection,
"pools/target", "door", "unrelated-target");
IrisJigsawPiece targetPiece = piece("objects/target", target);
targetPiece.setRotatable(rotatable);
resolver.objects.put("objects/start", object());
resolver.objects.put("objects/target", object());
IrisJigsawPiece startPiece = piece("objects/start", source);
startPiece.setRotatable(false);
resolver.pieces.put("pieces/start", startPiece);
resolver.pieces.put("pieces/target", targetPiece);
resolver.pools.put("pools/start", pool(entry("pieces/start", 1)));
resolver.pools.put("pools/target", pool(entry("pieces/target", 1)));
return resolver;
}
private static IrisStructure structure(String startPool) {
IrisStructure structure = new IrisStructure();
structure.setLoadKey("structures/test");
structure.setStartPool(startPool);
structure.setMaxDepth(3);
structure.setMaxSizeChunks(4);
return structure;
}
private static IrisObject object() {
return new IrisObject(3, 3, 3);
}
private static IrisJigsawPiece piece(String object, IrisJigsawConnector... connectors) {
IrisJigsawPiece piece = new IrisJigsawPiece();
piece.setObject(object);
piece.setConnectors(new KList<>(connectors));
return piece;
}
private static IrisJigsawPool pool(IrisJigsawPieceEntry... entries) {
IrisJigsawPool pool = new IrisJigsawPool();
pool.setPieces(new KList<>(entries));
return pool;
}
private static IrisJigsawPieceEntry entry(String piece, int weight) {
return new IrisJigsawPieceEntry().setPiece(piece).setWeight(weight);
}
private static IrisJigsawPieceEntry emptyEntry(int weight) {
return new IrisJigsawPieceEntry().setEmpty(true).setWeight(weight);
}
private static IrisJigsawConnector connector(IrisPosition position, IrisDirection direction, String pool,
String name, String targetName) {
return new IrisJigsawConnector()
.setPosition(position)
.setDirection(direction)
.setPool(pool)
.setName(name)
.setTargetName(targetName);
}
private static void assertCodes(StructureGraphCompilation result, StructureGraphDiagnostic.Code... expected) {
List<StructureGraphDiagnostic.Code> actual = new ArrayList<>();
for (StructureGraphDiagnostic diagnostic : result.getDiagnostics()) {
if (!actual.contains(diagnostic.code())) {
actual.add(diagnostic.code());
}
}
for (StructureGraphDiagnostic.Code code : expected) {
assertTrue("Missing diagnostic " + code + " in " + actual, actual.contains(code));
}
}
private static final class InMemoryResolver implements StructureGraphResolver {
private final Map<String, IrisJigsawPool> pools = new LinkedHashMap<>();
private final Map<String, IrisJigsawPiece> pieces = new LinkedHashMap<>();
private final Map<String, IrisObject> objects = new LinkedHashMap<>();
@Override
public IrisJigsawPool loadPool(String key) {
return pools.get(key);
}
@Override
public IrisJigsawPiece loadPiece(String key) {
return pieces.get(key);
}
@Override
public IrisObject loadObject(String key) {
return objects.get(key);
}
}
}
@@ -0,0 +1,140 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.framework.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class StructureResourceBundleGraphCompilerTest {
@Test
public void completeBundleCompilesAndPassesRuntimeGeometry() throws IOException {
StructureResourceBundle bundle = baseBundle()
.resource("objects/start.iob", object(1, 1, 1))
.textResource("jigsaw-pieces/start.json",
"{\"object\":\"start\",\"connectors\":[],\"rotatable\":false}")
.textResource("jigsaw-pools/start.json",
"{\"pieces\":[{\"piece\":\"start\",\"weight\":1}]}")
.textResource("structures/test.json",
"{\"startPool\":\"start\",\"maxDepth\":1,\"maxSizeChunks\":1}")
.build();
StructureResourceBundleGraphCompiler.requireViable(bundle);
assertEquals(1, StructureResourceBundleGraphCompiler.compile(bundle).size());
}
@Test
public void topologicalGraphThatCannotFitItsRadiusIsRejectedBeforePublish() throws IOException {
String sourceConnector = "{\"position\":{\"x\":0,\"y\":0,\"z\":0},"
+ "\"direction\":\"EAST_POSITIVE_X\",\"top\":\"UP_POSITIVE_Y\","
+ "\"pool\":\"target\",\"name\":\"source\",\"targetName\":\"door\","
+ "\"joint\":\"ROLLABLE\"}";
String targetConnector = "{\"position\":{\"x\":50,\"y\":0,\"z\":0},"
+ "\"direction\":\"WEST_NEGATIVE_X\",\"top\":\"UP_POSITIVE_Y\","
+ "\"pool\":\"target\",\"name\":\"door\",\"targetName\":\"unused\","
+ "\"joint\":\"ROLLABLE\"}";
StructureResourceBundle bundle = baseBundle()
.resource("objects/start.iob", object(1, 1, 1))
.resource("objects/target.iob", object(100, 1, 1))
.textResource("jigsaw-pieces/start.json",
"{\"object\":\"start\",\"connectors\":[" + sourceConnector + "],\"rotatable\":false}")
.textResource("jigsaw-pieces/target.json",
"{\"object\":\"target\",\"connectors\":[" + targetConnector + "],\"rotatable\":false}")
.textResource("jigsaw-pools/start.json",
"{\"pieces\":[{\"piece\":\"start\",\"weight\":1}]}")
.textResource("jigsaw-pools/target.json",
"{\"pieces\":[{\"piece\":\"target\",\"weight\":1}]}")
.textResource("structures/test.json",
"{\"startPool\":\"start\",\"maxDepth\":1,\"maxSizeChunks\":1}")
.build();
assertThrows(IllegalArgumentException.class,
() -> StructureResourceBundleGraphCompiler.requireViable(bundle));
}
@Test
public void intentionalEmptyStartIsAValidNoOpGraph() {
StructureResourceBundle bundle = baseBundle()
.textResource("jigsaw-pools/start.json",
"{\"pieces\":[{\"empty\":true,\"weight\":1}]}")
.textResource("structures/test.json",
"{\"startPool\":\"start\",\"maxDepth\":1,\"maxSizeChunks\":1}")
.build();
StructureResourceBundleGraphCompiler.requireViable(bundle);
}
@Test
public void truncatedObjectFrameIsRejectedBeforePublish() throws IOException {
StructureResourceBundle bundle = baseBundle()
.resource("objects/start.iob", truncatedObject(1, 1, 1))
.textResource("jigsaw-pieces/start.json",
"{\"object\":\"start\",\"connectors\":[],\"rotatable\":false}")
.textResource("jigsaw-pools/start.json",
"{\"pieces\":[{\"piece\":\"start\",\"weight\":1}]}")
.textResource("structures/test.json",
"{\"startPool\":\"start\",\"maxDepth\":1,\"maxSizeChunks\":1}")
.build();
assertThrows(IllegalArgumentException.class,
() -> StructureResourceBundleGraphCompiler.requireViable(bundle));
}
private StructureResourceBundle.Builder baseBundle() {
StructureKey key = StructureKey.parse("iris:test");
return StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.IRIS, key))
.backend(StructureBackend.IRIS_ASSEMBLY);
}
private byte[] object(int width, int height, int depth) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
output.writeUTF("Iris V2 IOB;");
output.writeShort(0);
output.writeInt(0);
output.writeInt(0);
}
return bytes.toByteArray();
}
private byte[] truncatedObject(int width, int height, int depth) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeInt(width);
output.writeInt(height);
output.writeInt(depth);
output.writeUTF("Iris V2 IOB;");
}
return bytes.toByteArray();
}
}
@@ -0,0 +1,124 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.engine.mantle.components;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
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 CaveObjectPlacementTransactionTest {
@Test
public void validPlacementCommitsBlocksTilesAndMetadataTogether() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
PlatformBlockState state = mock(PlatformBlockState.class);
TileData tile = mock(TileData.class);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, state);
transaction.setTile(4, 30, 7, tile);
transaction.setData(4, 30, 7, "object@1");
assertSame(state, transaction.get(4, 30, 7));
assertEquals("object@1", transaction.getData(4, 30, 7, String.class));
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setTile(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
org.mockito.InOrder order = inOrder(delegate);
order.verify(delegate).set(4, 30, 7, state);
order.verify(delegate).setTile(4, 30, 7, tile);
order.verify(delegate).setData(4, 30, 7, "object@1");
}
@Test
public void oneInvalidMetadataWriteRejectsTheWholePlacement() {
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(128);
when(engine.getHeight(eq(4), anyInt(), eq(true))).thenReturn(80);
when(engine.getHeight(eq(5), anyInt(), eq(true))).thenReturn(35);
IObjectPlacer delegate = mock(IObjectPlacer.class);
when(delegate.getEngine()).thenReturn(engine);
when(delegate.isCarved(anyInt(), anyInt(), anyInt())).thenReturn(true);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, mock(PlatformBlockState.class));
transaction.setData(5, 26, 7, "ghost");
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_BOUNDS, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void perColumnCaveCeilingRejectsTheWholePlacement() {
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(128);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(80);
IObjectPlacer delegate = mock(IObjectPlacer.class);
when(delegate.getEngine()).thenReturn(engine);
when(delegate.isCarved(eq(4), anyInt(), eq(7))).thenAnswer(invocation -> invocation.<Integer>getArgument(1) <= 60);
when(delegate.isCarved(eq(5), anyInt(), eq(7))).thenAnswer(invocation -> invocation.<Integer>getArgument(1) <= 24);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 30, 7, mock(PlatformBlockState.class));
transaction.setData(5, 25, 7, "outside-cave");
assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_BOUNDS, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void exactBurialBoundaryIsAllowedAndShallowerAnchorsAreExcluded() {
IObjectPlacer delegate = createPlacer(128, 80, 20, 90);
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
transaction.set(4, 70, 7, mock(PlatformBlockState.class));
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
assertEquals(71, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 10));
assertEquals(61, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 20));
}
private IObjectPlacer createPlacer(int worldHeight, int surfaceHeight, int caveFloor, int caveCeiling) {
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(worldHeight);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenReturn(surfaceHeight);
IObjectPlacer delegate = mock(IObjectPlacer.class);
when(delegate.getEngine()).thenReturn(engine);
when(delegate.isCarved(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int y = invocation.getArgument(1);
return y >= caveFloor && y < caveCeiling;
});
return delegate;
}
}
@@ -0,0 +1,100 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.engine.mantle.components;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
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 FloatingObjectPlacementTransactionTest {
@Test
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
IslandObjectPlacer delegate = createPlacer(false);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate);
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
TileData tile = mock(TileData.class);
transaction.set(15, 101, 8, first);
transaction.setData(15, 101, 8, "tree@7");
transaction.set(16, 101, 8, second);
transaction.setTile(16, 101, 8, tile);
transaction.setData(16, 101, 8, "tree@7");
assertSame(second, transaction.get(16, 101, 8));
assertEquals("tree@7", transaction.getData(16, 101, 8, String.class));
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setTile(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
assertEquals(FloatingObjectPlacementTransaction.CommitResult.REJECTED_SUPPORT, transaction.commit());
verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setTile(anyInt(), anyInt(), anyInt(), any());
verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any());
}
@Test
public void supportedNeighborChunkCommitsBlocksTilesAndMarkers() {
IslandObjectPlacer delegate = createPlacer(true);
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate);
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
TileData tile = mock(TileData.class);
transaction.set(15, 101, 8, first);
transaction.setData(15, 101, 8, "tree@7");
transaction.set(16, 101, 8, second);
transaction.setTile(16, 101, 8, tile);
transaction.setData(16, 101, 8, "tree@7");
assertEquals(FloatingObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
org.mockito.InOrder order = inOrder(delegate);
order.verify(delegate).set(15, 101, 8, first);
order.verify(delegate).setData(15, 101, 8, "tree@7");
order.verify(delegate).set(16, 101, 8, second);
order.verify(delegate).setTile(16, 101, 8, tile);
order.verify(delegate).setData(16, 101, 8, "tree@7");
assertEquals(FloatingObjectPlacementTransaction.CommitResult.EMPTY, transaction.commit());
verify(delegate).set(16, 101, 8, second);
}
private IslandObjectPlacer createPlacer(boolean supportNeighborChunk) {
IslandObjectPlacer placer = mock(IslandObjectPlacer.class);
Engine engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(384);
when(placer.getEngine()).thenReturn(engine);
when(placer.canWriteObjectBlock(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
return x == 15 || (supportNeighborChunk && x == 16);
});
return placer;
}
}
@@ -21,61 +21,47 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.block.data.BlockData;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
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.assertFalse;
import static org.junit.Assert.assertNotEquals;
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.doReturn;
import static org.mockito.Mockito.mock;
public class IrisCaveCarver3DNearParityTest {
private static Method sampleDensityMethod;
private static Method aquiferCandidateMethod;
private static Field engineField;
private static Field dataField;
private static Field profileField;
private static Field surfaceBreakDensityField;
private static Field thresholdRngField;
private static Field carveAirField;
private static Field carveWaterField;
private static Field carveLavaField;
private static Field carveForcedAirField;
@BeforeClass
public static void setupBukkit() throws Exception {
if (Bukkit.getServer() == null) {
Server server = mock(Server.class);
doReturn(Logger.getLogger("IrisTest")).when(server).getLogger();
doReturn("IrisTestServer").when(server).getName();
doReturn("1.0").when(server).getVersion();
doReturn("1.0").when(server).getBukkitVersion();
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, Material.class).name().toLowerCase(Locale.ROOT))).when(server).createBlockData(any(Material.class));
doAnswer((InvocationOnMock invocation) -> namedBlockData(invocation.getArgument(0, String.class))).when(server).createBlockData(anyString());
Bukkit.setServer(server);
}
public static void setupReflection() throws Exception {
sampleDensityMethod = IrisCaveCarver3D.class.getDeclaredMethod("sampleDensityOptimized", int.class, int.class, int.class);
sampleDensityMethod.setAccessible(true);
aquiferCandidateMethod = IrisCaveCarver3D.class.getDeclaredMethod("isAquiferCandidate", int.class, int.class, int.class, double.class);
aquiferCandidateMethod.setAccessible(true);
engineField = IrisCaveCarver3D.class.getDeclaredField("engine");
engineField.setAccessible(true);
dataField = IrisCaveCarver3D.class.getDeclaredField("data");
@@ -88,19 +74,14 @@ public class IrisCaveCarver3DNearParityTest {
thresholdRngField.setAccessible(true);
carveAirField = IrisCaveCarver3D.class.getDeclaredField("carveAir");
carveAirField.setAccessible(true);
carveWaterField = IrisCaveCarver3D.class.getDeclaredField("carveWater");
carveWaterField.setAccessible(true);
carveLavaField = IrisCaveCarver3D.class.getDeclaredField("carveLava");
carveLavaField.setAccessible(true);
carveForcedAirField = IrisCaveCarver3D.class.getDeclaredField("carveForcedAir");
carveForcedAirField.setAccessible(true);
}
private static BlockData namedBlockData(String key) {
String canonical = key.indexOf(':') >= 0 ? key : "minecraft:" + key;
BlockData data = mock(BlockData.class);
doReturn(canonical).when(data).getAsString();
return data;
}
@Test
public void carvedCellDistributionStableAcrossEquivalentCarvers() {
Engine engine = createEngine(128, 92);
@@ -229,6 +210,160 @@ public class IrisCaveCarver3DNearParityTest {
assertTrue(containsLiquidInRange(forcedAirCapture.carvedLiquids, 19, 47, (byte) 0));
}
@Test
public void waterPrecedesForcedAirWhenLavaIsDisabled() {
Engine engine = createEngine(48, 46);
int[] surfaceHeights = filledHeights(46);
IrisCaveProfile wetProfile = createWaterProfile()
.setVerticalRange(new IrisRange(2D, 18D))
.setAllowLava(false)
.setWaterRequiresFloor(false);
WriterCapture wetCapture = createWriterCapture(48);
new IrisCaveCarver3D(engine, wetProfile).carve(
wetCapture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights);
IrisCaveProfile dryProfile = createWaterProfile()
.setVerticalRange(new IrisRange(2D, 18D))
.setAllowWater(false)
.setAllowLava(false)
.setWaterRequiresFloor(false);
WriterCapture dryCapture = createWriterCapture(48);
new IrisCaveCarver3D(engine, dryProfile).carve(
dryCapture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights);
assertEquals(wetCapture.carvedCells, dryCapture.carvedCells);
assertTrue(containsLiquidInRange(wetCapture.carvedLiquids, 2, 18, (byte) 1));
assertTrue(containsLiquidInRange(wetCapture.carvedLiquids, 2, 18, (byte) 3));
assertEquals(0, countLiquid(dryCapture, (byte) 1));
assertTrue(containsLiquidInRange(dryCapture.carvedLiquids, 2, 18, (byte) 3));
}
@Test
public void waterToggleAndMinimumDepthUseEachColumnsTerrainSurface() {
Engine engine = createEngine(80, 70);
int[] surfaceHeights = splitSurfaceHeights(40, 70);
IrisCaveProfile enabledProfile = createWaterProfile().setAllowWater(true).setWaterMinDepthBelowSurface(10);
WriterCapture enabledCapture = createWriterCapture(80);
new IrisCaveCarver3D(engine, enabledProfile).carve(
enabledCapture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights);
IrisCaveProfile disabledProfile = createWaterProfile().setAllowWater(false).setWaterMinDepthBelowSurface(10);
WriterCapture disabledCapture = createWriterCapture(80);
new IrisCaveCarver3D(engine, disabledProfile).carve(
disabledCapture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights);
assertEquals(enabledCapture.carvedCells, disabledCapture.carvedCells);
assertTrue(countLiquid(enabledCapture, (byte) 1) > 0);
assertEquals(0, countLiquid(disabledCapture, (byte) 1));
assertWaterRespectsSplitCutoff(enabledCapture, 30, 60);
assertTrue(enabledCapture.carvedCells.contains(cellKey(0, 40, 0)));
assertTrue(enabledCapture.carvedCells.contains(cellKey(15, 70, 0)));
}
@Test
public void dimensionFluidHeightCapsWaterIntent() {
Engine engine = createEngine(80, 70);
IrisCaveProfile profile = createWaterProfile().setWaterMinDepthBelowSurface(0);
WriterCapture capture = createWriterCapture(80);
new IrisCaveCarver3D(engine, profile).carve(
capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(70));
assertTrue(countLiquid(capture, (byte) 1) > 0);
assertLiquidAtOrBelow(capture, (byte) 1, 64);
assertTrue(containsLiquidInRange(capture.carvedLiquids, 65, 70, (byte) 0));
}
@Test
public void floorRequiredWaterResolvesAfterTheCompleteCarveMask() {
Engine engine = createEngine(80, 70);
int[] surfaceHeights = filledHeights(70);
int chunkX = -8;
int chunkZ = -1;
IrisStyledRange cupThreshold = new IrisStyledRange(0.15D, 0.15D, new IrisGeneratorStyle(NoiseStyle.FLAT));
IrisCaveProfile supportedProfile = createWaterProfile()
.setDensityThreshold(cupThreshold)
.setWaterMinDepthBelowSurface(0)
.setWaterRequiresFloor(true);
WriterCapture firstCapture = createWriterCapture(80);
IrisCaveCarver3D.WaterSupportPlan supportPlan = new IrisCaveCarver3D.WaterSupportPlan();
new IrisCaveCarver3D(engine, supportedProfile).carve(
firstCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights, null, supportPlan);
int candidateCount = countLiquid(firstCapture, (byte) 1);
supportPlan.resolve(firstCapture.writer.acquireChunk(chunkX, chunkZ));
IrisCaveProfile repeatedProfile = createWaterProfile()
.setDensityThreshold(cupThreshold)
.setWaterMinDepthBelowSurface(0)
.setWaterRequiresFloor(true);
WriterCapture secondCapture = createWriterCapture(80);
new IrisCaveCarver3D(engine, repeatedProfile).carve(
secondCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights);
IrisCaveProfile unrestrictedProfile = createWaterProfile()
.setDensityThreshold(cupThreshold)
.setWaterMinDepthBelowSurface(0)
.setWaterRequiresFloor(false);
WriterCapture unrestrictedCapture = createWriterCapture(80);
new IrisCaveCarver3D(engine, unrestrictedProfile).carve(
unrestrictedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, null, surfaceHeights);
assertTrue(candidateCount > 0);
assertEquals(firstCapture.carvedLiquids, secondCapture.carvedLiquids);
assertTrue(countLiquid(firstCapture, (byte) 1) > 0);
assertTrue(countLiquid(firstCapture, (byte) 1) < countLiquid(unrestrictedCapture, (byte) 1));
assertWaterCellsHaveSolidSupport(firstCapture);
}
@Test
public void finalWaterSupportRejectsUnknownNeighborChunkEdges() {
WriterCapture capture = createWriterCapture(80);
MantleChunk<Matter> chunk = capture.writer.acquireChunk(0, 0);
MatterSlice<MatterCavern> slice = chunk.getOrCreate(3).slice(MatterCavern.class);
MatterCavern water = new MatterCavern(true, "", (byte) 1);
MatterCavern air = new MatterCavern(true, "", (byte) 0);
int y = 56;
int z = 8;
slice.set(0, y & 15, z, water);
slice.set(8, y & 15, z, water);
IrisCaveCarver3D.WaterSupportPlan supportPlan = new IrisCaveCarver3D.WaterSupportPlan();
supportPlan.add(0, y, z, water, air);
supportPlan.add(8, y, z, water, air);
supportPlan.resolve(chunk);
assertEquals(Byte.valueOf((byte) 0), capture.carvedLiquids.get(cellKey(0, y, z)));
assertEquals(Byte.valueOf((byte) 1), capture.carvedLiquids.get(cellKey(8, y, z)));
}
@Test
public void floorRequiredWaterSeesLaterProfileCarvePasses() {
Engine engine = createEngine(80, 70);
int[] surfaceHeights = filledHeights(70);
WriterCapture capture = createWriterCapture(80);
IrisCaveCarver3D.WaterSupportPlan waterSupportPlan = new IrisCaveCarver3D.WaterSupportPlan();
IrisCaveProfile waterProfile = createWaterProfile()
.setDensityThreshold(new IrisStyledRange(0.15D, 0.15D, new IrisGeneratorStyle(NoiseStyle.FLAT)))
.setWaterRequiresFloor(true);
IrisCaveProfile airProfile = createWaterProfile().setAllowWater(false);
new IrisCaveCarver3D(engine, waterProfile).carve(
capture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights,
new IrisRange(20D, 64D), waterSupportPlan);
String waterCell = firstCellWithLiquid(capture, (byte) 1);
assertTrue(waterCell != null);
int waterY = coordinate(waterCell, 1);
new IrisCaveCarver3D(engine, airProfile).carve(
capture.writer, 0, 0, fullWeights(), 0D, 0D, null, surfaceHeights,
new IrisRange(waterY - 1D, waterY - 1D), waterSupportPlan);
assertEquals(Byte.valueOf((byte) 1), capture.carvedLiquids.get(waterCell));
waterSupportPlan.resolve(capture.writer.acquireChunk(0, 0));
assertEquals(Byte.valueOf((byte) 0), capture.carvedLiquids.get(waterCell));
}
@Test
public void optimizedExactPathOutperformsNaiveReference() throws Exception {
Engine engine = createEngine(128, 92);
@@ -336,6 +471,7 @@ public class IrisCaveCarver3DNearParityTest {
CNG surfaceBreakDensity = (CNG) surfaceBreakDensityField.get(carver);
RNG thresholdRng = (RNG) thresholdRngField.get(carver);
MatterCavern carveAir = (MatterCavern) carveAirField.get(carver);
MatterCavern carveWater = (MatterCavern) carveWaterField.get(carver);
MatterCavern carveLava = (MatterCavern) carveLavaField.get(carver);
MatterCavern carveForcedAir = (MatterCavern) carveForcedAirField.get(carver);
@@ -366,6 +502,7 @@ public class IrisCaveCarver3DNearParityTest {
int[] columnTopY = new int[256];
int[] surfaceBreakFloorY = new int[256];
boolean[] surfaceBreakColumn = new boolean[256];
int[] waterMaxY = new int[256];
double[] passThreshold = new double[256];
double[] verticalEdgeFade = computeVerticalEdgeFade(profile, minY, maxY);
MatterCavern[] matterByY = computeMatterByY(engine, profile, carveAir, carveLava, carveForcedAir, minY, maxY);
@@ -388,6 +525,9 @@ public class IrisCaveCarver3DNearParityTest {
boolean breakColumn = allowSurfaceBreak && signed(surfaceBreakDensity.noiseFast2D(x, z)) >= surfaceBreakNoiseThreshold;
int resolvedTopY = breakColumn ? Math.min(maxY, Math.max(minY, columnSurfaceY)) : clearanceTopY;
columnTopY[columnIndex] = resolvedTopY;
waterMaxY[columnIndex] = profile.isAllowWater()
? Math.min(engine.getDimension().getFluidHeight(), columnSurfaceY - Math.max(0, profile.getWaterMinDepthBelowSurface()))
: Integer.MIN_VALUE;
surfaceBreakFloorY[columnIndex] = Math.max(minY, columnSurfaceY - surfaceBreakDepth);
surfaceBreakColumn[columnIndex] = breakColumn;
double columnWeight = clampColumnWeight(resolvedWeights[columnIndex]);
@@ -431,7 +571,12 @@ public class IrisCaveCarver3DNearParityTest {
Matter sectionMatter = chunk.getOrCreate(y >> 4);
MatterSlice<MatterCavern> cavernSlice = sectionMatter.slice(MatterCavern.class);
cavernSlice.set(localX, y & 15, localZ, matterByY[y - minY]);
MatterCavern verticalMatter = matterByY[y - minY];
boolean aquifer = verticalMatter == carveAir
&& y <= waterMaxY[columnIndex]
&& (boolean) aquiferCandidateMethod.invoke(carver, x, y, z, localThreshold);
MatterCavern matter = aquifer ? carveWater : verticalMatter;
cavernSlice.set(localX, y & 15, localZ, matter);
carved++;
}
}
@@ -469,9 +614,7 @@ public class IrisCaveCarver3DNearParityTest {
private MatterCavern[] computeMatterByY(Engine engine, IrisCaveProfile profile, MatterCavern carveAir, MatterCavern carveLava, MatterCavern carveForcedAir, int minY, int maxY) {
MatterCavern[] matterByY = new MatterCavern[Math.max(0, maxY - minY + 1)];
boolean allowLava = profile.isAllowLava();
boolean allowWater = profile.isAllowWater();
int lavaHeight = engine.getDimension().getCaveLavaHeight();
int fluidHeight = engine.getDimension().getFluidHeight();
for (int y = minY; y <= maxY; y++) {
int offset = y - minY;
@@ -479,10 +622,6 @@ public class IrisCaveCarver3DNearParityTest {
matterByY[offset] = carveLava;
continue;
}
if (allowWater && y <= fluidHeight) {
matterByY[offset] = carveAir;
continue;
}
if (!allowLava && y <= lavaHeight) {
matterByY[offset] = carveForcedAir;
continue;
@@ -565,6 +704,22 @@ public class IrisCaveCarver3DNearParityTest {
return profile;
}
private IrisCaveProfile createWaterProfile() {
return createProfile(false, false)
.setVerticalRange(new IrisRange(20D, 70D))
.setVerticalEdgeFade(0)
.setVerticalEdgeFadeStrength(0D)
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)))
.setAdaptiveSampling(false)
.setSurfaceClearance(5)
.setAllowSurfaceBreak(true)
.setSurfaceBreakNoiseThreshold(-1D)
.setSurfaceBreakThresholdBoost(0D)
.setAllowWater(true)
.setWaterRequiresFloor(false)
.setAllowLava(true);
}
private WriterCapture createWriterCapture(int worldHeight) {
MantleWriter writer = mock(MantleWriter.class);
@SuppressWarnings("unchecked")
@@ -579,6 +734,7 @@ public class IrisCaveCarver3DNearParityTest {
doReturn(mantle).when(writer).getMantle();
doReturn(worldHeight).when(mantle).getWorldHeight();
doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt());
doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt());
doAnswer(invocation -> {
int sectionIndex = invocation.getArgument(0);
Matter section = sections.get(sectionIndex);
@@ -601,6 +757,7 @@ public class IrisCaveCarver3DNearParityTest {
Map<Integer, MatterCavern> localCells = sectionCells.computeIfAbsent(sectionIndex, key -> new HashMap<>());
doReturn(slice).when(matter).slice(MatterCavern.class);
doReturn(slice).when(matter).getSlice(MatterCavern.class);
doAnswer(invocation -> {
int localX = invocation.getArgument(0);
int localY = invocation.getArgument(1);
@@ -635,6 +792,100 @@ public class IrisCaveCarver3DNearParityTest {
return heights;
}
private int[] splitSurfaceHeights(int lowHeight, int highHeight) {
int[] heights = new int[256];
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
heights[(localX << 4) | localZ] = localX < 8 ? lowHeight : highHeight;
}
}
return heights;
}
private void assertWaterCellsHaveSolidSupport(WriterCapture capture) {
for (Map.Entry<String, Byte> entry : capture.carvedLiquids.entrySet()) {
if (entry.getValue() != 1) {
continue;
}
String[] split = entry.getKey().split(":");
int x = Integer.parseInt(split[0]);
int y = Integer.parseInt(split[1]);
int z = Integer.parseInt(split[2]);
assertFalse(capture.carvedCells.contains(cellKey(x, y - 1, z)));
assertFalse(capture.carvedCells.contains(cellKey(x, y - 2, z)));
int support = 0;
if (!capture.carvedCells.contains(cellKey(x + 1, y, z))) {
support++;
}
if (!capture.carvedCells.contains(cellKey(x - 1, y, z))) {
support++;
}
if (!capture.carvedCells.contains(cellKey(x, y, z + 1))) {
support++;
}
if (!capture.carvedCells.contains(cellKey(x, y, z - 1))) {
support++;
}
if (!capture.carvedCells.contains(cellKey(x, y + 1, z))) {
support++;
}
assertTrue(support >= 4);
}
}
private void assertWaterRespectsSplitCutoff(WriterCapture capture, int lowCutoff, int highCutoff) {
boolean lowWater = false;
boolean highWater = false;
for (Map.Entry<String, Byte> entry : capture.carvedLiquids.entrySet()) {
if (entry.getValue() != 1) {
continue;
}
int x = coordinate(entry.getKey(), 0);
int y = coordinate(entry.getKey(), 1);
if (x < 8) {
assertTrue(y <= lowCutoff);
lowWater = true;
} else {
assertTrue(y <= highCutoff);
highWater = true;
}
}
assertTrue(lowWater);
assertTrue(highWater);
}
private void assertLiquidAtOrBelow(WriterCapture capture, byte liquid, int maxY) {
for (Map.Entry<String, Byte> entry : capture.carvedLiquids.entrySet()) {
if (entry.getValue() == liquid) {
assertTrue(coordinate(entry.getKey(), 1) <= maxY);
}
}
}
private int countLiquid(WriterCapture capture, byte liquid) {
int count = 0;
for (byte value : capture.carvedLiquids.values()) {
if (value == liquid) {
count++;
}
}
return count;
}
private String firstCellWithLiquid(WriterCapture capture, byte liquid) {
for (Map.Entry<String, Byte> entry : capture.carvedLiquids.entrySet()) {
if (entry.getValue() == liquid) {
return entry.getKey();
}
}
return null;
}
private int coordinate(String cell, int index) {
return Integer.parseInt(cell.split(":")[index]);
}
private double clampColumnWeight(double weight) {
if (Double.isNaN(weight) || Double.isInfinite(weight)) {
return 0D;
@@ -1,11 +1,19 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.NativeStructureSuppression;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
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.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -32,4 +40,64 @@ public class IrisStructureComponentMarkerTest {
assertEquals(first, repeated);
assertNotEquals(first, moved);
}
@Test
public void emptyResolvedPiecesRemainSkippableForNonReplacement() {
IrisStructureLocator.ResolvedPlacement resolved = resolvedPlacement(
NativeStructureSuppression.NONE, new KList<>());
assertNull(IrisStructureComponent.resolvedPiecesOrNull(resolved, 4, -3));
IrisStructureComponent.requireAppliedPieces(resolvedWithOnePiece(NativeStructureSuppression.NONE),
4, -3, 1);
}
@Test
public void emptyResolvedPiecesFailForReplacement() {
IrisStructureLocator.ResolvedPlacement resolved = resolvedPlacement(
NativeStructureSuppression.REPLACE_SOURCE, new KList<>());
assertReplacementFailure(
() -> IrisStructureComponent.resolvedPiecesOrNull(resolved, 4, -3),
"placement application received no assembled pieces");
}
@Test
public void rejectedAppliedPieceFailsForReplacement() {
IrisStructureLocator.ResolvedPlacement resolved = resolvedWithOnePiece(
NativeStructureSuppression.REPLACE_SOURCE);
assertReplacementFailure(
() -> IrisStructureComponent.requireAppliedPieces(resolved, 4, -3, 1),
"object placement rejected 1 of 1 assembled piece(s)");
}
private IrisStructureLocator.ResolvedPlacement resolvedWithOnePiece(
NativeStructureSuppression suppression
) {
KList<PlacedStructurePiece> pieces = new KList<>();
pieces.add(mock(PlacedStructurePiece.class));
return resolvedPlacement(suppression, pieces);
}
private IrisStructureLocator.ResolvedPlacement resolvedPlacement(
NativeStructureSuppression suppression,
KList<PlacedStructurePiece> pieces
) {
IrisStructurePlacement placement = new IrisStructurePlacement().setNativeSuppression(suppression);
placement.getStructures().add("test:city");
IrisStructure structure = new IrisStructure();
structure.setLoadKey("test:city");
return new IrisStructureLocator.ResolvedPlacement(
placement, "test:city", structure, pieces, new RNG(1L), 0, 64, 0, false);
}
private void assertReplacementFailure(Runnable operation, String expectedMessage) {
try {
operation.run();
} catch (IllegalStateException e) {
assertTrue(e.getMessage(), e.getMessage().contains(expectedMessage));
return;
}
throw new AssertionError("Expected strict native replacement failure containing '" + expectedMessage + "'");
}
}
@@ -0,0 +1,54 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import org.mockito.InOrder;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisStructureFoundationWriteTest {
@Test
public void resolvesWorldCoordinatesThenClearsCavernBeforeWritingSupport() {
MantleWriter writer = mock(MantleWriter.class);
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
RNG rng = new RNG(1337L);
IrisData data = mock(IrisData.class);
PlatformBlockState deepslateBricks = mock(PlatformBlockState.class);
when(palette.get(rng, 12, -247, -18, data)).thenReturn(deepslateBricks);
IrisStructureComponent.writeFoundationSupport(
writer, palette, rng, data, 12, 9, -18, -256);
InOrder order = inOrder(palette, writer);
order.verify(palette).get(rng, 12, -247, -18, data);
order.verify(writer).clearData(12, 9, -18, MatterCavern.class);
order.verify(writer).set(12, 9, -18, deepslateBricks);
}
@Test
public void unresolvedPaletteFailsBeforeChangingCavernOrBlocks() {
MantleWriter writer = mock(MantleWriter.class);
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
RNG rng = new RNG(1337L);
IrisData data = mock(IrisData.class);
try {
IrisStructureComponent.writeFoundationSupport(
writer, palette, rng, data, 12, 9, -18, -256);
} catch (IllegalStateException error) {
assertTrue(error.getMessage().contains("12,-247,-18"));
verifyNoInteractions(writer);
return;
}
throw new AssertionError("Expected an unresolved structure stilt palette to fail");
}
}
@@ -0,0 +1,103 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.engine.mantle.components;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectTranslate;
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.volmlib.util.collection.KList;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleFloatingObjectComponentBoundaryRadiusTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@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
@SuppressWarnings("unchecked")
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
File objectFile = temporaryFolder.newFile("floating.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/floating"))
.setTranslate(new IrisObjectTranslate().setX(32));
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
.setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
}
}
@@ -1,16 +1,26 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.FloatingObjectFootprint;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObjectRotation;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class MantleFloatingObjectComponentInvertedPlacementTest {
private FloatingObjectFootprint footprint(int lowestSolidKeyY, int highestSolidKeyY, int tallestKx, int tallestKz) throws Exception {
return footprint(lowestSolidKeyY, highestSolidKeyY, tallestKx, tallestKz, new long[0]);
}
private FloatingObjectFootprint footprint(int lowestSolidKeyY, int highestSolidKeyY,
int tallestKx, int tallestKz, long[] cells) throws Exception {
Constructor<FloatingObjectFootprint> constructor = FloatingObjectFootprint.class.getDeclaredConstructor(
int.class,
int.class,
@@ -34,10 +44,25 @@ public class MantleFloatingObjectComponentInvertedPlacementTest {
tallestKz,
99,
99,
new long[0]
cells
);
}
private FloatingIslandSample sample(IrisFloatingChildBiomes entry) throws Exception {
Constructor<FloatingIslandSample> constructor = FloatingIslandSample.class.getDeclaredConstructor(
IrisFloatingChildBiomes.class,
int.class,
int.class,
int.class,
int.class,
boolean[].class
);
constructor.setAccessible(true);
boolean[] solidMask = new boolean[10];
Arrays.fill(solidMask, true);
return constructor.newInstance(entry, 100, 10, 9, 10, solidMask);
}
@Test
public void invertedBaseY_anchorsOriginalLowestSolidBelowBottomFace() throws Exception {
FloatingObjectFootprint footprint = footprint(5, 30, 2, 3);
@@ -82,4 +107,21 @@ public class MantleFloatingObjectComponentInvertedPlacementTest {
assertEquals(104, MantleFloatingObjectComponent.invertedBaseY(100, footprint, rotation));
}
@Test
public void topFootprintChecksNeighborChunkTerrain() throws Exception {
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes();
FloatingIslandSample sample = sample(entry);
long[] cells = {0L, 1L << 32};
FloatingObjectFootprint footprint = footprint(0, 0, 0, 0, cells);
assertTrue(MantleFloatingObjectComponent.isFootprintFlat(
footprint, 15, 0, 109,
(x, z) -> z == 0 && (x == 15 || x == 16) ? sample : null,
entry, 2));
assertFalse(MantleFloatingObjectComponent.isFootprintFlat(
footprint, 15, 0, 109,
(x, z) -> x == 15 && z == 0 ? sample : null,
entry, 2));
}
}
@@ -0,0 +1,225 @@
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.core.nms.container.Pair;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.MatterGenerator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.IrisObjectScale;
import art.arcane.iris.engine.object.IrisObjectTranslate;
import art.arcane.iris.engine.object.IrisProceduralObjects;
import art.arcane.iris.engine.object.IrisProceduralPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisVacuumSettings;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.engine.object.ObjectPlaceMode;
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.math.IrisBlockVector;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
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.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
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.spy;
import static org.mockito.Mockito.when;
public class MantleObjectComponentBoundaryRadiusTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@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 verticalRotationAndTranslationExpandHorizontalReach() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement));
}
@Test
public void scaleWarpAndVacuumExpandPlacementReach() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setScale(new IrisObjectScale().setSize(2D))
.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(10D))
.setMode(ObjectPlaceMode.VACUUM)
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
assertEquals(43, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement));
}
@Test
public void proceduralTreeTransformsContributeToRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
IrisData data = mock(IrisData.class);
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
when(proceduralPlacement.getVariantObjects(data)).thenReturn(new KList<>(new IrisObject(5, 48, 7)));
when(proceduralPlacement.asPlacement()).thenReturn(placement);
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
when(proceduralObjects.isEmpty()).thenReturn(false);
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
IrisRegion region = mock(IrisRegion.class);
when(region.getObjects()).thenReturn(new KList<>());
when(region.getProceduralObjects()).thenReturn(proceduralObjects);
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getAllRegions(any())).thenReturn(new KList<>(region));
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
}
@Test
@SuppressWarnings("unchecked")
public void translatedTreeSchedulesDistantOwnerChunks() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/tree"))
.setTranslate(new IrisObjectTranslate().setX(32));
IrisBiome biome = new IrisBiome().setObjects(new KList<>(placement));
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.isUseMantle()).thenReturn(true);
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
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);
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
int chunkRadius = Math.ceilDiv(component.getRadius(), 16);
assertEquals(3, chunkRadius);
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));
List<String> generatedChunks = new ArrayList<>();
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(new GeneratorOptions(engine, mantle, component, chunkRadius));
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
assertTrue(generatedChunks.contains("-2,0"));
assertTrue(generatedChunks.contains("2,0"));
}
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 final int radius;
private TestMatterGenerator(GeneratorOptions options) {
this.engine = options.engine();
this.mantle = options.mantle();
this.components = List.of(new Pair<>(List.of(options.component()), options.radius()));
this.radius = options.radius();
}
@Override
public Engine getEngine() {
return engine;
}
@Override
public Mantle<Matter> getMantle() {
return mantle;
}
@Override
public int getRadius() {
return radius;
}
@Override
public int getRealRadius() {
return 0;
}
@Override
public List<Pair<List<MantleComponent>, Integer>> getComponents() {
return components;
}
}
private record GeneratorOptions(Engine engine, Mantle<Matter> mantle, MantleComponent component, int radius) {
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.spi.PlatformBlockState;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import org.junit.Test;
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.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class StructureFoundationPlannerTest {
@Test
public void recordsLowestPlacedOccludingCellAcrossTheAssembledFootprint() {
PlatformBlockState solid = mock(PlatformBlockState.class);
when(solid.isOccluding()).thenReturn(true);
PlatformBlockState decoration = mock(PlatformBlockState.class);
when(decoration.isOccluding()).thenReturn(false);
Long2IntOpenHashMap columns = new Long2IntOpenHashMap();
StructureFoundationPlanner.recordBaseCell(columns, 100, 48, 200, decoration);
StructureFoundationPlanner.recordBaseCell(columns, 100, 49, 200, solid);
StructureFoundationPlanner.recordBaseCell(columns, 100, 50, 200, solid);
StructureFoundationPlanner.recordBaseCell(columns, 100, 45, 200, solid);
StructureFoundationPlanner.recordBaseCell(columns, -17, 60, -33, solid);
assertEquals(2, columns.size());
assertEquals(45, columns.get(StructureFoundationPlanner.pack(100, 200)));
assertEquals(60, columns.get(StructureFoundationPlanner.pack(-17, -33)));
assertEquals(-17, StructureFoundationPlanner.unpackX(StructureFoundationPlanner.pack(-17, -33)));
assertEquals(-33, StructureFoundationPlanner.unpackZ(StructureFoundationPlanner.pack(-17, -33)));
}
@Test
public void scansThroughAirAndFluidUntilSolidGround() {
int groundY = StructureFoundationPlanner.findGroundY(10, 10, 0, y -> y == 3);
assertEquals(3, groundY);
assertEquals(StructureFoundationPlanner.NO_GROUND,
StructureFoundationPlanner.findGroundY(10, 6, 0, y -> y == 3));
}
@Test
public void compositeGroundUsesOverlayThenCarvingThenTerrain() {
PlatformBlockState solid = mock(PlatformBlockState.class);
when(solid.isSolid()).thenReturn(true);
PlatformBlockState fluid = mock(PlatformBlockState.class);
when(fluid.isSolid()).thenReturn(false);
assertTrue(StructureFoundationPlanner.isGroundSolid(solid, true, 12, 20));
assertFalse(StructureFoundationPlanner.isGroundSolid(fluid, false, 12, 20));
assertFalse(StructureFoundationPlanner.isGroundSolid(null, true, 12, 20));
assertTrue(StructureFoundationPlanner.isGroundSolid(null, false, 12, 20));
assertFalse(StructureFoundationPlanner.isGroundSolid(null, false, 21, 20));
assertTrue(StructureFoundationPlanner.isGroundSolid(null, true, 0, -20));
}
@Test
public void fillsOnlyTheGapBetweenTheFoundationAndGround() {
List<Integer> written = new ArrayList<>();
int count = StructureFoundationPlanner.fillSupportColumn(10, 3, written::add);
assertEquals(6, count);
assertEquals(List.of(9, 8, 7, 6, 5, 4), written);
}
@Test
public void doesNotWriteWithoutReachableGroundOrOverExistingGround() {
List<Integer> written = new ArrayList<>();
assertEquals(0, StructureFoundationPlanner.fillSupportColumn(
10, StructureFoundationPlanner.NO_GROUND, written::add));
assertEquals(0, StructureFoundationPlanner.fillSupportColumn(10, 9, written::add));
assertTrue(written.isEmpty());
}
}
@@ -0,0 +1,45 @@
package art.arcane.iris.engine.modifier;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisCarveModifierFluidIntentTest {
@Test
public void explicitCavernIntentsOverrideExistingFluid() {
MatterCavern airIntent = new MatterCavern(true, "", (byte) 0);
MatterCavern waterIntent = new MatterCavern(true, "", (byte) 1);
MatterCavern lavaIntent = new MatterCavern(true, "", (byte) 2);
MatterCavern forcedAirIntent = new MatterCavern(true, "", (byte) 3);
PlatformBlockState existingFluid = mock(PlatformBlockState.class);
PlatformBlockState fluid = mock(PlatformBlockState.class);
PlatformBlockState lava = mock(PlatformBlockState.class);
PlatformBlockState air = mock(PlatformBlockState.class);
when(existingFluid.isFluid()).thenReturn(true);
assertFalse(IrisCarveModifier.hasExplicitCarveIntent(null));
assertTrue(IrisCarveModifier.shouldPreserveExistingFluid(airIntent, existingFluid));
assertFalse(IrisCarveModifier.shouldPreserveExistingFluid(waterIntent, existingFluid));
assertFalse(IrisCarveModifier.shouldPreserveExistingFluid(lavaIntent, existingFluid));
assertFalse(IrisCarveModifier.shouldPreserveExistingFluid(forcedAirIntent, existingFluid));
assertNull(IrisCarveModifier.resolveExplicitCarveState(null, fluid, lava, air));
assertSame(fluid, IrisCarveModifier.resolveExplicitCarveState(waterIntent, fluid, lava, air));
assertSame(lava, IrisCarveModifier.resolveExplicitCarveState(lavaIntent, fluid, lava, air));
assertSame(air, IrisCarveModifier.resolveExplicitCarveState(forcedAirIntent, fluid, lava, air));
assertNull(IrisCarveModifier.resolveExplicitCarveState(airIntent, fluid, lava, air));
}
@Test
public void defaultLavaIncludesConfiguredBoundary() {
assertTrue(IrisCarveModifier.usesDefaultLava(18, 17));
assertTrue(IrisCarveModifier.usesDefaultLava(18, 18));
assertFalse(IrisCarveModifier.usesDefaultLava(18, 19));
}
}
@@ -0,0 +1,56 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.engine.modifier;
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.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDepositModifierContainmentTest {
@Test
public void depositSurfaceLimitKeepsEveryColumnSevenBlocksBuried() {
assertEquals(73, IrisDepositModifier.depositSurfaceLimit(80));
assertEquals(24, IrisDepositModifier.depositSurfaceLimit(31));
}
@Test
public void variantHeightUsesAbsoluteWorldY() {
assertEquals(-32, IrisDepositModifier.absoluteWorldY(-64, 32));
assertEquals(96, IrisDepositModifier.absoluteWorldY(0, 96));
}
@Test
public void airAndFluidTargetsAreRejected() {
PlatformBlockState solid = mock(PlatformBlockState.class);
PlatformBlockState air = mock(PlatformBlockState.class);
PlatformBlockState fluid = mock(PlatformBlockState.class);
when(air.isAir()).thenReturn(true);
when(fluid.isFluid()).thenReturn(true);
assertTrue(IrisDepositModifier.canReplaceDepositTarget(solid));
assertFalse(IrisDepositModifier.canReplaceDepositTarget(air));
assertFalse(IrisDepositModifier.canReplaceDepositTarget(fluid));
assertFalse(IrisDepositModifier.canReplaceDepositTarget(null));
}
}
@@ -0,0 +1,687 @@
/*
* Iris is a World Generator for Minecraft Bukkit 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.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class FloatingIslandBoundarySamplerTest {
@Test
public void edgeFade_remainsFullInsideOneParentBiome() {
IrisBiome parent = new IrisBiome();
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> parent);
assertEquals(1.0D, sampler.edgeFade(parent, 0, 0), 0.0D);
}
@Test
public void edgeFade_roundsTowardParentBiomeBoundary() {
IrisBiome left = new IrisBiome();
left.setLoadKey("left");
IrisBiome right = new IrisBiome();
right.setLoadKey("right");
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> x < 0 ? left : right);
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(1), sampler.edgeFade(right, 0, 0), 0.0D);
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(2), sampler.edgeFade(right, 1, 0), 0.0D);
assertEquals(1.0D, sampler.edgeFade(right, FloatingIslandBoundarySampler.EDGE_FADE_RADIUS - 1, 0), 0.0D);
}
@Test
public void edgeFade_acceptsReloadedBiomeInstanceWithSameKey() {
IrisBiome expected = new IrisBiome();
expected.setLoadKey("magnetics/mycelium");
IrisBiome reloaded = new IrisBiome();
reloaded.setLoadKey("magnetics/mycelium");
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> reloaded);
assertEquals(1.0D, sampler.edgeFade(expected, 0, 0), 0.0D);
}
@Test
public void edgeFade_detectsOffAxisBoundaryWithinRadius() {
IrisBiome parent = new IrisBiome();
parent.setLoadKey("parent");
IrisBiome neighbor = new IrisBiome();
neighbor.setLoadKey("neighbor");
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> x == 2 && z == 1 ? neighbor : parent);
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(2), sampler.edgeFade(parent, 0, 0), 0.0D);
}
@Test
public void edgeFade_isSymmetricAcrossChunkBoundary() {
IrisBiome left = new IrisBiome();
left.setLoadKey("left");
IrisBiome right = new IrisBiome();
right.setLoadKey("right");
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> x < 16 ? left : right);
assertEquals(sampler.edgeFade(left, 15, 0), sampler.edgeFade(right, 16, 0), 0.0D);
assertEquals(sampler.edgeFade(left, 14, 0), sampler.edgeFade(right, 17, 0), 0.0D);
assertEquals(sampler.edgeFade(left, 13, 0), sampler.edgeFade(right, 18, 0), 0.0D);
}
@Test
public void parent_cachesResolvedBiomeColumns() {
IrisBiome parent = new IrisBiome();
AtomicInteger calls = new AtomicInteger();
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> {
calls.incrementAndGet();
return parent;
});
assertSame(parent, sampler.parent(12, -9));
int firstReadCount = calls.get();
assertSame(parent, sampler.parent(12, -9));
assertEquals(firstReadCount, calls.get());
int rawWidth = 16 + (FloatingIslandBoundarySampler.EDGE_FADE_RADIUS * 2) + 2;
assertEquals(rawWidth * rawWidth, firstReadCount);
}
@Test
public void edgeFade_boundsBiomeStreamReadsToChunkHalo() {
IrisBiome parent = new IrisBiome();
AtomicInteger calls = new AtomicInteger();
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> {
calls.incrementAndGet();
return parent;
});
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
sampler.edgeFade(parent, x, z);
}
}
int rawWidth = 16 + (FloatingIslandBoundarySampler.EDGE_FADE_RADIUS * 2) + 2;
assertEquals(rawWidth * rawWidth, calls.get());
}
@Test
public void footprintFade_usesDistanceForSaturatedHalfPlane() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(12)) {
@Override
public double noise(double x, double z) {
return x >= 0 ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
int[] depths = new int[FloatingIslandBoundarySampler.EDGE_TAPER_WIDTH];
assertFalse(sampler.footprint(footprint, 0, 0, 0.0D).accepted());
for (int distance = 2; distance <= FloatingIslandBoundarySampler.EDGE_FADE_RADIUS; distance++) {
FloatingIslandBoundarySampler.FootprintSample sample = sampler.footprint(footprint, distance - 1, 0, 0.0D);
assertTrue(sample.accepted());
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(distance), sample.edgeFade(), 0.0D);
depths[distance - 2] = FloatingIslandSample.roundedEdgeDepth(20, 20, 0.0D, sample.edgeFade());
FloatingIslandSample.NeighborSupport support = new FloatingIslandSample.NeighborSupport(sample.cardinalSupport(), sample.diagonalSupport());
assertEquals(distance == FloatingIslandBoundarySampler.EDGE_FADE_RADIUS,
FloatingIslandSample.canCarveLaterally(sample.edgeFade(), support));
}
assertArrayEquals(new int[]{1, 2, 4, 7, 10, 13, 16, 18, 19, 20}, depths);
for (int i = 1; i < depths.length; i++) {
assertTrue(depths[i] >= depths[i - 1]);
assertTrue(depths[i] - depths[i - 1] <= 3);
}
}
@Test
public void footprint_deepSingleCellPinholeRemainsSolid() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(13)) {
@Override
public double noise(double x, double z) {
return x == 0.0D && z == 0.0D ? 0.0D : 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler.FootprintSample sample = sampler.footprint(footprint, 0, 0, 0.0D);
assertTrue(sample.accepted());
assertEquals(1.0D, sample.edgeFade(), 0.0D);
}
@Test
public void footprint_repairsOneByTwoAndDiagonalStaticGaps() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG adjacent = new CNG(new RNG(27)) {
@Override
public double noise(double x, double z) {
return z == 0.0D && (x == 0.0D || x == 1.0D) ? 0.0D : 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
CNG diagonal = new CNG(new RNG(28)) {
@Override
public double noise(double x, double z) {
boolean gap = x == 0.0D && z == 0.0D || x == 1.0D && z == 1.0D;
return gap ? 0.0D : 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertTrue(sampler.footprint(adjacent, 0, 0, 0.0D).accepted());
assertTrue(sampler.footprint(adjacent, 1, 0, 0.0D).accepted());
assertTrue(sampler.footprint(diagonal, 0, 0, 0.0D).accepted());
assertTrue(sampler.footprint(diagonal, 1, 1, 0.0D).accepted());
}
@Test
public void footprint_broadOpeningRemainsOpen() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(14)) {
@Override
public double noise(double x, double z) {
return Math.abs(x) <= 1.0D && Math.abs(z) <= 1.0D ? 0.0D : 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertFalse(sampler.footprint(footprint, 0, 0, 0.0D).accepted());
}
@Test
public void footprint_buildsOneBoundedChunkHalo() {
AtomicInteger calls = new AtomicInteger();
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(15)) {
@Override
public double noise(double x, double z) {
calls.incrementAndGet();
return 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
sampler.footprint(footprint, x, z, 0.0D);
}
}
int rawWidth = 16 + (FloatingIslandBoundarySampler.EDGE_FADE_RADIUS * 2) + 2;
assertEquals(rawWidth * rawWidth, calls.get());
}
@Test
public void footprintFade_isStableAcrossChunkAlignedFields() {
CNG footprint = new CNG(new RNG(16)) {
@Override
public double noise(double x, double z) {
return z >= 0.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler forwardSampler = new FloatingIslandBoundarySampler((x, z) -> null);
FloatingIslandBoundarySampler reverseSampler = new FloatingIslandBoundarySampler((x, z) -> null);
for (int distance = 2; distance <= FloatingIslandBoundarySampler.EDGE_FADE_RADIUS; distance++) {
double expected = FloatingIslandBoundarySampler.edgeFadeForDistance(distance);
double forwardLeft = forwardSampler.footprint(footprint, 15, distance - 1, 0.0D).edgeFade();
double forwardRight = forwardSampler.footprint(footprint, 16, distance - 1, 0.0D).edgeFade();
double forwardNegative = forwardSampler.footprint(footprint, -1, distance - 1, 0.0D).edgeFade();
double forwardOrigin = forwardSampler.footprint(footprint, 0, distance - 1, 0.0D).edgeFade();
double reverseRight = reverseSampler.footprint(footprint, 16, distance - 1, 0.0D).edgeFade();
double reverseLeft = reverseSampler.footprint(footprint, 15, distance - 1, 0.0D).edgeFade();
assertEquals(expected, forwardLeft, 0.0D);
assertEquals(expected, forwardRight, 0.0D);
assertEquals(expected, forwardNegative, 0.0D);
assertEquals(expected, forwardOrigin, 0.0D);
assertEquals(forwardLeft, reverseLeft, 0.0D);
assertEquals(forwardRight, reverseRight, 0.0D);
}
}
@Test
public void footprint_rejectsTwoColumnStaticFragment() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(17)) {
@Override
public double noise(double x, double z) {
return z == 0.0D && (x == 0.0D || x == 1.0D) ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertFalse(sampler.footprint(footprint, 0, 0, 0.0D).accepted());
assertFalse(sampler.footprint(footprint, 1, 0, 0.0D).accepted());
}
@Test
public void footprint_rejectsComponentWithoutThreeCellDeepCore() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG threeByThree = new CNG(new RNG(21)) {
@Override
public double noise(double x, double z) {
return Math.abs(x) <= 1.0D && Math.abs(z) <= 1.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
CNG fourWideTendril = new CNG(new RNG(22)) {
@Override
public double noise(double x, double z) {
return x >= 0.0D && x <= 3.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertFalse(sampler.footprint(threeByThree, 0, 0, 0.0D).accepted());
assertFalse(sampler.footprint(fourWideTendril, 1, 0, 0.0D).accepted());
assertFalse(sampler.footprint(fourWideTendril, 2, 0, 0.0D).accepted());
}
@Test
public void footprint_acceptsComponentWithThreeCellDeepCore() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(23)) {
@Override
public double noise(double x, double z) {
return Math.abs(x) <= 2.0D && Math.abs(z) <= 2.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler.FootprintSample sample = sampler.footprint(footprint, 0, 0, 0.0D);
assertTrue(sample.accepted());
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(3), sample.edgeFade(), 0.0D);
}
@Test
public void footprint_trimsAttachedThinArmWithoutChunkSeam() {
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
CNG footprint = new CNG(new RNG(25)) {
@Override
public double noise(double x, double z) {
boolean arm = Math.abs(z) <= 1.0D;
boolean core = x >= -10.0D && x <= -6.0D && Math.abs(z) <= 2.0D;
return arm || core ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
assertTrue(sampler.footprint(footprint, -8, 0, 0.0D).accepted());
assertFalse(sampler.footprint(footprint, 15, 0, 0.0D).accepted());
assertFalse(sampler.footprint(footprint, 16, 0, 0.0D).accepted());
}
@Test
public void ownershipFade_tapersAcrossPickerBoundary() {
IrisFloatingChildBiomes left = new IrisFloatingChildBiomes().setRarity(1);
IrisFloatingChildBiomes right = new IrisFloatingChildBiomes().setRarity(1);
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(left);
entries.add(right);
CNG picker = new CNG(new RNG(18)) {
@Override
public double noise(double x, double z) {
return x < 0.0D ? 0.0D : 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
assertSame(right, sampler.ownership(entries, picker, 0, 0).owner());
assertEquals(0.0D, sampler.ownership(entries, picker, 0, 0).edgeFade(), 0.0D);
for (int distance = 2; distance <= FloatingIslandBoundarySampler.EDGE_FADE_RADIUS; distance++) {
FloatingIslandBoundarySampler.OwnershipSample sample = sampler.ownership(entries, picker, distance - 1, 0);
assertSame(right, sample.owner());
assertEquals(FloatingIslandBoundarySampler.edgeFadeForDistance(distance), sample.edgeFade(), 0.0D);
}
}
@Test
public void ownershipFade_rejectsThinPickerRegion() {
IrisFloatingChildBiomes left = new IrisFloatingChildBiomes().setRarity(1);
IrisFloatingChildBiomes right = new IrisFloatingChildBiomes().setRarity(1);
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(left);
entries.add(right);
CNG picker = new CNG(new RNG(24)) {
@Override
public double noise(double x, double z) {
return Math.abs(x) <= 1.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> null);
FloatingIslandBoundarySampler.OwnershipSample sample = sampler.ownership(entries, picker, 0, 0);
assertSame(right, sample.owner());
assertEquals(0.0D, sample.edgeFade(), 0.0D);
}
@Test
public void labelFieldsTrimAttachedThinArmWithoutChunkSeam() {
IrisBiome leftParent = new IrisBiome();
leftParent.setLoadKey("left");
IrisBiome rightParent = new IrisBiome();
rightParent.setLoadKey("right");
IrisFloatingChildBiomes leftChild = new IrisFloatingChildBiomes().setRarity(1);
IrisFloatingChildBiomes rightChild = new IrisFloatingChildBiomes().setRarity(1);
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(leftChild);
entries.add(rightChild);
CNG picker = new CNG(new RNG(29)) {
@Override
public double noise(double x, double z) {
return isAttachedArm(x, z) ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler(
(x, z) -> isAttachedArm(x, z) ? rightParent : leftParent);
assertTrue(sampler.edgeFade(rightParent, -8, 0) > 0.0D);
assertEquals(0.0D, sampler.edgeFade(rightParent, 15, 0), 0.0D);
assertEquals(0.0D, sampler.edgeFade(rightParent, 16, 0), 0.0D);
assertSame(rightChild, sampler.ownership(entries, picker, 15, 0).owner());
assertSame(rightChild, sampler.ownership(entries, picker, 16, 0).owner());
assertEquals(0.0D, sampler.ownership(entries, picker, 15, 0).edgeFade(), 0.0D);
assertEquals(0.0D, sampler.ownership(entries, picker, 16, 0).edgeFade(), 0.0D);
}
@Test
public void sampler_fieldsPublishSafelyAcrossConcurrentReaders() throws Exception {
IrisBiome parent = new IrisBiome();
parent.setLoadKey("parent");
IrisFloatingChildBiomes left = new IrisFloatingChildBiomes().setRarity(1);
IrisFloatingChildBiomes right = new IrisFloatingChildBiomes().setRarity(1);
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(left);
entries.add(right);
CNG constant = new CNG(new RNG(26)) {
@Override
public double noise(double x, double z) {
return 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return 1.0D;
}
};
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> parent);
ExecutorService executor = Executors.newFixedThreadPool(8);
KList<Future<Boolean>> futures = new KList<>();
try {
for (int task = 0; task < 64; task++) {
int x = (task % 4) * 16;
futures.add(executor.submit(() -> sampler.parent(x, 0) == parent
&& sampler.edgeFade(parent, x, 0) == 1.0D
&& sampler.footprint(constant, x, 0, 0.0D).accepted()
&& sampler.footprint(constant, x, 0, 0.0D).edgeFade() == 1.0D
&& sampler.ownership(entries, constant, x, 0).owner() == right
&& sampler.ownership(entries, constant, x, 0).edgeFade() == 1.0D));
}
for (Future<Boolean> future : futures) {
assertTrue(future.get(10, TimeUnit.SECONDS));
}
} finally {
executor.shutdownNow();
}
}
@Test
public void sample_appliesSpatialProfileToFinalWorldCoordinates() {
CNG footprint = new CNG(new RNG(19)) {
@Override
public double noise(double x, double z) {
return x >= 0.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
CNG constant = new CNG(new RNG(20)) {
@Override
public double noise(double x, double z) {
return 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return 1.0D;
}
};
IrisFloatingChildBiomes entry = mock(IrisFloatingChildBiomes.class);
IrisBiome parent = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
Engine engine = mock(Engine.class);
IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(320).build();
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(entry);
when(parent.getFloatingChildBiomes()).thenReturn(entries);
when(parent.isMergeFloatingChildBiomes()).thenReturn(false);
when(entry.getFootprintCng(anyLong(), same(data))).thenReturn(footprint);
when(entry.getFootprintThreshold()).thenReturn(0.5D);
when(entry.getAltitudeCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getMinHeightAboveSurface()).thenReturn(160);
when(entry.getMaxHeightAboveSurface()).thenReturn(160);
when(entry.getRealBiome(parent, data)).thenReturn(parent);
when(entry.getTopShapeMode()).thenReturn(TopShapeMode.NOISE);
when(entry.getTopShapeCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getTopShapeAmp()).thenReturn(1.0D);
when(entry.getMaxTopHeight()).thenReturn(18);
when(entry.getBottomCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getBottomExponent()).thenReturn(1.0D);
when(entry.getBottomDepthMin()).thenReturn(20);
when(entry.getBottomDepthMax()).thenReturn(20);
when(entry.getMaxThickness()).thenReturn(64);
when(entry.getMinAbsoluteY()).thenReturn(null);
when(entry.getMaxAbsoluteY()).thenReturn(null);
when(entry.getWallWarpCng(anyLong(), same(data))).thenReturn(null);
when(entry.getWallWarpAmplitude()).thenReturn(0.0D);
when(entry.getCarvingProfileSampler(engine, data)).thenReturn(null);
when(entry.hasCarvingReference()).thenReturn(false);
when(entry.getCarveCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getCarveThreshold()).thenReturn(0.5D);
when(engine.getWorld()).thenReturn(world);
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> parent);
int[] expectedBottom = {159, 158, 156, 153, 150, 147, 144, 142, 141, 140};
int[] expectedTop = {161, 162, 164, 166, 169, 172, 174, 176, 177, 178};
assertNull(FloatingIslandSample.sample(parent, 0, 0, 320, 31L, data, engine, sampler));
for (int i = 0; i < expectedBottom.length; i++) {
FloatingIslandSample sample = FloatingIslandSample.sample(parent, i + 1, 0, 320, 31L, data, engine, sampler);
assertNotNull("distance " + (i + 2) + " rejected with code " + FloatingIslandSample.getLastReject(), sample);
assertEquals(expectedBottom[i], sample.bottomY());
assertEquals(expectedTop[i], sample.topY());
if (i < expectedBottom.length - 1) {
assertEquals(sample.thickness, sample.solidCount);
} else {
assertTrue(sample.solidCount < sample.thickness);
}
if (i > 0) {
assertTrue(Math.abs(expectedBottom[i] - expectedBottom[i - 1]) <= 3);
assertTrue(Math.abs(expectedTop[i] - expectedTop[i - 1]) <= 3);
}
}
}
@Test
public void sample_mergesNoiseTopEntriesAcrossSpatialRim() {
CNG footprint = new CNG(new RNG(33)) {
@Override
public double noise(double x, double z) {
return x >= 0.0D ? 1.0D : 0.0D;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
CNG constant = new CNG(new RNG(34)) {
@Override
public double noise(double x, double z) {
return 1.0D;
}
@Override
public double noise(double x, double y, double z) {
return 1.0D;
}
};
IrisFloatingChildBiomes lower = mock(IrisFloatingChildBiomes.class);
IrisFloatingChildBiomes upper = mock(IrisFloatingChildBiomes.class);
IrisBiome parent = mock(IrisBiome.class);
IrisData data = mock(IrisData.class);
Engine engine = mock(Engine.class);
IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(320).build();
KList<IrisFloatingChildBiomes> entries = new KList<>();
entries.add(lower);
entries.add(upper);
when(parent.getFloatingChildBiomes()).thenReturn(entries);
when(parent.isMergeFloatingChildBiomes()).thenReturn(true);
for (IrisFloatingChildBiomes entry : entries) {
when(entry.getFootprintCng(anyLong(), same(data))).thenReturn(footprint);
when(entry.getFootprintThreshold()).thenReturn(0.5D);
when(entry.getAltitudeCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getRealBiome(parent, data)).thenReturn(parent);
when(entry.getTopShapeMode()).thenReturn(TopShapeMode.NOISE);
when(entry.getTopShapeCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getTopShapeAmp()).thenReturn(1.0D);
when(entry.getMaxTopHeight()).thenReturn(18);
when(entry.getBottomCng(anyLong(), same(data))).thenReturn(constant);
when(entry.getBottomExponent()).thenReturn(1.0D);
when(entry.getBottomDepthMin()).thenReturn(20);
when(entry.getBottomDepthMax()).thenReturn(20);
when(entry.getMaxThickness()).thenReturn(64);
when(entry.getMinAbsoluteY()).thenReturn(null);
when(entry.getMaxAbsoluteY()).thenReturn(null);
when(entry.getWallWarpCng(anyLong(), same(data))).thenReturn(null);
when(entry.getWallWarpAmplitude()).thenReturn(0.0D);
when(entry.getCarvingProfileSampler(engine, data)).thenReturn(null);
when(entry.hasCarvingReference()).thenReturn(false);
when(entry.getCarveCng(anyLong(), same(data))).thenReturn(null);
when(entry.getCarveThreshold()).thenReturn(1.0D);
}
when(lower.getMinHeightAboveSurface()).thenReturn(160);
when(lower.getMaxHeightAboveSurface()).thenReturn(160);
when(upper.getMinHeightAboveSurface()).thenReturn(180);
when(upper.getMaxHeightAboveSurface()).thenReturn(180);
when(engine.getWorld()).thenReturn(world);
FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> parent);
assertNull(FloatingIslandSample.sample(parent, 0, 0, 320, 35L, data, engine, sampler));
FloatingIslandSample sample = FloatingIslandSample.sample(parent, 1, 0, 320, 35L, data, engine, sampler);
assertNotNull(sample);
assertTrue(sample.hasMergedEntries());
assertEquals(159, sample.bottomY());
assertEquals(181, sample.topY());
assertEquals(23, sample.thickness);
assertEquals(6, sample.solidCount);
assertSame(upper, sample.entryAt(sample.topIdx));
}
private static boolean isAttachedArm(double x, double z) {
return Math.abs(z) <= 1.0D || x >= -10.0D && x <= -6.0D && Math.abs(z) <= 2.0D;
}
}
@@ -110,27 +110,28 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(15, FloatingIslandSample.roundedEdgeDepth(10, 20, 0.5, 1.0));
}
@Test
public void roundedEdgeDepth_fixedDepthFollowsOnlyFootprintFade() {
assertEquals(0, FloatingIslandSample.roundedEdgeDepth(20, 20, 0.0, 0.0));
assertEquals(5, FloatingIslandSample.roundedEdgeDepth(20, 20, 0.0, 0.25));
assertEquals(10, FloatingIslandSample.roundedEdgeDepth(20, 20, 1.0, 0.5));
assertEquals(20, FloatingIslandSample.roundedEdgeDepth(20, 20, 0.0, 1.0));
}
@Test
public void effectiveWallWarpAmplitude_fadesWarpOutAtRoundedRim() {
assertEquals(0.0, FloatingIslandSample.effectiveWallWarpAmplitude(5.0, 0.0), 0.0);
assertEquals(2.5, FloatingIslandSample.effectiveWallWarpAmplitude(5.0, 0.5), 0.0);
assertEquals(5.0, FloatingIslandSample.effectiveWallWarpAmplitude(5.0, 1.0), 0.0);
}
@Test
public void carveSolidInterior_preservesOuterShell() {
boolean[] mask = {false, true, true, true, true, false};
CNG carve = new CNG(new RNG(1), new NoiseGenerator() {
@Override
public double noise(double x) {
return 1.0;
}
boolean[] lateralCarveMask = {true, true, true, true, true, true};
CNG carve = constantCng(1.0);
@Override
public double noise(double x, double z) {
return 1.0;
}
@Override
public double noise(double x, double y, double z) {
return 1.0;
}
}, 1.0, 1);
int count = FloatingIslandSample.carveSolidInterior(mask, 100, 0, 0, carve, 0.5);
int count = FloatingIslandSample.carveSolidInterior(mask, lateralCarveMask, 100, 0, 0, carve, 0.5);
assertEquals(2, count);
assertEquals(false, mask[0]);
@@ -141,6 +142,41 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(false, mask[5]);
}
@Test
public void carveSolidInterior_preservesLateralPerimeter() {
boolean[] mask = {true, true, true, true, true, true};
boolean[] lateralCarveMask = {false, false, false, false, false, false};
int count = FloatingIslandSample.carveSolidInterior(mask, lateralCarveMask, 100, 0, 0, constantCng(1.0), 0.5);
assertEquals(6, count);
for (boolean solid : mask) {
assertTrue(solid);
}
}
@Test
public void lateralCarving_requiresRoundedInteriorAndFullNeighborRing() {
CNG fullRing = constantCng(1.0);
FloatingIslandSample.NeighborSupport fullSupport = FloatingIslandSample.footprintNeighborSupport(fullRing, 0, 0, 0.0);
CNG missingDiagonal = new CNG(new RNG(11)) {
@Override
public double noise(double x, double z) {
return x == 1 && z == 1 ? 0.0 : 1.0;
}
@Override
public double noise(double x, double y, double z) {
return noise(x, z);
}
};
FloatingIslandSample.NeighborSupport partialSupport = FloatingIslandSample.footprintNeighborSupport(missingDiagonal, 0, 0, 0.0);
assertTrue(FloatingIslandSample.canCarveLaterally(1.0, fullSupport));
assertFalse(FloatingIslandSample.canCarveLaterally(0.99, fullSupport));
assertFalse(FloatingIslandSample.canCarveLaterally(1.0, partialSupport));
}
@Test
public void footprintPinholeFill_acceptsSingleColumnHoleWithSolidRing() {
CNG footprint = new CNG(new RNG(4)) {
@@ -156,7 +192,7 @@ public class FloatingIslandSampleBottomYTest {
};
FloatingIslandSample.NeighborSupport support = FloatingIslandSample.footprintNeighborSupport(footprint, 0, 0, 0.0);
assertTrue(FloatingIslandSample.isFootprintPinholeRepairable(-0.08, 0.0, support));
assertTrue(FloatingIslandSample.isFootprintPinholeRepairable(support));
}
@Test
@@ -174,7 +210,7 @@ public class FloatingIslandSampleBottomYTest {
};
FloatingIslandSample.NeighborSupport support = FloatingIslandSample.footprintNeighborSupport(footprint, 0, 0, 0.0);
assertFalse(FloatingIslandSample.isFootprintPinholeRepairable(-1.0, 0.0, support));
assertFalse(FloatingIslandSample.isFootprintPinholeRepairable(support));
}
@Test
@@ -233,7 +269,8 @@ public class FloatingIslandSampleBottomYTest {
}
}, 1.0, 1);
int count = FloatingIslandSample.carveSolidInterior(mask, 100, 0, 0, carve, 0.5);
boolean[] lateralCarveMask = {true, true, true, true, true};
int count = FloatingIslandSample.carveSolidInterior(mask, lateralCarveMask, 100, 0, 0, carve, 0.5);
assertEquals(5, count);
assertEquals(true, mask[1]);
@@ -244,27 +281,14 @@ public class FloatingIslandSampleBottomYTest {
@Test
public void carveSolidInterior_capsBroadVerticalCarveSheet() {
boolean[] mask = new boolean[30];
boolean[] lateralCarveMask = new boolean[30];
for (int i = 0; i < mask.length; i++) {
mask[i] = true;
lateralCarveMask[i] = true;
}
CNG carve = new CNG(new RNG(10), new NoiseGenerator() {
@Override
public double noise(double x) {
return 1.0;
}
CNG carve = constantCng(1.0);
@Override
public double noise(double x, double z) {
return 1.0;
}
@Override
public double noise(double x, double y, double z) {
return 1.0;
}
}, 1.0, 1);
int count = FloatingIslandSample.carveSolidInterior(mask, 100, 0, 0, carve, 0.5);
int count = FloatingIslandSample.carveSolidInterior(mask, lateralCarveMask, 100, 0, 0, carve, 0.5);
assertEquals(25, count);
for (int i = 0; i < FloatingIslandSample.carveShellThickness(mask.length); i++) {
@@ -341,4 +365,23 @@ public class FloatingIslandSampleBottomYTest {
assertEquals(true, FloatingIslandSample.hasFootprintNeighborSupport(footprint, 0, 0, 0.0));
}
private CNG constantCng(double value) {
return new CNG(new RNG(12), new NoiseGenerator() {
@Override
public double noise(double x) {
return value;
}
@Override
public double noise(double x, double z) {
return value;
}
@Override
public double noise(double x, double y, double z) {
return value;
}
}, 1.0, 1);
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.engine.object;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertNull;
public class IrisEntitySpawnTest {
@Test
public void caveSpawnWithoutSafeMarkerIsSkipped() {
assertNull(IrisEntitySpawn.selectCaveSpawnLocation(new KList<>(), null, new RNG(1L)));
}
}
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisImportedStructureControlTest {
@@ -37,15 +38,17 @@ public class IrisImportedStructureControlTest {
}
@Test
public void defaultModeGeneratesEveryVanillaStructure() {
public void defaultPolicyGeneratesEveryRegisteredStructureKey() {
IrisImportedStructureControl control = new IrisImportedStructureControl();
assertTrue(control.shouldGenerate("minecraft:village_plains"));
assertTrue(control.shouldGenerate("minecraft:stronghold"));
assertTrue(control.shouldGenerate("minecraft:ocean_monument"));
assertTrue(control.shouldGenerate("minecraft:monument"));
assertEquals(NativeStructureGenerationStatus.GENERATE_NATIVE,
control.resolve("minecraft:monument", false).status());
}
@Test
public void defaultModeGeneratesDatapackStructures() {
public void defaultPolicyGeneratesDatapackStructures() {
IrisImportedStructureControl control = new IrisImportedStructureControl();
assertTrue(control.isDatapackOverrides());
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
@@ -69,26 +72,6 @@ public class IrisImportedStructureControlTest {
assertTrue(control.shouldGenerate("minecraft:stronghold"));
}
@Test
public void allOffWhitelistMatchesExactKey() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF)
.setEnabled(keys("minecraft:village_plains"));
assertTrue(control.shouldGenerate("minecraft:village_plains"));
assertFalse(control.shouldGenerate("minecraft:village_taiga"));
assertFalse(control.shouldGenerate("minecraft:stronghold"));
}
@Test
public void allOffWhitelistMatchesNamespacePrefix() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF)
.setEnabled(keys("minecraft:village"));
assertTrue(control.shouldGenerate("minecraft:village_taiga"));
assertTrue(control.shouldGenerate("minecraft:village_desert"));
assertFalse(control.shouldGenerate("minecraft:stronghold"));
}
@Test
public void datapackOverridesFalseDoesNotDisableModOrDatapackNamespaces() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
@@ -99,10 +82,9 @@ public class IrisImportedStructureControlTest {
}
@Test
public void allOffWhitelistStillControlsThirdPartyStructuresWhenOverridesAreFalse() {
public void disabledListControlsThirdPartyStructuresWhenOverridesAreFalse() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF)
.setEnabled(keys("nova_structures:desert_temple", "minecraft:village_plains"))
.setDisabled(keys("aquaculture:treasure_vault"))
.setDatapackOverrides(false);
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
assertTrue(control.shouldGenerate("minecraft:village_plains"));
@@ -110,7 +92,7 @@ public class IrisImportedStructureControlTest {
}
@Test
public void datapackOverridesTrueAllowsDatapackKeysWhileAllOn() {
public void datapackOverridesTrueAllowsDatapackKeysByDefault() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setDatapackOverrides(true);
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
@@ -130,26 +112,6 @@ public class IrisImportedStructureControlTest {
assertTrue(control.shouldGenerate("minecraft:ancient_city"));
}
@Test
public void activeTrueWhenAllOn() {
assertTrue(new IrisImportedStructureControl().active());
}
@Test
public void activeFalseWhenAllOffWithoutWhitelist() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF);
assertFalse(control.active());
}
@Test
public void activeTrueWhenAllOffWithWhitelist() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF)
.setEnabled(keys("minecraft:village_plains"));
assertTrue(control.active());
}
@Test
public void emptyOrNullListEntriesNeverMatchEveryKey() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
@@ -159,11 +121,11 @@ public class IrisImportedStructureControlTest {
}
@Test
public void nullKeyIsNullSafeAndNotWhitelisted() {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setMode(VanillaStructureMode.ALL_OFF)
.setEnabled(keys("minecraft:village_plains"));
public void nullKeyIsRejectedAsAnInvalidRegistryKey() {
IrisImportedStructureControl control = new IrisImportedStructureControl();
assertFalse(control.shouldGenerate(null));
assertEquals(NativeStructureGenerationStatus.INVALID_REGISTRY_KEY,
control.resolve(null, false).status());
}
@Test
@@ -205,7 +167,7 @@ public class IrisImportedStructureControlTest {
@Test
public void postprocessingUsesPrefixMatches() {
IrisVanillaStructureStiltSettings stilt = new IrisVanillaStructureStiltSettings();
IrisStructureStiltSettings stilt = new IrisStructureStiltSettings();
IrisVanillaStructureAdjustment adjustment = new IrisVanillaStructureAdjustment()
.setMatch(keys("minecraft:village"))
.setClearVegetation(true)
@@ -221,8 +183,8 @@ public class IrisImportedStructureControlTest {
@Test
public void multipleMatchesMergeVegetationAndUseLastConfiguredStilt() {
IrisVanillaStructureStiltSettings broadStilt = new IrisVanillaStructureStiltSettings().setMaxDepth(32);
IrisVanillaStructureStiltSettings specificStilt = new IrisVanillaStructureStiltSettings().setMaxDepth(96);
IrisStructureStiltSettings broadStilt = new IrisStructureStiltSettings().setMaxDepth(32);
IrisStructureStiltSettings specificStilt = new IrisStructureStiltSettings().setMaxDepth(96);
IrisVanillaStructureAdjustment broad = new IrisVanillaStructureAdjustment()
.setMatch(keys("minecraft:village"))
.setClearVegetation(true)
@@ -262,4 +224,18 @@ public class IrisImportedStructureControlTest {
assertFalse(control.shouldGenerate("example:castle"));
assertTrue(control.shouldGenerate("other:castle"));
}
@Test
public void malformedNullPolicyListsFailWithTheirExactField() {
IrisImportedStructureControl nullDisabled = new IrisImportedStructureControl().setDisabled(null);
IrisImportedStructureControl nullAdjustments = new IrisImportedStructureControl().setAdjustments(null);
NullPointerException disabled = assertThrows(NullPointerException.class,
() -> nullDisabled.shouldGenerate("minecraft:monument"));
NullPointerException adjustments = assertThrows(NullPointerException.class,
() -> nullAdjustments.resolve("minecraft:monument", false));
assertTrue(disabled.getMessage().contains("importedStructures.disabled"));
assertTrue(adjustments.getMessage().contains("importedStructures.adjustments"));
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisObjectStructurePieceAirTest {
@Test
public void explicitAirPlacesOnlyForRawStructurePieces() {
assertTrue(IrisObject.shouldPlaceObjectBlock(true, true, false));
assertFalse(IrisObject.shouldPlaceObjectBlock(false, true, false));
}
@Test
public void ordinaryObjectAirRemainsNondestructive() {
assertFalse(IrisObject.shouldPlaceObjectBlock(false, true, false));
assertTrue(IrisObject.shouldPlaceObjectBlock(false, false, false));
}
@Test
public void vineReplacementRemainsRejectedForEveryPlacementMode() {
assertFalse(IrisObject.shouldPlaceObjectBlock(false, false, true));
assertFalse(IrisObject.shouldPlaceObjectBlock(true, false, true));
assertFalse(IrisObject.shouldPlaceObjectBlock(true, true, true));
}
}
@@ -9,10 +9,10 @@ import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class IrisVanillaStructureStiltSettingsTest {
public class IrisStructureStiltSettingsTest {
@Test
public void defaultsToCobblestoneFoundation() {
IrisVanillaStructureStiltSettings settings = new IrisVanillaStructureStiltSettings();
IrisStructureStiltSettings settings = new IrisStructureStiltSettings();
assertEquals(64, settings.getMaxDepth());
assertNotNull(settings.getPalette());
@@ -22,7 +22,7 @@ public class IrisVanillaStructureStiltSettingsTest {
@Test
public void maxDepthSchemaIsBoundedToTheMaximumWorldHeight() throws NoSuchFieldException {
Field maxDepth = IrisVanillaStructureStiltSettings.class.getDeclaredField("maxDepth");
Field maxDepth = IrisStructureStiltSettings.class.getDeclaredField("maxDepth");
MinNumber minimum = maxDepth.getAnnotation(MinNumber.class);
MaxNumber maximum = maxDepth.getAnnotation(MaxNumber.class);
@@ -19,7 +19,7 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer placer = IslandObjectPlacer.bottom(null, sampleProvider(samples), null, 100);
int result = placer.getHighest(0, 0, null);
assertEquals(100, result);
@@ -30,7 +30,7 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer placer = IslandObjectPlacer.bottom(null, sampleProvider(samples), null, 100);
int result = placer.getHighest(15, 15, null);
assertEquals(100, result);
@@ -41,7 +41,7 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer placer = IslandObjectPlacer.bottom(null, sampleProvider(samples), null, 100);
assertEquals(false, placer.canWriteObjectBlock(0, 101, 0));
placer.set(0, 101, 0, null);
@@ -52,7 +52,7 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer placer = IslandObjectPlacer.bottom(null, sampleProvider(samples), null, 100);
assertEquals(true, placer.canWriteObjectBlock(0, 99, 0));
}
@@ -62,7 +62,7 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 100, IslandObjectPlacer.AnchorFace.BOTTOM);
IslandObjectPlacer placer = IslandObjectPlacer.bottom(null, sampleProvider(samples), null, 100);
assertEquals(false, placer.canWriteObjectBlock(0, 100, 0));
assertEquals(false, placer.canWriteObjectBlock(0, 101, 0));
@@ -73,9 +73,36 @@ public class IslandObjectPlacerAnchorFaceTest {
FloatingIslandSample[] samples = new FloatingIslandSample[256];
samples[0] = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = new IslandObjectPlacer(null, samples, 0, 0, 105);
IslandObjectPlacer placer = IslandObjectPlacer.top(null, sampleProvider(samples), null, 105);
assertEquals(false, placer.canWriteObjectBlock(1, 104, 0));
placer.set(1, 104, 0, null);
}
@Test
public void topFace_allowsSupportedNeighborChunkWrite() {
FloatingIslandSample sample = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = IslandObjectPlacer.top(null,
(x, z) -> z == 0 && (x == 15 || x == 16) ? sample : null, null, 109);
assertEquals(true, placer.canWriteObjectBlock(16, 110, 0));
}
@Test
public void topFace_rejectsUnsupportedWorldColumnBeyondOverhang() {
FloatingIslandSample sample = sampleWithBottomAt(100, 0);
IslandObjectPlacer placer = IslandObjectPlacer.top(null,
(x, z) -> x == 15 && z == 0 ? sample : null, null, 109);
assertEquals(false, placer.canWriteObjectBlock(18, 110, 0));
}
private IslandObjectPlacer.SampleProvider sampleProvider(FloatingIslandSample[] samples) {
return (x, z) -> {
if (x < 0 || x >= 16 || z < 0 || z >= 16) {
return null;
}
return samples[(z << 4) | x];
};
}
}
@@ -0,0 +1,42 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class PlatformStateHookContractTest {
@Test
public void missingPlatformRotatorFailsLoudly() {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> IrisObjectRotation.requirePlatformRotator(null));
assertEquals("No platform block-state rotator is bound", error.getMessage());
}
@Test
public void missingPlatformMergerFailsLoudly() {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> BlockDataMergeSupport.requirePlatformMerger(null));
assertEquals("No platform block-state merger is bound", error.getMessage());
}
@Test
public void missingPlatformTileFactoryFailsLoudly() {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> TileData.requirePlatformFactory(null));
assertEquals("No platform tile-data factory is bound", error.getMessage());
}
@Test
public void missingPlatformTileReaderFailsLoudly() {
IOException error = assertThrows(IOException.class,
() -> TileData.requirePlatformReader(null));
assertEquals("No platform tile-data reader is bound", error.getMessage());
}
}
@@ -0,0 +1,172 @@
package art.arcane.iris.engine.platform;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Chest;
import org.bukkit.loot.LootTable;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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.Mockito.mock;
import static org.mockito.Mockito.when;
public class EngineBukkitOpsLootTest {
@Test
public void singleChestIsCanonical() {
Block block = chestBlock(4, 8, Chest.Type.SINGLE, BlockFace.NORTH);
assertTrue(EngineBukkitOps.isCanonicalContainer(block));
}
@Test
public void doubleChestHasExactlyOneCanonicalHalf() {
Block left = chestBlock(0, 0, Chest.Type.LEFT, BlockFace.NORTH);
Block right = chestBlock(1, 0, Chest.Type.RIGHT, BlockFace.NORTH);
assertTrue(EngineBukkitOps.isCanonicalContainer(left));
assertFalse(EngineBukkitOps.isCanonicalContainer(right));
}
@Test
public void nativeLootIsDetectedWithoutOpeningInventory() {
Block block = chestBlock(0, 0, Chest.Type.SINGLE, BlockFace.NORTH);
org.bukkit.block.Chest state = mock(org.bukkit.block.Chest.class);
LootTable table = mock(LootTable.class);
when(block.getState()).thenReturn(state);
when(state.getLootTable()).thenReturn(table);
assertTrue(EngineBukkitOps.hasNativeLootTable(block));
}
@Test
public void foliaLootMutationUsesOwningRegionOnly() {
World world = mock(World.class);
AtomicInteger primaryChecks = new AtomicInteger();
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
Runnable mutation = () -> mutated.set(true);
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 12, -3, mutation);
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
true,
() -> {
primaryChecks.incrementAndGet();
return true;
},
regionTask -> {
regionSchedules.incrementAndGet();
assertSame(world, regionTask.world());
assertEquals(12, regionTask.chunkX());
assertEquals(-3, regionTask.chunkZ());
assertSame(mutation, regionTask.mutation());
regionTask.mutation().run();
return true;
},
runnable -> syncSchedules.incrementAndGet()
);
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(1, regionSchedules.get());
assertEquals(0, primaryChecks.get());
assertEquals(0, syncSchedules.get());
}
@Test
public void failedFoliaRegionScheduleDoesNotMutateInventoryElsewhere() {
World world = mock(World.class);
AtomicInteger primaryChecks = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 1, 2, () -> mutated.set(true));
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
true,
() -> {
primaryChecks.incrementAndGet();
return true;
},
regionTask -> false,
runnable -> syncSchedules.incrementAndGet()
);
assertFalse(scheduled);
assertFalse(mutated.get());
assertEquals(0, primaryChecks.get());
assertEquals(0, syncSchedules.get());
}
@Test
public void nonFoliaPrimaryThreadMutatesInline() {
World world = mock(World.class);
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 4, 5, () -> mutated.set(true));
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
false,
() -> true,
regionTask -> {
regionSchedules.incrementAndGet();
return true;
},
runnable -> syncSchedules.incrementAndGet()
);
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(0, regionSchedules.get());
assertEquals(0, syncSchedules.get());
}
@Test
public void nonFoliaAsyncCompletionUsesSyncScheduler() {
World world = mock(World.class);
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 6, 7, () -> mutated.set(true));
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
false,
() -> false,
regionTask -> {
regionSchedules.incrementAndGet();
return true;
},
runnable -> {
syncSchedules.incrementAndGet();
runnable.run();
}
);
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(0, regionSchedules.get());
assertEquals(1, syncSchedules.get());
}
private Block chestBlock(int x, int z, Chest.Type type, BlockFace facing) {
Block block = mock(Block.class);
Chest chest = mock(Chest.class);
when(block.getX()).thenReturn(x);
when(block.getZ()).thenReturn(z);
when(block.getBlockData()).thenReturn(chest);
when(chest.getType()).thenReturn(type);
when(chest.getFacing()).thenReturn(facing);
return block;
}
}