mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Vwoop
This commit is contained in:
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");
|
||||
}
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
@@ -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"));
|
||||
}
|
||||
}
|
||||
+33
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+90
@@ -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"));
|
||||
}
|
||||
}
|
||||
+71
@@ -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);
|
||||
}
|
||||
}
|
||||
+544
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+251
@@ -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
|
||||
) {
|
||||
}
|
||||
}
|
||||
+132
@@ -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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+75
@@ -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));
|
||||
}
|
||||
}
|
||||
+140
@@ -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);
|
||||
}
|
||||
}
|
||||
+273
@@ -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) {
|
||||
}
|
||||
}
|
||||
+20
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-14
@@ -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);
|
||||
|
||||
+85
-18
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+196
@@ -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;
|
||||
}
|
||||
}
|
||||
+576
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -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();
|
||||
}
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
+100
@@ -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;
|
||||
}
|
||||
}
|
||||
+285
-34
@@ -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;
|
||||
|
||||
+68
@@ -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 + "'");
|
||||
}
|
||||
}
|
||||
+54
@@ -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");
|
||||
}
|
||||
}
|
||||
+103
@@ -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());
|
||||
}
|
||||
}
|
||||
+43
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
+225
@@ -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) {
|
||||
}
|
||||
}
|
||||
+81
@@ -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());
|
||||
}
|
||||
}
|
||||
+45
@@ -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));
|
||||
}
|
||||
}
|
||||
+56
@@ -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));
|
||||
}
|
||||
}
|
||||
+687
@@ -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;
|
||||
}
|
||||
}
|
||||
+80
-37
@@ -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)));
|
||||
}
|
||||
}
|
||||
+30
-54
@@ -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));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+33
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user