mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 12:41:43 +00:00
d
This commit is contained in:
@@ -303,15 +303,19 @@ public class ServerConfigurator {
|
||||
return "";
|
||||
}
|
||||
Path root = packsDir.toPath().toAbsolutePath().normalize();
|
||||
if (Files.isSymbolicLink(root)) {
|
||||
throw new IllegalArgumentException("Iris packs root is a symbolic link: " + root);
|
||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return "";
|
||||
}
|
||||
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isDirectory(root)) {
|
||||
if (Files.isSymbolicLink(root)) {
|
||||
throw new IllegalArgumentException("Iris packs root target is missing or unsafe: " + root);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
Path resolvedRoot = root.toRealPath();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
List<FingerprintEntry> entries = collectFingerprintEntries(root);
|
||||
List<FingerprintEntry> entries = collectFingerprintEntries(resolvedRoot);
|
||||
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
|
||||
byte[] buffer = new byte[8192];
|
||||
for (FingerprintEntry entry : entries) {
|
||||
|
||||
@@ -91,6 +91,7 @@ import java.util.stream.Stream;
|
||||
|
||||
public final class DatapackIngestService {
|
||||
private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)";
|
||||
private static final String FINDER_METADATA = ".DS_Store";
|
||||
private static final String OVERRIDES_STRIPPED_MARKER = ".iris-overrides-stripped";
|
||||
private static final String OWNERSHIP_MARKER = ".iris-managed.json";
|
||||
private static final String TRANSACTION_DIRECTORY = ".iris-datapack-transactions";
|
||||
@@ -803,6 +804,7 @@ public final class DatapackIngestService {
|
||||
if (!ownershipSourceMatches(ownership, entry)) {
|
||||
throw new IOException("Ownership marker at " + directory.getPath() + " belongs to '" + ownership.id + "'");
|
||||
}
|
||||
removeFinderMetadata(directory);
|
||||
if (!Objects.equals(ownership.contentHash, directoryHash(directory))) {
|
||||
throw new IOException("Refusing to remove modified or corrupt Iris-managed datapack " + directory.getPath());
|
||||
}
|
||||
@@ -1479,6 +1481,9 @@ public final class DatapackIngestService {
|
||||
}
|
||||
validateInstallTree(target, worldFolder, "Existing datapack install");
|
||||
Ownership ownership = readOwnershipOrNull(target);
|
||||
if (ownership != null) {
|
||||
removeFinderMetadata(target);
|
||||
}
|
||||
String currentHash = directoryHash(target);
|
||||
originalHash = currentHash;
|
||||
originalMarkerHash = ownershipMarkerFingerprint(target);
|
||||
@@ -1705,6 +1710,9 @@ public final class DatapackIngestService {
|
||||
throw new IOException("Missing or unsafe " + purpose + " at " + directory.getPath());
|
||||
}
|
||||
validateInstallTree(directory, storeAnchor, purpose);
|
||||
if (Files.exists(new File(directory, OWNERSHIP_MARKER).toPath(), LinkOption.NOFOLLOW_LINKS)) {
|
||||
removeFinderMetadata(directory);
|
||||
}
|
||||
if (!expectedIdentity.isEmpty()
|
||||
&& !Objects.equals(directoryIdentity(directory), expectedIdentity)) {
|
||||
throw new IOException("Datapack directory identity changed in " + purpose + " at " + directory.getPath());
|
||||
@@ -1991,6 +1999,7 @@ public final class DatapackIngestService {
|
||||
if (!id.equals(ownership.id)) {
|
||||
throw new IOException("Datapack ownership mismatch at " + directory.getPath());
|
||||
}
|
||||
removeFinderMetadata(directory);
|
||||
}
|
||||
|
||||
private static void rejectSymbolicLinks(File root) throws IOException {
|
||||
@@ -2019,6 +2028,13 @@ public final class DatapackIngestService {
|
||||
if (path.equals(rootMarker)) {
|
||||
continue;
|
||||
}
|
||||
if (isFinderMetadata(path)) {
|
||||
if (Files.isSymbolicLink(path)
|
||||
|| !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Suspicious Finder metadata in datapack: " + path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
pathCount++;
|
||||
if (pathCount > MAX_MANAGED_PATHS) {
|
||||
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
|
||||
@@ -2087,6 +2103,31 @@ public final class DatapackIngestService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeFinderMetadata(File root) throws IOException {
|
||||
List<Path> entries;
|
||||
try (Stream<Path> paths = Files.walk(root.toPath())) {
|
||||
entries = paths.limit(MAX_MANAGED_PATHS + 1L).toList();
|
||||
}
|
||||
if (entries.size() > MAX_MANAGED_PATHS) {
|
||||
throw new IOException("Datapack contains more than " + MAX_MANAGED_PATHS + " paths");
|
||||
}
|
||||
for (Path path : entries) {
|
||||
if (!isFinderMetadata(path)) {
|
||||
continue;
|
||||
}
|
||||
if (Files.isSymbolicLink(path)
|
||||
|| !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IOException("Suspicious Finder metadata in datapack: " + path);
|
||||
}
|
||||
Files.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFinderMetadata(Path path) {
|
||||
Path fileName = path.getFileName();
|
||||
return fileName != null && FINDER_METADATA.equals(fileName.toString());
|
||||
}
|
||||
|
||||
private static PackResources scanPackResources(File root) throws IOException {
|
||||
TreeSet<String> structureKeys = new TreeSet<>();
|
||||
TreeSet<String> templateKeys = new TreeSet<>();
|
||||
@@ -2133,6 +2174,7 @@ public final class DatapackIngestService {
|
||||
if (!id.equals(ownership.id)) {
|
||||
throw new IOException("Ownership marker belongs to '" + ownership.id + "'");
|
||||
}
|
||||
removeFinderMetadata(directory);
|
||||
if (!Objects.equals(ownership.contentHash, directoryHash(directory))) {
|
||||
throw new IOException("Refusing to delete modified or corrupt Iris-managed datapack " + directory.getPath());
|
||||
}
|
||||
@@ -3440,7 +3482,7 @@ public final class DatapackIngestService {
|
||||
}
|
||||
|
||||
private static boolean isHarmlessRecoveryArtifact(Path path) throws IOException {
|
||||
if (!".DS_Store".equals(path.getFileName().toString())) {
|
||||
if (!isFinderMetadata(path)) {
|
||||
return false;
|
||||
}
|
||||
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||
|
||||
@@ -44,7 +44,7 @@ final class PackDimensionValidator {
|
||||
continue;
|
||||
}
|
||||
|
||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors);
|
||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
@@ -90,7 +90,7 @@ final class PackDimensionValidator {
|
||||
}
|
||||
|
||||
static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension,
|
||||
List<String> blockingErrors) {
|
||||
List<String> blockingErrors, List<String> warnings) {
|
||||
if (!dimension.has("importedStructures")) {
|
||||
return;
|
||||
}
|
||||
@@ -127,6 +127,10 @@ final class PackDimensionValidator {
|
||||
+ "' importedStructures.adjustments has a non-object entry at index " + index + ".");
|
||||
continue;
|
||||
}
|
||||
if (adjustment.has("clearVegetation")) {
|
||||
warnings.add("Dimension '" + dimensionKey + "' importedStructures.adjustments[" + index
|
||||
+ "].clearVegetation was removed and is ignored. Vegetation is always cleared inside structure piece envelopes.");
|
||||
}
|
||||
validateStructureKeyList(dimensionKey, adjustment, "match", blockingErrors);
|
||||
validateAdjustmentYBand(dimensionKey, adjustment, index, blockingErrors);
|
||||
PackStructurePlacementValidator.validateNativeTerrain("Dimension '" + dimensionKey
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class PackDirectoryResolver {
|
||||
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return List.of();
|
||||
}
|
||||
if (Files.isSymbolicLink(root) || !Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||
if (!Files.isDirectory(root)) {
|
||||
throw new IOException("Pack workspace is missing or unsafe: " + root);
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(root)) {
|
||||
|
||||
+2
-6
@@ -34,7 +34,7 @@ public record NativeStructureOwnershipRecord(
|
||||
String contentFingerprint,
|
||||
DecisionSnapshot decision
|
||||
) {
|
||||
public static final int CURRENT_SCHEMA = 1;
|
||||
public static final int CURRENT_SCHEMA = 2;
|
||||
public static final int MAX_REFERENCE_DISTANCE_CHUNKS = 8;
|
||||
private static final int MAX_KEY_BYTES = 512;
|
||||
private static final int MAX_FINGERPRINT_BYTES = 128;
|
||||
@@ -251,7 +251,7 @@ public record NativeStructureOwnershipRecord(
|
||||
}
|
||||
}
|
||||
|
||||
public record DecisionSnapshot(boolean clearVegetation, String stiltJson, String terrainJson) {
|
||||
public record DecisionSnapshot(String stiltJson, String terrainJson) {
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final int MAX_JSON_BYTES = 65_536;
|
||||
|
||||
@@ -269,7 +269,6 @@ public record NativeStructureOwnershipRecord(
|
||||
throw new IllegalArgumentException("Only generated native structure decisions can be persisted");
|
||||
}
|
||||
return new DecisionSnapshot(
|
||||
resolved.clearVegetation(),
|
||||
GSON.toJson(resolved.stilt()),
|
||||
GSON.toJson(Objects.requireNonNullElseGet(
|
||||
resolved.terrain(), IrisStructureTerrain::new))
|
||||
@@ -288,21 +287,18 @@ public record NativeStructureOwnershipRecord(
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
clearVegetation,
|
||||
stilt,
|
||||
terrain
|
||||
);
|
||||
}
|
||||
|
||||
void write(DataOutputStream output) throws IOException {
|
||||
output.writeBoolean(clearVegetation);
|
||||
writeString(output, stiltJson, MAX_JSON_BYTES, "stilt snapshot");
|
||||
writeString(output, terrainJson, MAX_JSON_BYTES, "terrain snapshot");
|
||||
}
|
||||
|
||||
static DecisionSnapshot read(DataInputStream input) throws IOException {
|
||||
return new DecisionSnapshot(
|
||||
input.readBoolean(),
|
||||
readString(input, MAX_JSON_BYTES, "stilt snapshot"),
|
||||
readString(input, MAX_JSON_BYTES, "terrain snapshot")
|
||||
);
|
||||
|
||||
-1
@@ -79,7 +79,6 @@ public final class NativeStructurePlacementPlanner {
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
!placement.isUnderground(),
|
||||
placement.getStilt(),
|
||||
placement.resolvedTerrain()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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.mantle.components;
|
||||
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class CarveOrphanSweep {
|
||||
private static final int CHUNK_SIZE = 16;
|
||||
private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE;
|
||||
private static final int BAND_FLOOR_MARGIN = 4;
|
||||
private static final int MAX_ORPHAN_CELLS = 16;
|
||||
private static final MatterCavern ORPHAN_CAVERN = new MatterCavern(true, "", (byte) 0);
|
||||
private static final ThreadLocal<SweepScratch> SCRATCH = ThreadLocal.withInitial(SweepScratch::new);
|
||||
|
||||
public interface CarveAccess {
|
||||
boolean isCarved(int localX, int y, int localZ);
|
||||
|
||||
void markCarved(int localX, int y, int localZ);
|
||||
}
|
||||
|
||||
private CarveOrphanSweep() {
|
||||
}
|
||||
|
||||
static int sweepChunk(MantleChunk<Matter> chunk, int[] surfaceHeights, int maxSurfaceBreakDepth, int worldCeilingY) {
|
||||
if (chunk == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, new MantleCarveAccess(chunk));
|
||||
}
|
||||
|
||||
public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) {
|
||||
if (surfaceHeights == null || surfaceHeights.length < CHUNK_AREA || access == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int minSurfaceY = Integer.MAX_VALUE;
|
||||
int maxSurfaceY = Integer.MIN_VALUE;
|
||||
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
|
||||
int surfaceY = surfaceHeights[columnIndex];
|
||||
if (surfaceY < minSurfaceY) {
|
||||
minSurfaceY = surfaceY;
|
||||
}
|
||||
if (surfaceY > maxSurfaceY) {
|
||||
maxSurfaceY = surfaceY;
|
||||
}
|
||||
}
|
||||
|
||||
int bandTop = Math.min(worldCeilingY, maxSurfaceY);
|
||||
int bandFloor = Math.max(worldFloorY + 1, minSurfaceY - Math.max(0, maxSurfaceBreakDepth) - BAND_FLOOR_MARGIN);
|
||||
if (bandTop < bandFloor) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bandHeight = bandTop - bandFloor + 1;
|
||||
int cellCount = bandHeight * CHUNK_AREA;
|
||||
SweepScratch scratch = SCRATCH.get();
|
||||
scratch.prepare(cellCount);
|
||||
long[] solid = scratch.solid;
|
||||
long[] visited = scratch.visited;
|
||||
int[] stack = scratch.stack;
|
||||
int[] component = scratch.component;
|
||||
|
||||
boolean carvedPresent = false;
|
||||
for (int y = bandFloor; y <= bandTop; y++) {
|
||||
int layer = (y - bandFloor) * CHUNK_AREA;
|
||||
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
|
||||
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
|
||||
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
|
||||
if (y > surfaceHeights[columnIndex]) {
|
||||
continue;
|
||||
}
|
||||
if (access.isCarved(localX, y, localZ)) {
|
||||
carvedPresent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
setBit(solid, layer + columnIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!carvedPresent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int marked = 0;
|
||||
for (int rootIndex = 0; rootIndex < cellCount; rootIndex++) {
|
||||
if (!getBit(solid, rootIndex) || getBit(visited, rootIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setBit(visited, rootIndex);
|
||||
stack[0] = rootIndex;
|
||||
int stackSize = 1;
|
||||
int componentSize = 0;
|
||||
boolean anchored = false;
|
||||
|
||||
while (stackSize > 0) {
|
||||
int current = stack[--stackSize];
|
||||
int columnIndex = current & (CHUNK_AREA - 1);
|
||||
int localX = columnIndex >>> 4;
|
||||
int localZ = columnIndex & (CHUNK_SIZE - 1);
|
||||
int y = bandFloor + (current / CHUNK_AREA);
|
||||
|
||||
if (!anchored) {
|
||||
if (localX == 0 || localX == CHUNK_SIZE - 1 || localZ == 0 || localZ == CHUNK_SIZE - 1) {
|
||||
anchored = true;
|
||||
} else if (y == bandFloor && isSolidBelowBand(access, surfaceHeights, columnIndex, localX, localZ, bandFloor - 1, worldFloorY)) {
|
||||
anchored = true;
|
||||
} else if (componentSize >= MAX_ORPHAN_CELLS) {
|
||||
anchored = true;
|
||||
} else {
|
||||
component[componentSize++] = current;
|
||||
}
|
||||
}
|
||||
|
||||
if (y > bandFloor) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current - CHUNK_AREA);
|
||||
}
|
||||
if (y < bandTop) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current + CHUNK_AREA);
|
||||
}
|
||||
if (localX > 0) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current - CHUNK_SIZE);
|
||||
}
|
||||
if (localX < CHUNK_SIZE - 1) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current + CHUNK_SIZE);
|
||||
}
|
||||
if (localZ > 0) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current - 1);
|
||||
}
|
||||
if (localZ < CHUNK_SIZE - 1) {
|
||||
stackSize = push(solid, visited, stack, stackSize, current + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (anchored) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int index = 0; index < componentSize; index++) {
|
||||
int cell = component[index];
|
||||
int columnIndex = cell & (CHUNK_AREA - 1);
|
||||
access.markCarved(columnIndex >>> 4, bandFloor + (cell / CHUNK_AREA), columnIndex & (CHUNK_SIZE - 1));
|
||||
marked++;
|
||||
}
|
||||
}
|
||||
|
||||
return marked;
|
||||
}
|
||||
|
||||
private static int push(long[] solid, long[] visited, int[] stack, int stackSize, int neighbor) {
|
||||
if (!getBit(solid, neighbor) || getBit(visited, neighbor)) {
|
||||
return stackSize;
|
||||
}
|
||||
|
||||
setBit(visited, neighbor);
|
||||
stack[stackSize] = neighbor;
|
||||
return stackSize + 1;
|
||||
}
|
||||
|
||||
private static boolean isSolidBelowBand(CarveAccess access, int[] surfaceHeights, int columnIndex, int localX, int localZ, int belowY, int worldFloorY) {
|
||||
if (belowY < worldFloorY || belowY > surfaceHeights[columnIndex]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !access.isCarved(localX, belowY, localZ);
|
||||
}
|
||||
|
||||
private static boolean getBit(long[] bits, int index) {
|
||||
return (bits[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
private static void setBit(long[] bits, int index) {
|
||||
bits[index >>> 6] |= 1L << (index & 63);
|
||||
}
|
||||
|
||||
private static final class MantleCarveAccess implements CarveAccess {
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private MatterSlice<MatterCavern> cachedSlice;
|
||||
private int cachedSectionIndex = -1;
|
||||
|
||||
private MantleCarveAccess(MantleChunk<Matter> chunk) {
|
||||
this.chunk = chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int localX, int y, int localZ) {
|
||||
MatterSlice<MatterCavern> cavernSlice = resolveSlice(y >> 4);
|
||||
if (cavernSlice == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MatterCavern cavern = cavernSlice.get(localX, y & 15, localZ);
|
||||
return cavern != null && cavern.isCavern();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markCarved(int localX, int y, int localZ) {
|
||||
chunk.getOrCreate(y >> 4).slice(MatterCavern.class).set(localX, y & 15, localZ, ORPHAN_CAVERN);
|
||||
cachedSectionIndex = -1;
|
||||
cachedSlice = null;
|
||||
}
|
||||
|
||||
private MatterSlice<MatterCavern> resolveSlice(int sectionIndex) {
|
||||
if (sectionIndex == cachedSectionIndex) {
|
||||
return cachedSlice;
|
||||
}
|
||||
|
||||
Matter section = sectionIndex >= 0 && sectionIndex < chunk.sectionCount() ? chunk.get(sectionIndex) : null;
|
||||
cachedSlice = section == null ? null : section.getSlice(MatterCavern.class);
|
||||
cachedSectionIndex = sectionIndex;
|
||||
return cachedSlice;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SweepScratch {
|
||||
private final int[] component = new int[MAX_ORPHAN_CELLS];
|
||||
private long[] solid = new long[0];
|
||||
private long[] visited = new long[0];
|
||||
private int[] stack = new int[0];
|
||||
|
||||
private void prepare(int cellCount) {
|
||||
int words = (cellCount + 63) >>> 6;
|
||||
if (solid.length < words) {
|
||||
solid = new long[words];
|
||||
visited = new long[words];
|
||||
} else {
|
||||
Arrays.fill(solid, 0, words, 0L);
|
||||
Arrays.fill(visited, 0, words, 0L);
|
||||
}
|
||||
|
||||
if (stack.length < cellCount) {
|
||||
stack = new int[cellCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ public class IrisCaveCarver3D {
|
||||
private static final byte LIQUID_FORCED_AIR = 3;
|
||||
private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 16;
|
||||
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
|
||||
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
|
||||
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D;
|
||||
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
|
||||
|
||||
@@ -586,6 +585,12 @@ public class IrisCaveCarver3D {
|
||||
boolean[] planeCarve = scratch.planeCarve;
|
||||
int minSection = PowerOfTwoCoordinates.floorDivPow2(minY, 4);
|
||||
int maxSection = PowerOfTwoCoordinates.floorDivPow2(maxY, 4);
|
||||
int effectiveAdaptiveSampleStep = Math.max(adaptiveSampleStep, ADAPTIVE_DEEP_SAMPLE_STEP);
|
||||
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
|
||||
adaptiveThresholdMargin,
|
||||
adaptiveSampleStep,
|
||||
effectiveAdaptiveSampleStep
|
||||
);
|
||||
|
||||
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
|
||||
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
|
||||
@@ -614,12 +619,6 @@ public class IrisCaveCarver3D {
|
||||
continue;
|
||||
}
|
||||
|
||||
int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(y, adaptiveSampleStep);
|
||||
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
|
||||
adaptiveThresholdMargin,
|
||||
adaptiveSampleStep,
|
||||
effectiveAdaptiveSampleStep
|
||||
);
|
||||
classifyDensityPlaneAdaptive(
|
||||
scratch,
|
||||
x0,
|
||||
@@ -678,16 +677,6 @@ public class IrisCaveCarver3D {
|
||||
return carved;
|
||||
}
|
||||
|
||||
private int resolveAdaptivePlaneSampleStep(int y, int adaptiveSampleStep) {
|
||||
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
|
||||
return adaptiveSampleStep;
|
||||
}
|
||||
|
||||
int profileMaxY = (int) Math.ceil(profile.getVerticalRange().getMax());
|
||||
int fineBandFloorY = profileMaxY - profile.getSurfaceBreakDepth() - ADAPTIVE_DEEP_SURFACE_MARGIN;
|
||||
return y >= fineBandFloorY ? adaptiveSampleStep : ADAPTIVE_DEEP_SAMPLE_STEP;
|
||||
}
|
||||
|
||||
private double resolveAdaptivePlaneThresholdMargin(
|
||||
double adaptiveThresholdMargin,
|
||||
int adaptiveSampleStep,
|
||||
|
||||
+17
@@ -101,6 +101,23 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
carveUpperTerrain(upperCtx, weightedProfiles, writer, x, z, chunkSurfaceHeights, waterSupportPlan);
|
||||
}
|
||||
waterSupportPlan.resolve(writer.acquireChunk(x, z));
|
||||
|
||||
if (!weightedProfiles.isEmpty()) {
|
||||
CarveOrphanSweep.sweepChunk(
|
||||
writer.acquireChunk(x, z),
|
||||
chunkSurfaceHeights,
|
||||
maxSurfaceBreakDepth(weightedProfiles),
|
||||
writer.getMantle().getWorldHeight() - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static int maxSurfaceBreakDepth(List<WeightedProfile> weightedProfiles) {
|
||||
int maxDepth = 0;
|
||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||
maxDepth = Math.max(maxDepth, Math.max(0, weightedProfile.profile.getSurfaceBreakDepth()));
|
||||
}
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
|
||||
@@ -100,10 +100,8 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
sGenMatter,
|
||||
sTerrain
|
||||
));
|
||||
registerStage(burst(
|
||||
sCave,
|
||||
sPost
|
||||
));
|
||||
registerStage(sCave);
|
||||
registerStage(sPost);
|
||||
registerStage(sFloatingTerrainSolid);
|
||||
registerStage(burst(
|
||||
sDeposit,
|
||||
|
||||
@@ -81,16 +81,6 @@ public class IrisCaveProfile {
|
||||
@Desc("Additional adaptive ambiguity margin used before the cave predictor falls back to exact sampling.")
|
||||
private double adaptiveThresholdMargin = 0.04;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(4096)
|
||||
@Desc("Minimum carved cells expected from this profile before recovery boost applies.")
|
||||
private int minCarveCells = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Additional threshold boost used when profile carve output is too sparse.")
|
||||
private double recoveryThresholdBoost = 0.08;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Desc("Minimum solid clearance below terrain surface where carving may occur.")
|
||||
|
||||
@@ -52,7 +52,7 @@ public class IrisImportedStructureControl {
|
||||
private boolean datapackOverrides = true;
|
||||
|
||||
@ArrayType(type = IrisVanillaStructureAdjustment.class, min = 1)
|
||||
@Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching vegetation option explicitly clears logs and leaves inside structure piece bounds. A matching preserveSourceY option disables Iris burial repositioning for that structure. The last matching entry with stilt settings controls foundation columns, and likewise for terrain and yBand settings. A structure suppressed by an Iris placement is unaffected.")
|
||||
@Desc("Per-structure adjustments applied to vanilla, mod, and datapack structures that still generate natively. Vertical shifts from every matching entry stack. A matching preserveSourceY option disables Iris burial repositioning for that structure. The last matching entry with stilt settings controls foundation columns, and likewise for terrain and yBand settings. A structure suppressed by an Iris placement is unaffected.")
|
||||
private KList<IrisVanillaStructureAdjustment> adjustments = new KList<>();
|
||||
|
||||
public boolean shouldGenerate(String key) {
|
||||
@@ -64,7 +64,6 @@ public class IrisImportedStructureControl {
|
||||
adjustments, "importedStructures.adjustments must not be null");
|
||||
int y = undergroundStep ? undergroundYShift : 0;
|
||||
boolean preserveSourceY = false;
|
||||
boolean clearVegetation = false;
|
||||
IrisStructureStiltSettings stilt = null;
|
||||
IrisStructureTerrain terrain = null;
|
||||
IrisStructureYBand yBand = null;
|
||||
@@ -72,7 +71,6 @@ public class IrisImportedStructureControl {
|
||||
if (adjustment != null && adjustment.matches(key)) {
|
||||
y += adjustment.getYShift();
|
||||
preserveSourceY |= adjustment.isPreserveSourceY();
|
||||
clearVegetation |= adjustment.isClearVegetation();
|
||||
if (adjustment.getStilt() != null) {
|
||||
stilt = adjustment.getStilt();
|
||||
}
|
||||
@@ -85,7 +83,7 @@ public class IrisImportedStructureControl {
|
||||
}
|
||||
}
|
||||
return new IrisNativeStructureDecision(
|
||||
generationStatus(key), y, yBand, preserveSourceY, clearVegetation, stilt, terrain);
|
||||
generationStatus(key), y, yBand, preserveSourceY, stilt, terrain);
|
||||
}
|
||||
|
||||
private NativeStructureGenerationStatus generationStatus(String key) {
|
||||
|
||||
@@ -23,7 +23,6 @@ public record IrisNativeStructureDecision(
|
||||
int yShift,
|
||||
IrisStructureYBand yBand,
|
||||
boolean preserveSourceY,
|
||||
boolean clearVegetation,
|
||||
IrisStructureStiltSettings stilt,
|
||||
IrisStructureTerrain terrain
|
||||
) {
|
||||
@@ -33,6 +32,6 @@ public record IrisNativeStructureDecision(
|
||||
|
||||
public IrisNativeStructureDecision withStatus(NativeStructureGenerationStatus replacement) {
|
||||
return new IrisNativeStructureDecision(
|
||||
replacement, yShift, yBand, preserveSourceY, clearVegetation, stilt, terrain);
|
||||
replacement, yShift, yBand, preserveSourceY, stilt, terrain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ 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. 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.")
|
||||
@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 raises terrain from processed rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. 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)
|
||||
|
||||
@@ -9,7 +9,7 @@ public enum IrisStructureTerrainMode {
|
||||
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.")
|
||||
@Desc("Raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff, even when the registered structure has no terrain adaptation. Existing higher terrain and authored air remain untouched.")
|
||||
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.")
|
||||
|
||||
+2
-5
@@ -32,7 +32,7 @@ import lombok.experimental.Accessors;
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Surface structures clear intersecting trees automatically; optional postprocessing can force vegetation clearing or build palette-driven foundation columns.")
|
||||
@Desc("A per-structure adjustment applied to vanilla, mod, and datapack structures that still generate natively (those NOT suppressed by an Iris 'structures' placement). Vertical shifts move the structure start, pieces, bounds, and jigsaw metadata together before references and placement. Intersecting trees are cleared automatically inside structure piece envelopes; optional postprocessing can build palette-driven foundation columns.")
|
||||
@Data
|
||||
public class IrisVanillaStructureAdjustment {
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@@ -51,13 +51,10 @@ public class IrisVanillaStructureAdjustment {
|
||||
@Desc("When true, skip Iris burial repositioning so the structure keeps the Y its own vanilla placement chose. Underground structures are otherwise pushed below the lowest solid column across their whole footprint, which hides terrain-aware structures such as mineshafts that intentionally breach cliffs and surfaces. Vertical shifts still apply on top of the preserved Y: this dimension's undergroundYShift plus every matching adjustment's yShift.")
|
||||
private boolean preserveSourceY = false;
|
||||
|
||||
@Desc("When true, force logs and leaves out of the structure footprint even when the structure does not reach the detected tree base. Normal surface-intersecting structures are protected automatically.")
|
||||
private boolean clearVegetation = false;
|
||||
|
||||
@Desc("Optional foundation columns placed beneath the native structure piece bases after placement.")
|
||||
private IrisStructureStiltSettings stilt = null;
|
||||
|
||||
@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.")
|
||||
@Desc("Optional terrain integration override. VACUUM raises surface terrain from processed solid rigid-template foundations at or below each authored ground plane with a 12-block falloff without lowering existing ground. 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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -81,6 +82,23 @@ public class IrisDatapackCompilerTest {
|
||||
assertTrue(Files.isRegularFile(datapackRoot.resolve("pack.mcmeta")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectsInstalledPacksThroughSymbolicLinkWorkspace() throws Exception {
|
||||
Path dataDirectory = temporaryFolder.newFolder("linked-data").toPath();
|
||||
Path serverRoot = temporaryFolder.newFolder("linked-server").toPath();
|
||||
Path sharedPacks = temporaryFolder.newFolder("linked-shared-packs").toPath();
|
||||
Path overworld = sharedPacks.resolve("overworld");
|
||||
createPack(overworld, "overworld", "linked_custom");
|
||||
try {
|
||||
Files.createSymbolicLink(dataDirectory.resolve("packs"), sharedPacks);
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
|
||||
assertEquals(List.of(dataDirectory.resolve("packs/overworld").toFile()),
|
||||
IrisDatapackCompiler.collectPackRoots(dataDirectory, serverRoot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compilingNoPacksPublishesCleanEmptyDatapack() throws Exception {
|
||||
Path datapackRoot = temporaryFolder.newFolder("empty-datapack").toPath();
|
||||
|
||||
+40
@@ -148,6 +148,46 @@ public class ServerConfiguratorDatapackFingerprintTest {
|
||||
assertNotEquals(before, ServerConfigurator.computePackFingerprint(packsDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computePackFingerprintReadsSafeSymbolicWorkspaceRoots() throws Exception {
|
||||
Path workspace = tmp.newFolder("pack-workspace").toPath();
|
||||
Path externalPack = tmp.newFolder("workspace-linked-pack").toPath();
|
||||
Path dimension = externalPack.resolve("dimensions/overworld.json");
|
||||
Files.createDirectories(dimension.getParent());
|
||||
Files.writeString(dimension, "first", StandardCharsets.UTF_8);
|
||||
Path packLink = workspace.resolve("overworld");
|
||||
Path workspaceLink = tmp.getRoot().toPath().resolve("packs-link");
|
||||
try {
|
||||
Files.createSymbolicLink(packLink, externalPack);
|
||||
Files.createSymbolicLink(workspaceLink, workspace);
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
String before = ServerConfigurator.computePackFingerprint(workspaceLink.toFile());
|
||||
assertEquals(ServerConfigurator.computePackFingerprint(workspace.toFile()), before);
|
||||
|
||||
Files.writeString(dimension, "other", StandardCharsets.UTF_8);
|
||||
|
||||
assertNotEquals(before, ServerConfigurator.computePackFingerprint(workspaceLink.toFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void computePackFingerprintRejectsDanglingSymbolicWorkspaceRoots() throws Exception {
|
||||
Path workspaceLink = tmp.getRoot().toPath().resolve("dangling-packs-link");
|
||||
try {
|
||||
Files.createSymbolicLink(workspaceLink, tmp.getRoot().toPath().resolve("missing-workspace"));
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
|
||||
try {
|
||||
ServerConfigurator.computePackFingerprint(workspaceLink.toFile());
|
||||
fail("Dangling symbolic workspace roots must be rejected");
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertTrue(expected.getMessage().contains("missing or unsafe"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteExternalDatapackRecoveryBlocksCompilation() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
|
||||
@@ -449,6 +449,39 @@ public class DatapackIngestServiceTest {
|
||||
assertEquals(List.of("original:template"), entry.templateKeys);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finderMetadataDoesNotInvalidateManagedStaging() throws Exception {
|
||||
File staging = datapackDirectory("finder-metadata-staging");
|
||||
File nested = new File(staging, "data/example");
|
||||
assertTrue(nested.mkdirs());
|
||||
DatapackIngestService.Entry entry = entry("finder-metadata-staging", "v1", "1", "sha");
|
||||
DatapackIngestService.writeOwnership(staging, entry);
|
||||
File rootMetadata = new File(staging, ".DS_Store");
|
||||
File nestedMetadata = new File(nested, ".DS_Store");
|
||||
Files.writeString(rootMetadata.toPath(), "finder", StandardCharsets.UTF_8);
|
||||
Files.writeString(nestedMetadata.toPath(), "finder", StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(DatapackIngestService.isUsableStaging(staging, entry));
|
||||
assertFalse(rootMetadata.exists());
|
||||
assertFalse(nestedMetadata.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finderMetadataDirectoryCannotBypassManagedHashing() throws Exception {
|
||||
File staging = datapackDirectory("finder-metadata-directory");
|
||||
assertTrue(new File(staging, ".DS_Store").mkdir());
|
||||
DatapackIngestService.Entry entry = entry("finder-metadata-directory", "v1", "1", "sha");
|
||||
|
||||
try {
|
||||
DatapackIngestService.writeOwnership(staging, entry);
|
||||
fail("Expected suspicious Finder metadata to be rejected");
|
||||
} catch (IOException expected) {
|
||||
assertTrue(expected.getMessage().contains("Suspicious Finder metadata"));
|
||||
}
|
||||
|
||||
assertFalse(new File(staging, ".iris-managed.json").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedOwnershipNamedResourceRemainsInsideTheManagedHash() throws Exception {
|
||||
File staging = datapackDirectory("nested-ownership-resource");
|
||||
@@ -743,7 +776,7 @@ public class DatapackIngestServiceTest {
|
||||
DatapackIngestService.InstallPlan plan = prepareLegacyStagingPlan(fixture);
|
||||
DatapackIngestService.publishInstallPlan(plan);
|
||||
|
||||
assertTrue(plan.contentChanged());
|
||||
assertFalse(plan.contentChanged());
|
||||
assertTrue(DatapackIngestService.freshInstallRequiresRestart(plan.contentChanged(), true));
|
||||
assertEquals("same", Files.readString(
|
||||
new File(fixture.target(), "value.txt").toPath(), StandardCharsets.UTF_8));
|
||||
@@ -1566,6 +1599,80 @@ public class DatapackIngestServiceTest {
|
||||
assertFalse(transaction.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishingInstallCrashRemovesFinderMetadataBeforeRollback() throws Exception {
|
||||
File root = temporaryFolder.newFolder("finder-install-crash-rollback-root");
|
||||
File world = temporaryFolder.newFolder("finder-install-crash-rollback-world");
|
||||
DatapackIngestService.Entry entry = entry("managed", "v1", "1", "sha");
|
||||
writeManifest(root, entry);
|
||||
|
||||
File target = new File(world, entry.id);
|
||||
writeManagedDatapack(target, entry, "old");
|
||||
String originalHash = ownershipHash(target);
|
||||
File scratch = new File(world.getParentFile(), ".iris-datapack-install");
|
||||
assertTrue(scratch.mkdirs());
|
||||
File pending = new File(scratch, "managed-pending-finder");
|
||||
File backup = new File(scratch, "managed-backup-finder");
|
||||
writeManagedDatapack(pending, entry, "new");
|
||||
assertTrue(new File(pending, "data/nova_structures").mkdirs());
|
||||
DatapackIngestService.writeOwnership(pending, entry);
|
||||
String desiredHash = ownershipHash(pending);
|
||||
Files.move(target.toPath(), backup.toPath());
|
||||
Files.move(pending.toPath(), target.toPath());
|
||||
Map<String, Object> directory = installDirectory(
|
||||
target, pending, backup, true, originalHash, desiredHash);
|
||||
File transaction = writeCoordinator(root, "INSTALL", "PUBLISHING", entry, true,
|
||||
List.of(directory), List.of());
|
||||
Files.writeString(new File(target, ".DS_Store").toPath(), "finder", StandardCharsets.UTF_8);
|
||||
Files.writeString(new File(target, "data/.DS_Store").toPath(), "finder", StandardCharsets.UTF_8);
|
||||
Files.writeString(new File(target, "data/nova_structures/.DS_Store").toPath(),
|
||||
"finder", StandardCharsets.UTF_8);
|
||||
|
||||
DatapackIngestService.recoverTransactions(root, List.of(world));
|
||||
|
||||
assertEquals("old", Files.readString(new File(target, "value.txt").toPath(), StandardCharsets.UTF_8));
|
||||
assertFalse(new File(target, ".DS_Store").exists());
|
||||
assertFalse(backup.exists());
|
||||
assertFalse(transaction.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishingInstallCrashStillRejectsAuthoredContentMutation() throws Exception {
|
||||
File root = temporaryFolder.newFolder("changed-install-crash-rollback-root");
|
||||
File world = temporaryFolder.newFolder("changed-install-crash-rollback-world");
|
||||
DatapackIngestService.Entry entry = entry("managed", "v1", "1", "sha");
|
||||
writeManifest(root, entry);
|
||||
|
||||
File target = new File(world, entry.id);
|
||||
writeManagedDatapack(target, entry, "old");
|
||||
String originalHash = ownershipHash(target);
|
||||
File scratch = new File(world.getParentFile(), ".iris-datapack-install");
|
||||
assertTrue(scratch.mkdirs());
|
||||
File pending = new File(scratch, "managed-pending-changed");
|
||||
File backup = new File(scratch, "managed-backup-changed");
|
||||
writeManagedDatapack(pending, entry, "new");
|
||||
String desiredHash = ownershipHash(pending);
|
||||
Files.move(target.toPath(), backup.toPath());
|
||||
Files.move(pending.toPath(), target.toPath());
|
||||
Map<String, Object> directory = installDirectory(
|
||||
target, pending, backup, true, originalHash, desiredHash);
|
||||
File transaction = writeCoordinator(root, "INSTALL", "PUBLISHING", entry, true,
|
||||
List.of(directory), List.of());
|
||||
Files.writeString(new File(target, "value.txt").toPath(), "changed", StandardCharsets.UTF_8);
|
||||
|
||||
try {
|
||||
DatapackIngestService.recoverTransactions(root, List.of(world));
|
||||
fail("Expected authored datapack mutation to block recovery");
|
||||
} catch (IOException expected) {
|
||||
assertTrue(expected.getMessage().contains("content changed"));
|
||||
}
|
||||
|
||||
assertEquals("changed", Files.readString(
|
||||
new File(target, "value.txt").toPath(), StandardCharsets.UTF_8));
|
||||
assertTrue(backup.exists());
|
||||
assertTrue(transaction.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publishingInstallCrashRestoresManagedStagingWithEveryWorld() throws Exception {
|
||||
File root = temporaryFolder.newFolder("staging-install-crash-rollback-root");
|
||||
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -111,6 +112,42 @@ public class DefaultPackBootstrapProvisionerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coldInstallPreservesSymbolicLinkPackWorkspace() throws Exception {
|
||||
byte[] archive = packArchive("overworld", "bootstrap_biome");
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HttpServer server = server(archive, requests);
|
||||
Path root = Files.createTempDirectory("iris-bootstrap-linked-workspace");
|
||||
try {
|
||||
Path dataDirectory = root.resolve("plugins/Iris");
|
||||
Path sharedPacks = root.resolve("shared-plugin-data/iris/packs");
|
||||
Files.createDirectories(dataDirectory);
|
||||
Files.createDirectories(sharedPacks);
|
||||
try {
|
||||
Files.createSymbolicLink(dataDirectory.resolve("packs"), sharedPacks);
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
DefaultPackBootstrapProvisioner.ProvisionOptions options = options(server, root, Duration.ofHours(1));
|
||||
|
||||
DefaultPackBootstrapProvisioner.ProvisionResult installed = DefaultPackBootstrapProvisioner.provision(
|
||||
dataDirectory,
|
||||
ignored -> {
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
assertEquals(DefaultPackBootstrapProvisioner.ProvisionStatus.INSTALLED, installed.status());
|
||||
assertEquals(1, requests.get());
|
||||
assertTrue(Files.isSymbolicLink(dataDirectory.resolve("packs")));
|
||||
assertTrue(Files.isRegularFile(sharedPacks.resolve("overworld/dimensions/overworld.json")));
|
||||
assertTrue(Files.isRegularFile(installed.datapackRoot().resolve("pack.mcmeta")));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
delete(root);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void corruptCacheRedownloadsAndRepairsArchive() throws Exception {
|
||||
byte[] archive = packArchive("overworld", "bootstrap_biome");
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@@ -78,6 +79,42 @@ public class PackDirectoryResolverTest {
|
||||
assertNull(PackDirectoryResolver.resolveExisting(packs, ".custom-stage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listsPacksThroughSymbolicLinkWorkspace() throws Exception {
|
||||
File sharedPacks = temporaryFolder.newFolder("shared-packs");
|
||||
File overworld = new File(sharedPacks, "overworld");
|
||||
Files.createDirectory(overworld.toPath());
|
||||
Path workspace = temporaryFolder.getRoot().toPath().resolve("packs");
|
||||
try {
|
||||
Files.createSymbolicLink(workspace, sharedPacks.toPath());
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
|
||||
assertEquals(List.of(workspace.resolve("overworld").toFile()),
|
||||
PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(workspace.toFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingWorkspaceListsNoPacks() throws Exception {
|
||||
File missing = new File(temporaryFolder.getRoot(), "missing-packs");
|
||||
|
||||
assertEquals(List.of(), PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(missing));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsDanglingSymbolicLinkWorkspace() throws Exception {
|
||||
Path workspace = temporaryFolder.getRoot().toPath().resolve("dangling-packs");
|
||||
try {
|
||||
Files.createSymbolicLink(workspace, temporaryFolder.getRoot().toPath().resolve("missing-target"));
|
||||
} catch (IOException | UnsupportedOperationException | SecurityException exception) {
|
||||
Assume.assumeNoException(exception);
|
||||
}
|
||||
|
||||
assertThrows(IOException.class,
|
||||
() -> PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(workspace.toFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsSymbolicLinksInsidePackTrees() throws Exception {
|
||||
File packs = temporaryFolder.newFolder("nested-link-root");
|
||||
|
||||
+22
-3
@@ -110,19 +110,38 @@ public class PackValidatorImportedStructurePolicyTest {
|
||||
@Test
|
||||
public void explicitNullPolicyIsRejectedWhileOmissionUsesDefaults() {
|
||||
List<String> missingErrors = new ArrayList<>();
|
||||
PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject(), missingErrors);
|
||||
PackDimensionValidator.validateImportedStructurePolicy("overworld", new JSONObject(),
|
||||
missingErrors, new ArrayList<>());
|
||||
assertTrue(missingErrors.isEmpty());
|
||||
|
||||
List<String> nullErrors = new ArrayList<>();
|
||||
PackDimensionValidator.validateImportedStructurePolicy("overworld",
|
||||
new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors);
|
||||
new JSONObject().put("importedStructures", JSONObject.NULL), nullErrors, new ArrayList<>());
|
||||
assertEquals(List.of("Dimension 'overworld' importedStructures must be an object."), nullErrors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removedClearVegetationAdjustmentWarnsWithoutBlockingThePack() {
|
||||
JSONObject policy = new JSONObject()
|
||||
.put("adjustments", new JSONArray().put(new JSONObject()
|
||||
.put("match", new JSONArray().put("minecraft:village"))
|
||||
.put("clearVegetation", true)));
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
|
||||
PackDimensionValidator.validateImportedStructurePolicy("overworld",
|
||||
new JSONObject().put("importedStructures", policy), errors, warnings);
|
||||
|
||||
assertTrue(errors.toString(), errors.isEmpty());
|
||||
assertEquals(1, warnings.size());
|
||||
assertTrue(warnings.get(0), warnings.get(0)
|
||||
.contains("adjustments[0].clearVegetation was removed and is ignored"));
|
||||
}
|
||||
|
||||
private List<String> validate(JSONObject policy) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
PackDimensionValidator.validateImportedStructurePolicy("overworld",
|
||||
new JSONObject().put("importedStructures", policy), errors);
|
||||
new JSONObject().put("importedStructures", policy), errors, new ArrayList<>());
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -87,7 +87,8 @@ public class PackValidatorStructureTerrainBackendTest {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
PackDimensionValidator.validateImportedStructurePolicy(
|
||||
"overworld", new JSONObject().put("importedStructures", policy), errors);
|
||||
"overworld", new JSONObject().put("importedStructures", policy),
|
||||
errors, new ArrayList<>());
|
||||
|
||||
assertTrue(mode + ": " + errors, errors.isEmpty());
|
||||
}
|
||||
|
||||
+18
-5
@@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -25,6 +26,7 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureOwnershipRecordTest {
|
||||
private static final String FINGERPRINT = "12".repeat(32);
|
||||
private static final int SCHEMA_OFFSET = 8;
|
||||
|
||||
@Test
|
||||
public void binaryRoundTripPreservesMultipleVersionedOwnershipRecords() throws Exception {
|
||||
@@ -44,7 +46,7 @@ public class NativeStructureOwnershipRecordTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decisionSnapshotFreezesTerrainVegetationAndStiltSettings() {
|
||||
public void decisionSnapshotFreezesTerrainAndStiltSettings() {
|
||||
IrisStructureTerrain terrain = new IrisStructureTerrain()
|
||||
.setMode(IrisStructureTerrainMode.FORCE_CARVE)
|
||||
.setHorizontalPadding(9)
|
||||
@@ -64,7 +66,6 @@ public class NativeStructureOwnershipRecordTest {
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
stilt,
|
||||
terrain
|
||||
);
|
||||
@@ -76,7 +77,6 @@ public class NativeStructureOwnershipRecordTest {
|
||||
IrisNativeStructureDecision restored = snapshot.restore();
|
||||
|
||||
assertTrue(restored.generate());
|
||||
assertTrue(restored.clearVegetation());
|
||||
assertEquals(IrisStructureTerrainMode.FORCE_CARVE, restored.terrain().resolvedMode());
|
||||
assertEquals(IrisStructureCarveShape.ERODED, restored.terrain().resolvedShape());
|
||||
assertEquals(9, restored.terrain().getHorizontalPadding());
|
||||
@@ -186,7 +186,6 @@ public class NativeStructureOwnershipRecordTest {
|
||||
new LinkedHashMap<>();
|
||||
NativeStructureOwnershipRecord.DecisionSnapshot largeDecision =
|
||||
new NativeStructureOwnershipRecord.DecisionSnapshot(
|
||||
false,
|
||||
"null",
|
||||
"{\"unused\":\"" + "x".repeat(65_000) + "\"}"
|
||||
);
|
||||
@@ -302,6 +301,21 @@ public class NativeStructureOwnershipRecordTest {
|
||||
assertNull(record.restoredDecision().stilt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bundlesWrittenUnderAnOlderSchemaRevisionFailClosed() throws Exception {
|
||||
NativeStructureOwnershipRecord record = record("nova_structures:tavern_oak", 4, -7, 71L);
|
||||
NativeStructureOwnershipBundle bundle = NativeStructureOwnershipBundle.empty().with(record);
|
||||
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
|
||||
bundle.write(new DataOutputStream(encoded));
|
||||
byte[] payload = encoded.toByteArray();
|
||||
ByteBuffer.wrap(payload).putInt(SCHEMA_OFFSET, NativeStructureOwnershipRecord.CURRENT_SCHEMA - 1);
|
||||
|
||||
IOException failure = assertThrows(IOException.class, () -> NativeStructureOwnershipBundle.read(
|
||||
new DataInputStream(new ByteArrayInputStream(payload))));
|
||||
|
||||
assertTrue(failure.getMessage(), failure.getMessage().contains("schema"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedButSizedBinaryRecordsFailAsIoErrors() throws Exception {
|
||||
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
|
||||
@@ -362,7 +376,6 @@ public class NativeStructureOwnershipRecordTest {
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
new IrisStructureTerrain()
|
||||
));
|
||||
|
||||
+30
-24
@@ -31,7 +31,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
TestStorage storage = new TestStorage();
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record("test:origin_only", 4, -7, 91L, false);
|
||||
NativeStructureOwnershipRecord record = record("test:origin_only", 4, -7, 91L);
|
||||
|
||||
state.record(record);
|
||||
|
||||
@@ -48,8 +48,10 @@ public class NativeStructureOwnershipStoreTest {
|
||||
public void originAuthorityIgnoresAStaleTargetReplicaWhenOnlyPolicyChanged() {
|
||||
Engine engine = engine();
|
||||
TestStorage storage = new TestStorage();
|
||||
NativeStructureOwnershipRecord stale = record("test:replacement", 2, 3, 11L, false);
|
||||
NativeStructureOwnershipRecord current = record("test:replacement", 2, 3, 11L, true);
|
||||
NativeStructureOwnershipRecord stale = record("test:replacement", 2, 3, 11L,
|
||||
new IrisStructureTerrain().setHorizontalPadding(2), 8);
|
||||
NativeStructureOwnershipRecord current = record("test:replacement", 2, 3, 11L,
|
||||
new IrisStructureTerrain().setHorizontalPadding(9), 8);
|
||||
long origin = NativeStructureOwnershipStore.pack(2, 3);
|
||||
long target = NativeStructureOwnershipStore.pack(3, 4);
|
||||
storage.write(origin, current);
|
||||
@@ -61,7 +63,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
3, 4, current.structureKey(), 2, 3);
|
||||
|
||||
assertEquals(current, resolved);
|
||||
assertTrue(resolved.restoredDecision().clearVegetation());
|
||||
assertEquals(9, resolved.restoredDecision().terrain().getHorizontalPadding());
|
||||
assertEquals(stale.contentFingerprint(), resolved.contentFingerprint());
|
||||
assertEquals(stale, storage.find(target, stale));
|
||||
}
|
||||
@@ -73,7 +75,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:narrow_authority", 8, -3, 19L, false, 1);
|
||||
"test:narrow_authority", 8, -3, 19L, 1);
|
||||
state.record(record);
|
||||
|
||||
assertNull(state.find(
|
||||
@@ -86,7 +88,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
public void staleTargetReplicaCannotReplaceAMissingOriginAuthority() {
|
||||
Engine engine = engine();
|
||||
TestStorage storage = new TestStorage();
|
||||
NativeStructureOwnershipRecord stale = record("test:deleted", -2, 5, 17L, false);
|
||||
NativeStructureOwnershipRecord stale = record("test:deleted", -2, 5, 17L);
|
||||
storage.write(NativeStructureOwnershipStore.pack(-1, 5), stale);
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
@@ -105,7 +107,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
for (int chunkZ = -8; chunkZ <= 8; chunkZ++) {
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:dense_" + chunkX + "_" + chunkZ,
|
||||
chunkX, chunkZ, records, false);
|
||||
chunkX, chunkZ, records);
|
||||
state.record(record);
|
||||
assertEquals(record, state.find(
|
||||
0, 0, record.structureKey(), chunkX, chunkZ));
|
||||
@@ -126,7 +128,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
BlockingStorage storage = new BlockingStorage();
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record("test:flush_race", -4, 8, 42L, false);
|
||||
NativeStructureOwnershipRecord record = record("test:flush_race", -4, 8, 42L);
|
||||
storage.blockedTarget = NativeStructureOwnershipStore.pack(-4, 8);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
@@ -152,7 +154,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
BlockingStorage storage = new BlockingStorage();
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record("test:close_race", 6, -9, 73L, false);
|
||||
NativeStructureOwnershipRecord record = record("test:close_race", 6, -9, 73L);
|
||||
storage.blockedTarget = NativeStructureOwnershipStore.pack(6, -9);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
@@ -178,7 +180,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:closing_session", -7, 12, 74L, false);
|
||||
"test:closing_session", -7, 12, 74L);
|
||||
|
||||
state.record(record);
|
||||
|
||||
@@ -207,8 +209,8 @@ public class NativeStructureOwnershipStoreTest {
|
||||
PostWriteBlockingStorage storage = new PostWriteBlockingStorage();
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord first = record("test:same_origin", 3, -6, 1L, false);
|
||||
NativeStructureOwnershipRecord second = record("test:same_origin", 3, -6, 2L, true);
|
||||
NativeStructureOwnershipRecord first = record("test:same_origin", 3, -6, 1L);
|
||||
NativeStructureOwnershipRecord second = record("test:same_origin", 3, -6, 2L);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<?> firstWrite = callers.submit(() -> state.record(first));
|
||||
@@ -238,7 +240,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:autosave_durable", -11, 14, 101L, true);
|
||||
"test:autosave_durable", -11, 14, 101L);
|
||||
|
||||
state.record(record);
|
||||
state.flush();
|
||||
@@ -258,7 +260,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:flush_retry", 12, -15, 102L, false);
|
||||
"test:flush_retry", 12, -15, 102L);
|
||||
|
||||
state.record(record);
|
||||
assertThrows(IllegalStateException.class, state::flush);
|
||||
@@ -279,7 +281,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:clean_flush", 16, 17, 103L, false);
|
||||
"test:clean_flush", 16, 17, 103L);
|
||||
|
||||
state.flush();
|
||||
state.record(record);
|
||||
@@ -294,7 +296,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
Engine engine = engine();
|
||||
CrashableStorage storage = new CrashableStorage();
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:discard_durable", -18, 19, 104L, false);
|
||||
"test:discard_durable", -18, 19, 104L);
|
||||
storage.write(NativeStructureOwnershipStore.pack(-18, 19), record);
|
||||
storage.flush();
|
||||
NativeStructureOwnershipStore.State state =
|
||||
@@ -318,7 +320,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:close_durable", 20, -21, 105L, true);
|
||||
"test:close_durable", 20, -21, 105L);
|
||||
|
||||
state.record(record);
|
||||
state.close();
|
||||
@@ -338,7 +340,7 @@ public class NativeStructureOwnershipStoreTest {
|
||||
NativeStructureOwnershipStore.State state =
|
||||
new NativeStructureOwnershipStore.State(engine, storage);
|
||||
NativeStructureOwnershipRecord record = record(
|
||||
"test:close_retry", -22, 23, 106L, false);
|
||||
"test:close_retry", -22, 23, 106L);
|
||||
|
||||
state.record(record);
|
||||
assertThrows(IllegalStateException.class, state::close);
|
||||
@@ -362,14 +364,19 @@ public class NativeStructureOwnershipStoreTest {
|
||||
}
|
||||
|
||||
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
|
||||
long placementIdentity,
|
||||
boolean clearVegetation) {
|
||||
return record(key, originX, originZ, placementIdentity, clearVegetation, 8);
|
||||
long placementIdentity) {
|
||||
return record(key, originX, originZ, placementIdentity, new IrisStructureTerrain(), 8);
|
||||
}
|
||||
|
||||
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
|
||||
long placementIdentity,
|
||||
boolean clearVegetation,
|
||||
int referenceRadius) {
|
||||
return record(key, originX, originZ, placementIdentity, new IrisStructureTerrain(), referenceRadius);
|
||||
}
|
||||
|
||||
private static NativeStructureOwnershipRecord record(String key, int originX, int originZ,
|
||||
long placementIdentity,
|
||||
IrisStructureTerrain terrain,
|
||||
int referenceRadius) {
|
||||
return new NativeStructureOwnershipRecord(
|
||||
NativeStructureOwnershipRecord.CURRENT_SCHEMA,
|
||||
@@ -396,9 +403,8 @@ public class NativeStructureOwnershipStoreTest {
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
clearVegetation,
|
||||
null,
|
||||
new IrisStructureTerrain()
|
||||
terrain
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
-1
@@ -82,7 +82,6 @@ public class NativeStructurePlacementPlannerTest {
|
||||
IrisNativeStructureDecision decision = NativeStructurePlacementPlanner.decisionFor(plan);
|
||||
|
||||
assertEquals(NativeStructureGenerationStatus.GENERATE_NATIVE, decision.status());
|
||||
assertEquals(false, decision.clearVegetation());
|
||||
assertSame(terrain, decision.terrain());
|
||||
}
|
||||
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package art.arcane.iris.engine.mantle.components;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CarveOrphanSweepTest {
|
||||
private static final int WORLD_HEIGHT = 64;
|
||||
private static final int WORLD_FLOOR_Y = 0;
|
||||
private static final int WORLD_CEILING_Y = WORLD_HEIGHT - 1;
|
||||
private static final int SURFACE_Y = 40;
|
||||
private static final int SURFACE_BREAK_DEPTH = 18;
|
||||
private static final int BAND_FLOOR_Y = SURFACE_Y - SURFACE_BREAK_DEPTH - 4;
|
||||
|
||||
@Test
|
||||
public void interiorClumpsOfOneTwoAndFiveCellsAreMarkedCarved() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(1, 14, 25, 35, 1, 14);
|
||||
fixture.uncarve(5, 30, 5);
|
||||
fixture.uncarve(8, 30, 8);
|
||||
fixture.uncarve(9, 30, 8);
|
||||
fixture.uncarve(5, 33, 10);
|
||||
fixture.uncarve(4, 33, 10);
|
||||
fixture.uncarve(6, 33, 10);
|
||||
fixture.uncarve(5, 33, 9);
|
||||
fixture.uncarve(5, 33, 11);
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(8, marked);
|
||||
assertTrue(fixture.wasMarked(5, 30, 5));
|
||||
assertTrue(fixture.wasMarked(8, 30, 8));
|
||||
assertTrue(fixture.wasMarked(9, 30, 8));
|
||||
assertTrue(fixture.wasMarked(5, 33, 10));
|
||||
assertTrue(fixture.wasMarked(4, 33, 10));
|
||||
assertTrue(fixture.wasMarked(6, 33, 10));
|
||||
assertTrue(fixture.wasMarked(5, 33, 9));
|
||||
assertTrue(fixture.wasMarked(5, 33, 11));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stalactiteHangingFromSurfaceCrustIsKept() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(1, 14, 25, 35, 1, 14);
|
||||
fixture.uncarve(7, 35, 7);
|
||||
fixture.uncarve(7, 34, 7);
|
||||
fixture.uncarve(7, 33, 7);
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(0, marked);
|
||||
assertFalse(fixture.wasMarked(7, 35, 7));
|
||||
assertFalse(fixture.wasMarked(7, 34, 7));
|
||||
assertFalse(fixture.wasMarked(7, 33, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void componentTouchingSolidBelowBandFloorIsKept() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(0, 15, BAND_FLOOR_Y, 35, 0, 15);
|
||||
fixture.uncarve(7, BAND_FLOOR_Y, 7);
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(0, marked);
|
||||
assertFalse(fixture.wasMarked(7, BAND_FLOOR_Y, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void componentContainingSeamCellIsKept() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(0, 15, 25, 35, 0, 15);
|
||||
fixture.uncarve(0, 30, 7);
|
||||
fixture.uncarve(15, 31, 9);
|
||||
fixture.uncarve(6, 32, 0);
|
||||
fixture.uncarve(6, 33, 15);
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(0, marked);
|
||||
assertFalse(fixture.wasMarked(0, 30, 7));
|
||||
assertFalse(fixture.wasMarked(15, 31, 9));
|
||||
assertFalse(fixture.wasMarked(6, 32, 0));
|
||||
assertFalse(fixture.wasMarked(6, 33, 15));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void componentLargerThanSixteenCellsIsKept() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(1, 14, 25, 35, 1, 14);
|
||||
for (int localX = 3; localX <= 5; localX++) {
|
||||
for (int localZ = 3; localZ <= 5; localZ++) {
|
||||
fixture.uncarve(localX, 27, localZ);
|
||||
fixture.uncarve(localX, 28, localZ);
|
||||
}
|
||||
}
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(0, marked);
|
||||
assertFalse(fixture.wasMarked(4, 27, 4));
|
||||
assertFalse(fixture.wasMarked(4, 28, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void componentOfExactlySixteenCellsIsMarkedCarved() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carveBox(1, 14, 25, 35, 1, 14);
|
||||
for (int localX = 3; localX <= 6; localX++) {
|
||||
for (int localZ = 3; localZ <= 6; localZ++) {
|
||||
fixture.uncarve(localX, 30, localZ);
|
||||
}
|
||||
}
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(16, marked);
|
||||
assertTrue(fixture.wasMarked(3, 30, 3));
|
||||
assertTrue(fixture.wasMarked(6, 30, 6));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sweepIsDeterministicAndIdempotent() {
|
||||
Fixture first = new Fixture();
|
||||
first.carveBox(1, 14, 25, 35, 1, 14);
|
||||
first.uncarve(5, 30, 5);
|
||||
first.uncarve(8, 30, 8);
|
||||
first.uncarve(9, 30, 8);
|
||||
|
||||
Fixture second = new Fixture();
|
||||
second.carveBox(1, 14, 25, 35, 1, 14);
|
||||
second.uncarve(5, 30, 5);
|
||||
second.uncarve(8, 30, 8);
|
||||
second.uncarve(9, 30, 8);
|
||||
|
||||
int firstMarked = first.sweep();
|
||||
int secondMarked = second.sweep();
|
||||
|
||||
assertEquals(firstMarked, secondMarked);
|
||||
assertEquals(first.marks(), second.marks());
|
||||
|
||||
first.clearMarks();
|
||||
int rerun = first.sweep();
|
||||
|
||||
assertEquals(0, rerun);
|
||||
assertTrue(first.marks().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkWithoutCarveMarkersInBandDoesNothing() {
|
||||
Fixture fixture = new Fixture();
|
||||
|
||||
int marked = fixture.sweep();
|
||||
|
||||
assertEquals(0, marked);
|
||||
assertTrue(fixture.marks().isEmpty());
|
||||
}
|
||||
|
||||
private static final class Fixture implements CarveOrphanSweep.CarveAccess {
|
||||
private final boolean[] carved = new boolean[16 * WORLD_HEIGHT * 16];
|
||||
private final int[] surfaceHeights = new int[256];
|
||||
private final List<Integer> marks = new ArrayList<>();
|
||||
|
||||
private Fixture() {
|
||||
Arrays.fill(surfaceHeights, SURFACE_Y);
|
||||
}
|
||||
|
||||
private int sweep() {
|
||||
return CarveOrphanSweep.sweep(surfaceHeights, SURFACE_BREAK_DEPTH, WORLD_FLOOR_Y, WORLD_CEILING_Y, this);
|
||||
}
|
||||
|
||||
private void carveBox(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) {
|
||||
for (int localX = minX; localX <= maxX; localX++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int localZ = minZ; localZ <= maxZ; localZ++) {
|
||||
carved[index(localX, y, localZ)] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void uncarve(int localX, int y, int localZ) {
|
||||
carved[index(localX, y, localZ)] = false;
|
||||
}
|
||||
|
||||
private boolean wasMarked(int localX, int y, int localZ) {
|
||||
return marks.contains(index(localX, y, localZ));
|
||||
}
|
||||
|
||||
private List<Integer> marks() {
|
||||
return marks;
|
||||
}
|
||||
|
||||
private void clearMarks() {
|
||||
marks.clear();
|
||||
}
|
||||
|
||||
private static int index(int localX, int y, int localZ) {
|
||||
return (y * 256) + (localX * 16) + localZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int localX, int y, int localZ) {
|
||||
return carved[index(localX, y, localZ)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markCarved(int localX, int y, int localZ) {
|
||||
carved[index(localX, y, localZ)] = true;
|
||||
marks.add(index(localX, y, localZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
-2
@@ -673,8 +673,6 @@ public class IrisCaveCarver3DNearParityTest {
|
||||
profile.setDensityThreshold(new IrisStyledRange(1D, 1D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
|
||||
profile.setThresholdBias(0D);
|
||||
profile.setSampleStep(1);
|
||||
profile.setMinCarveCells(0);
|
||||
profile.setRecoveryThresholdBoost(0D);
|
||||
profile.setSurfaceClearance(5);
|
||||
profile.setAllowSurfaceBreak(true);
|
||||
profile.setSurfaceBreakNoiseThreshold(0.16D);
|
||||
|
||||
+2
-8
@@ -159,9 +159,7 @@ public class IrisImportedStructureControlTest {
|
||||
@Test
|
||||
public void postprocessingDefaultsAreDisabled() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
|
||||
assertNull(control.resolve("minecraft:village_plains", false).stilt());
|
||||
assertFalse(control.resolve(null, false).clearVegetation());
|
||||
assertNull(control.resolve(null, false).stilt());
|
||||
assertFalse(control.resolve("minecraft:mineshaft_mesa", true).preserveSourceY());
|
||||
}
|
||||
@@ -253,24 +251,21 @@ public class IrisImportedStructureControlTest {
|
||||
IrisStructureStiltSettings stilt = new IrisStructureStiltSettings();
|
||||
IrisVanillaStructureAdjustment adjustment = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village"))
|
||||
.setClearVegetation(true)
|
||||
.setStilt(stilt);
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setAdjustments(new KList<IrisVanillaStructureAdjustment>().qadd(adjustment));
|
||||
|
||||
assertTrue(control.resolve("minecraft:village_plains", false).clearVegetation());
|
||||
assertSame(stilt, control.resolve("minecraft:village_taiga", false).stilt());
|
||||
assertFalse(control.resolve("minecraft:woodland_mansion", false).clearVegetation());
|
||||
assertNull(control.resolve("minecraft:woodland_mansion", false).stilt());
|
||||
assertNull(control.resolve("minecraft:stronghold", true).stilt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleMatchesMergeVegetationAndUseLastConfiguredStilt() {
|
||||
public void multipleMatchesUseTheLastConfiguredStilt() {
|
||||
IrisStructureStiltSettings broadStilt = new IrisStructureStiltSettings().setMaxDepth(32);
|
||||
IrisStructureStiltSettings specificStilt = new IrisStructureStiltSettings().setMaxDepth(96);
|
||||
IrisVanillaStructureAdjustment broad = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village"))
|
||||
.setClearVegetation(true)
|
||||
.setStilt(broadStilt);
|
||||
IrisVanillaStructureAdjustment exactWithoutStilt = new IrisVanillaStructureAdjustment()
|
||||
.setMatch(keys("minecraft:village_plains"));
|
||||
@@ -284,7 +279,6 @@ public class IrisImportedStructureControlTest {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl().setAdjustments(adjustments);
|
||||
|
||||
IrisNativeStructureDecision plains = control.resolve("minecraft:village_plains", false);
|
||||
assertTrue(plains.clearVegetation());
|
||||
assertSame(specificStilt, plains.stilt());
|
||||
assertEquals(96, plains.stilt().getMaxDepth());
|
||||
assertSame(broadStilt, control.resolve("minecraft:village_desert", false).stilt());
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package art.arcane.iris.spi;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class IrisLoggingTest {
|
||||
@Before
|
||||
public void resetBinding() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@After
|
||||
public void clearBinding() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextualReportPrintsFullStacktraceWithBoundPlatform() {
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
IrisPlatforms.bind(platform);
|
||||
IllegalStateException failure = new IllegalStateException("outer", new IllegalArgumentException("inner"));
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream originalErr = System.err;
|
||||
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||
try {
|
||||
IrisLogging.reportError("Runtime world creation failed.", failure);
|
||||
} finally {
|
||||
System.setErr(originalErr);
|
||||
}
|
||||
|
||||
verify(platform).log(LogLevel.ERROR, "Runtime world creation failed.");
|
||||
verify(platform).reportError(failure);
|
||||
String text = output.toString(StandardCharsets.UTF_8);
|
||||
assertTrue(text.contains("IllegalStateException"));
|
||||
assertTrue(text.contains("IllegalArgumentException"));
|
||||
assertTrue(text.contains("inner"));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -250,7 +250,7 @@ public class StructureHandlerTest {
|
||||
}
|
||||
|
||||
private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
|
||||
return new IrisNativeStructureDecision(status, 0, null, false, false, null, null);
|
||||
return new IrisNativeStructureDecision(status, 0, null, false, null, null);
|
||||
}
|
||||
|
||||
private static IrisStructurePlacement nativePlacement(String key, StructureDistribution distribution,
|
||||
|
||||
Reference in New Issue
Block a user