This commit is contained in:
Brian Neumann-Fopiano
2026-08-05 14:30:07 -06:00
parent a66cba6418
commit 70d355621c
26 changed files with 1719 additions and 35 deletions
@@ -3205,7 +3205,7 @@ public final class DatapackIngestService {
}
int transactionCount = 0;
for (Path transactionRoot : transactionRoots) {
if (isHarmlessTransactionArtifact(transactionRoot)) {
if (isHarmlessRecoveryArtifact(transactionRoot)) {
continue;
}
transactionCount++;
@@ -3214,7 +3214,7 @@ public final class DatapackIngestService {
}
}
for (Path transactionRoot : transactionRoots) {
if (isHarmlessTransactionArtifact(transactionRoot)) {
if (isHarmlessRecoveryArtifact(transactionRoot)) {
Files.deleteIfExists(transactionRoot);
continue;
}
@@ -3242,15 +3242,25 @@ public final class DatapackIngestService {
verifyDirectoryContainerIfPresent(scratchRoot.toFile(), "datapack install scratch");
List<Path> children;
try (Stream<Path> paths = Files.list(scratchRoot)) {
children = paths.limit(MAX_MANAGED_PATHS + 1L).sorted().toList();
children = paths.limit(MAX_MANAGED_PATHS + 2L).sorted().toList();
}
if (children.size() > MAX_MANAGED_PATHS) {
throw new IOException("Datapack install scratch contains too many entries");
int managedEntries = 0;
for (Path child : children) {
if (!isHarmlessRecoveryArtifact(child)) {
managedEntries++;
}
if (managedEntries > MAX_MANAGED_PATHS) {
throw new IOException("Datapack install scratch contains too many entries");
}
}
List<StagingScratch> pending = new ArrayList<>();
List<StagingScratch> backups = new ArrayList<>();
for (Path child : children) {
if (isHarmlessRecoveryArtifact(child)) {
Files.deleteIfExists(child);
continue;
}
StagingScratch scratch = parseInstallScratch(scratchRoot, child);
if (scratch == null) {
throw new IOException("Unexpected datapack install scratch artifact " + child);
@@ -3429,12 +3439,12 @@ public final class DatapackIngestService {
return ownership;
}
private static boolean isHarmlessTransactionArtifact(Path path) throws IOException {
private static boolean isHarmlessRecoveryArtifact(Path path) throws IOException {
if (!".DS_Store".equals(path.getFileName().toString())) {
return false;
}
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Suspicious datapack transaction artifact " + path);
throw new IOException("Suspicious datapack recovery artifact " + path);
}
return true;
}
@@ -117,6 +117,7 @@ final class PackStructurePlacementValidator {
registries.hooks(), blockingErrors);
continue;
}
validateEditableStructureTerrain(placementPath, placement, blockingErrors);
Set<String> editableStructureKeys = new HashSet<>();
for (int referenceIndex = 0; referenceIndex < references.length(); referenceIndex++) {
Object rawReference = references.opt(referenceIndex);
@@ -650,6 +651,20 @@ final class PackStructurePlacementValidator {
}
}
private static void validateEditableStructureTerrain(
String path, JSONObject placement, List<String> blockingErrors) {
JSONObject terrain = placement.optJSONObject("terrain");
if (terrain == null) {
return;
}
String mode = terrain.optString("mode", "SOURCE");
if ("VACUUM".equals(mode) || "ENCASE".equals(mode)) {
blockingErrors.add(path + ".terrain.mode " + mode
+ " cannot target editable Iris structures; use nativeStructures or "
+ "importedStructures.adjustments for native terrain preparation.");
}
}
static void validateStructureStartPools(File structuresFolder,
Set<String> poolKeys,
List<String> blockingErrors) {
@@ -118,6 +118,32 @@ public record NativeStructureOwnershipRecord(
&& chunkZ >= referenceMinChunkZ && chunkZ <= referenceMaxChunkZ;
}
public NativeStructureOwnershipRecord withReferenceEnvelope(
int referenceMinChunkX, int referenceMaxChunkX,
int referenceMinChunkZ, int referenceMaxChunkZ) {
return new NativeStructureOwnershipRecord(
schema,
structureKey,
originChunkX,
originChunkZ,
placementIdentity,
baseY,
contentMinX,
contentMinY,
contentMinZ,
contentMaxX,
contentMaxY,
contentMaxZ,
locatorY,
referenceMinChunkX,
referenceMaxChunkX,
referenceMinChunkZ,
referenceMaxChunkZ,
contentFingerprint,
decision
);
}
public OwnershipKey ownershipKey() {
return new OwnershipKey(structureKey, originChunkX, originChunkZ);
}
@@ -91,7 +91,7 @@ public class IrisStructurePlacement {
@Desc("If true, the structure starts at a deterministic random world Y inside [minHeight, maxHeight]. Terrain integration is then controlled independently by terrain.")
private boolean underground = false;
@Desc("Terrain integration shared by editable Iris assemblies and live registered structures.")
@Desc("Terrain integration for this placement. The editable structures backend supports SOURCE, PRESERVE, BORE, and FORCE_CARVE. The nativeStructures backend supports every terrain mode.")
private IrisStructureTerrain terrain = new IrisStructureTerrain();
@Desc("Optional foundation columns placed beneath the assembled structure's occupied bottom cells. Columns pass through air and fluids until they reach solid ground, up to maxDepth.")
@@ -22,12 +22,12 @@ public class IrisStructureTerrain {
private static final double MAX_EROSION_FREQUENCY = 1D;
private static final double MAX_LOBE_FREQUENCY = 1D;
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. FORCE_CARVE clears the requested envelope, while ENCASE fills it before placement so native shells are not lost to pre-carved air.")
@Desc("Terrain operation. SOURCE applies the registered native structure's authored terrain adaptation and is a no-op for editable Iris structures. PRESERVE disables terrain integration. VACUUM forces a 12-block surface bend to every rigid native piece base. BORE and FORCE_CARVE clear the requested envelope, while ENCASE fills it before placement so native shells are not lost to pre-carved air.")
private IrisStructureTerrainMode mode = IrisStructureTerrainMode.SOURCE;
@MinNumber(0)
@MaxNumber(128)
@Desc("Horizontal clearance around the assembled pieces.")
@Desc("Horizontal clearance around the assembled pieces for BORE, FORCE_CARVE, and ENCASE. VACUUM uses its fixed 12-block terrain falloff.")
private int horizontalPadding = 0;
@MinNumber(0)
@@ -8,6 +8,8 @@ public enum IrisStructureTerrainMode {
PRESERVE,
BORE,
FORCE_CARVE,
@Desc("Bends surface terrain to every rigid piece base with a 12-block falloff, even when the registered structure has no authored terrain adaptation. Terrain is raised or lowered rather than carved away.")
VACUUM,
@Desc("Fills the padded piece volume with solid blocks before any piece is placed so shells, walls, and floors land in solid ground instead of pre-carved air. Only air and liquid cells are filled; existing terrain and structures are never overwritten. Native pieces then carve their own interiors.")
@@ -57,7 +57,7 @@ public class IrisVanillaStructureAdjustment {
@Desc("Optional foundation columns placed beneath the native structure piece bases after placement.")
private IrisStructureStiltSettings stilt = null;
@Desc("Optional terrain integration override. FORCE_CARVE clears every intersecting chunk before native pieces are placed, ENCASE fills it with solid blocks instead. Left unset, SOURCE replays the registered structure's authored terrain adaptation, including surface fitting, burial, and encapsulation.")
@Desc("Optional terrain integration override. VACUUM forces surface terrain to bend to every rigid native piece base even when the structure did not author terrain adaptation. BORE and FORCE_CARVE clear every intersecting chunk before native pieces are placed, while ENCASE fills it with solid blocks instead. Left unset, SOURCE replays the registered structure's authored terrain adaptation, including surface fitting, burial, and encapsulation.")
private IrisStructureTerrain terrain = null;
public boolean matches(String key) {
@@ -1714,6 +1714,41 @@ public class DatapackIngestServiceTest {
assertFalse(scratch.exists());
}
@Test
public void recoveryRemovesFinderMetadataFromInstallScratch() throws Exception {
File root = temporaryFolder.newFolder("orphan-install-finder-metadata-root");
File scratch = new File(root, ".iris-datapack-install");
assertTrue(scratch.mkdirs());
File pending = new File(scratch, "managed-" + UUID.randomUUID());
assertTrue(pending.mkdirs());
Files.writeString(new File(pending, "partial.dat").toPath(), "partial", StandardCharsets.UTF_8);
File metadata = new File(scratch, ".DS_Store");
Files.writeString(metadata.toPath(), "finder", StandardCharsets.UTF_8);
DatapackIngestService.recoverTransactions(root, List.of());
assertFalse(metadata.exists());
assertFalse(pending.exists());
assertFalse(scratch.exists());
}
@Test
public void recoveryRejectsFinderMetadataDirectoryInInstallScratch() throws Exception {
File root = temporaryFolder.newFolder("orphan-install-finder-directory-root");
File scratch = new File(root, ".iris-datapack-install");
assertTrue(scratch.mkdirs());
File metadata = new File(scratch, ".DS_Store");
assertTrue(metadata.mkdirs());
try {
DatapackIngestService.recoverTransactions(root, List.of());
fail("Expected suspicious Finder metadata to block recovery");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("Suspicious datapack recovery artifact"));
}
assertTrue(metadata.isDirectory());
}
@Test
public void recoveryPreservesAndBlocksOnAnUnjournaledInstallBackup() throws Exception {
File root = temporaryFolder.newFolder("orphan-install-backup-root");
@@ -0,0 +1,111 @@
package art.arcane.iris.core.pack;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PackValidatorStructureTerrainBackendTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsNativeOnlyTerrainModesForEditablePlacementsAcrossEveryHost() throws Exception {
for (String mode : List.of("VACUUM", "ENCASE")) {
String folderName = "editable-" + mode.toLowerCase(Locale.ROOT);
File pack = temporaryFolder.newFolder(folderName);
writePlacement(pack, "dimensions/main.json", "dimension-" + folderName,
"dimension_city", false, mode);
writePlacement(pack, "regions/forest.json", "region-" + folderName,
"forest_tower", false, mode);
writePlacement(pack, "biomes/plains.json", "biome-" + folderName,
"plains_farm", false, mode);
List<String> errors = new ArrayList<>();
PackStructurePlacementValidator.validateStructurePlacements(
pack, Set.of("dimension_city", "forest_tower", "plains_farm"), false, errors);
String suffix = ".terrain.mode " + mode + " cannot target editable Iris structures; "
+ "use nativeStructures or importedStructures.adjustments for native terrain preparation.";
assertEquals(List.of(
"Dimension 'main' structures[0]" + suffix,
"Region 'forest' structures[0]" + suffix,
"Biome 'plains' structures[0]" + suffix
), errors);
}
}
@Test
public void acceptsNativeOnlyTerrainModesForNativeStructurePlacements() throws Exception {
for (String mode : List.of("VACUUM", "ENCASE")) {
File pack = temporaryFolder.newFolder("native-" + mode.toLowerCase(Locale.ROOT));
writePlacement(pack, "dimensions/main.json", "native-" + mode,
"minecraft:village_plains", true, mode);
List<String> errors = new ArrayList<>();
PackStructurePlacementValidator.validateStructurePlacements(pack, Set.of(), false, errors);
assertTrue(mode + ": " + errors, errors.isEmpty());
}
}
@Test
public void acceptsSupportedTerrainModesForEditablePlacements() throws Exception {
for (String mode : List.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE")) {
File pack = temporaryFolder.newFolder("editable-" + mode.toLowerCase(Locale.ROOT));
writePlacement(pack, "dimensions/main.json", "editable-" + mode,
"dimension_city", false, mode);
List<String> errors = new ArrayList<>();
PackStructurePlacementValidator.validateStructurePlacements(
pack, Set.of("dimension_city"), false, errors);
assertTrue(mode + ": " + errors, errors.isEmpty());
}
}
@Test
public void acceptsNativeOnlyTerrainModesForImportedStructureAdjustments() {
for (String mode : List.of("VACUUM", "ENCASE")) {
JSONObject policy = new JSONObject().put("adjustments", new JSONArray().put(
new JSONObject()
.put("match", new JSONArray().put("towns_and_towers:"))
.put("terrain", new JSONObject().put("mode", mode))));
List<String> errors = new ArrayList<>();
PackDimensionValidator.validateImportedStructurePolicy(
"overworld", new JSONObject().put("importedStructures", policy), errors);
assertTrue(mode + ": " + errors, errors.isEmpty());
}
}
private void writePlacement(File pack, String relativePath, String placementId,
String structureKey, boolean nativeStructure,
String terrainMode) throws Exception {
String source = nativeStructure
? "\"nativeStructures\":[{\"structure\":\"" + structureKey + "\"}]"
: "\"structures\":[\"" + structureKey + "\"]";
write(pack, relativePath, "{\"structures\":[{\"placementId\":\"" + placementId
+ "\"," + source + ",\"terrain\":{\"mode\":\"" + terrainMode + "\"}}]}");
}
private void write(File root, String relativePath, String content) throws Exception {
Path path = root.toPath().resolve(relativePath);
Files.createDirectories(path.getParent());
Files.writeString(path, content, StandardCharsets.UTF_8);
}
}
@@ -156,6 +156,7 @@ public class SchemaBuilderParityTest {
JSONObject terrainDefinition = schema.getJSONObject("definitions")
.getJSONObject(terrain.getString("$ref").substring("#/definitions/".length()));
JSONObject terrainProperties = terrainDefinition.getJSONObject("properties");
JSONObject terrainMode = terrainProperties.getJSONObject("mode");
JSONObject carveShape = terrainProperties.getJSONObject("shape");
JSONObject erosionStrength = terrainProperties.getJSONObject("erosionStrength");
JSONObject erosionFrequency = terrainProperties.getJSONObject("erosionFrequency");
@@ -163,10 +164,17 @@ public class SchemaBuilderParityTest {
JSONObject lobeStrength = terrainProperties.getJSONObject("lobeStrength");
String carveShapeDefinition = carveShape.getString("$ref")
.substring("#/definitions/".length());
String terrainModeDefinition = terrainMode.getString("$ref")
.substring("#/definitions/".length());
assertTrue(properties.has("structures"));
assertTrue(properties.has("nativeStructures"));
assertTrue(properties.has("distribution"));
assertTrue(terrain.getString("description").contains(
"The editable structures backend supports SOURCE, PRESERVE, BORE, and FORCE_CARVE. "
+ "The nativeStructures backend supports every terrain mode."));
assertEquals(List.of("SOURCE", "PRESERVE", "BORE", "FORCE_CARVE", "VACUUM", "ENCASE"),
oneOfValues(schema.getJSONObject("definitions"), terrainModeDefinition));
assertEquals(List.of("BOX", "ROUNDED", "ERODED"), oneOfValues(
schema.getJSONObject("definitions"), carveShapeDefinition));
assertEquals(0D, erosionStrength.getDouble("minimum"), 0D);
@@ -100,6 +100,33 @@ public class NativeStructureOwnershipRecordTest {
assertEquals(83L, record.placementIdentity());
}
@Test
public void referenceEnvelopeRefreshPreservesPersistedAuthority() {
NativeStructureOwnershipRecord original = record(
"nova_structures:tavern_oak", -3, 9, 83L);
NativeStructureOwnershipRecord refreshed = original.withReferenceEnvelope(
-6, 0, 6, 12);
assertEquals(original.schema(), refreshed.schema());
assertEquals(original.ownershipKey(), refreshed.ownershipKey());
assertEquals(original.placementIdentity(), refreshed.placementIdentity());
assertEquals(original.baseY(), refreshed.baseY());
assertEquals(original.contentMinX(), refreshed.contentMinX());
assertEquals(original.contentMinY(), refreshed.contentMinY());
assertEquals(original.contentMinZ(), refreshed.contentMinZ());
assertEquals(original.contentMaxX(), refreshed.contentMaxX());
assertEquals(original.contentMaxY(), refreshed.contentMaxY());
assertEquals(original.contentMaxZ(), refreshed.contentMaxZ());
assertEquals(original.locatorY(), refreshed.locatorY());
assertEquals(original.contentFingerprint(), refreshed.contentFingerprint());
assertEquals(original.decision(), refreshed.decision());
assertEquals(-6, refreshed.referenceMinChunkX());
assertEquals(0, refreshed.referenceMaxChunkX());
assertEquals(6, refreshed.referenceMinChunkZ());
assertEquals(12, refreshed.referenceMaxChunkZ());
}
@Test
public void ownershipRoundTripPreservesExactBoundsAndPriority() throws Exception {
NativeStructureOwnershipRecord ownership = record(
@@ -202,6 +202,35 @@ public class IrisImportedStructureControlTest {
assertNull(control.resolve("minecraft:trial_chambers", true).yBand());
}
@Test
public void namespaceVacuumCanPreserveSpecificStructuresLater() {
IrisVanillaStructureAdjustment namespace = new IrisVanillaStructureAdjustment()
.setMatch(keys("towns_and_towers:"))
.setTerrain(new IrisStructureTerrain().setMode(IrisStructureTerrainMode.VACUUM));
IrisVanillaStructureAdjustment preserveShips = new IrisVanillaStructureAdjustment()
.setMatch(keys(
"towns_and_towers:mimic_desert",
"towns_and_towers:pillager_outpost_ocean",
"towns_and_towers:village_ocean",
"towns_and_towers:wreckage_ocean"))
.setTerrain(new IrisStructureTerrain().setMode(IrisStructureTerrainMode.PRESERVE));
KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
adjustments.add(namespace);
adjustments.add(preserveShips);
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setAdjustments(adjustments);
assertEquals(IrisStructureTerrainMode.VACUUM,
control.resolve("towns_and_towers:village_forest", false)
.terrain().resolvedMode());
assertEquals(IrisStructureTerrainMode.PRESERVE,
control.resolve("towns_and_towers:village_ocean", false)
.terrain().resolvedMode());
assertEquals(IrisStructureTerrainMode.PRESERVE,
control.resolve("towns_and_towers:mimic_desert", true)
.terrain().resolvedMode());
}
@Test
public void preserveSourceYMatchesTheStructureFamilyAndKeepsExplicitShifts() {
IrisVanillaStructureAdjustment adjustment = new IrisVanillaStructureAdjustment()