mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Place static objects at absolute world coordinates
A dimension can now list staticObjects: an object key, a world position, a fixed rotation and scale, block edits and an optional bore or smart bore. The layer compiles once per dimension and stamps its blocks into every newly generated chunk it covers, later entries winning overlaps. Pack validation, the schema, the export closure and the object transforms (origin-anchored scaling, tile rotation) cover the new fields.
This commit is contained in:
@@ -24,10 +24,12 @@ import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.project.SchemaBuilder;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.data.KCache;
|
||||
@@ -415,6 +417,9 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
rawText = IO.readAll(j);
|
||||
JSONObject parsed = new JSONObject(rawText);
|
||||
JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass);
|
||||
if (objectClass == IrisDimension.class) {
|
||||
PackValidator.requireValidStaticObjects(manager.getDataFolder(), name, parsed);
|
||||
}
|
||||
T t = getManager().getGson()
|
||||
.fromJson(preprocess(parsed).toString(0), objectClass);
|
||||
t.setLoadKey(name);
|
||||
|
||||
@@ -49,6 +49,7 @@ final class PackDimensionValidator {
|
||||
validateImportedStructurePolicy(dimensionKey, dimJson, blockingErrors, warnings);
|
||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
@@ -93,6 +94,17 @@ final class PackDimensionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("staticObjects")) {
|
||||
return;
|
||||
}
|
||||
JSONObject height = resolveDimensionHeight(packFolder, dimension);
|
||||
int minY = height == null ? -64 : (int) height.optDouble("min", 16D);
|
||||
int maxY = height == null ? 320 : (int) height.optDouble("max", 32D);
|
||||
PackStaticObjectValidator.validate(packFolder, dimensionKey, dimension, minY, maxY, blockingErrors);
|
||||
}
|
||||
|
||||
static void validateImportedStructurePolicy(String dimensionKey, JSONObject dimension,
|
||||
List<String> blockingErrors, List<String> warnings) {
|
||||
if (!dimension.has("importedStructures")) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
|
||||
/**
|
||||
@@ -69,4 +70,17 @@ public final class PackExportClosure {
|
||||
}
|
||||
return objectKeys;
|
||||
}
|
||||
|
||||
public static KSet<String> collectStaticObjectKeys(Iterable<IrisStaticObject> placements) {
|
||||
KSet<String> objectKeys = new KSet<>();
|
||||
if (placements == null) {
|
||||
return objectKeys;
|
||||
}
|
||||
for (IrisStaticObject placement : placements) {
|
||||
if (placement != null && placement.getObject() != null && !placement.getObject().isBlank()) {
|
||||
objectKeys.add(placement.getObject());
|
||||
}
|
||||
}
|
||||
return objectKeys;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacementScaleInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
final class PackStaticObjectValidator {
|
||||
private PackStaticObjectValidator() {
|
||||
}
|
||||
|
||||
static void validate(File packFolder, String dimensionKey, JSONObject dimension,
|
||||
int minY, int maxY, List<String> errors) {
|
||||
if (!dimension.has("staticObjects")) {
|
||||
return;
|
||||
}
|
||||
String path = "Dimension '" + dimensionKey + "'.staticObjects";
|
||||
JSONArray entries = dimension.optJSONArray("staticObjects");
|
||||
if (entries == null) {
|
||||
errors.add(path + " must be an array.");
|
||||
return;
|
||||
}
|
||||
Path objectsRoot = packFolder.toPath().toAbsolutePath().normalize().resolve("objects");
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
String entryPath = path + "[" + index + "]";
|
||||
JSONObject entry = entries.optJSONObject(index);
|
||||
if (entry == null) {
|
||||
errors.add(entryPath + " must be an object.");
|
||||
continue;
|
||||
}
|
||||
validateObjectKey(entryPath, entry, objectsRoot, errors);
|
||||
validatePosition(packFolder, entryPath, entry, minY, maxY, errors);
|
||||
validateRotation(entryPath, entry, errors);
|
||||
validateNumber(entryPath, entry, "scale", IrisStaticObject.MINIMUM_SCALE,
|
||||
IrisStaticObject.MAXIMUM_SCALE, errors);
|
||||
validateInterpolation(entryPath, entry, errors);
|
||||
validateBoolean(entryPath, entry, "bore", errors);
|
||||
validateBoolean(entryPath, entry, "smartBore", errors);
|
||||
validateSeed(entryPath, entry, errors);
|
||||
validateEdits(packFolder, entryPath, entry, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateObjectKey(String path, JSONObject entry, Path objectsRoot,
|
||||
List<String> errors) {
|
||||
Object raw = entry.opt("object");
|
||||
if (!(raw instanceof String key) || key.isBlank()) {
|
||||
errors.add(path + ".object must be a nonblank object load key.");
|
||||
} else {
|
||||
Path objectFile = resolveContained(objectsRoot, key + ".iob");
|
||||
if (objectFile == null || !Files.isRegularFile(objectFile)) {
|
||||
errors.add(path + ".object references missing object '" + key + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePosition(File packFolder, String path, JSONObject entry,
|
||||
int minY, int maxY, List<String> errors) {
|
||||
JSONObject position = resolveObject(packFolder, entry.opt("position"), "position-3d",
|
||||
path + ".position", errors);
|
||||
if (position == null) {
|
||||
return;
|
||||
}
|
||||
validateInteger(path + ".position", position, "x", -IrisStaticObject.MAXIMUM_HORIZONTAL_POSITION,
|
||||
IrisStaticObject.MAXIMUM_HORIZONTAL_POSITION, 0, errors);
|
||||
validateInteger(path + ".position", position, "y", minY, maxY - 1, 0, errors);
|
||||
validateInteger(path + ".position", position, "z", -IrisStaticObject.MAXIMUM_HORIZONTAL_POSITION,
|
||||
IrisStaticObject.MAXIMUM_HORIZONTAL_POSITION, 0, errors);
|
||||
}
|
||||
|
||||
private static void validateRotation(String path, JSONObject entry, List<String> errors) {
|
||||
if (!entry.has("rotation")) {
|
||||
return;
|
||||
}
|
||||
JSONObject rotation = entry.optJSONObject("rotation");
|
||||
if (rotation == null) {
|
||||
errors.add(path + ".rotation must be an object.");
|
||||
return;
|
||||
}
|
||||
validateNumber(path + ".rotation", rotation, "x", -360, 360, errors);
|
||||
validateNumber(path + ".rotation", rotation, "y", -360, 360, errors);
|
||||
validateNumber(path + ".rotation", rotation, "z", -360, 360, errors);
|
||||
}
|
||||
|
||||
private static void validateInterpolation(String path, JSONObject entry, List<String> errors) {
|
||||
if (!entry.has("scaleInterpolation")) {
|
||||
return;
|
||||
}
|
||||
Object raw = entry.opt("scaleInterpolation");
|
||||
if (raw instanceof String name) {
|
||||
for (IrisObjectPlacementScaleInterpolator value : IrisObjectPlacementScaleInterpolator.values()) {
|
||||
if (value.name().equals(name)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
errors.add(path + ".scaleInterpolation must be NONE, TRILINEAR, TRICUBIC or TRIHERMITE.");
|
||||
}
|
||||
|
||||
private static void validateSeed(String path, JSONObject entry, List<String> errors) {
|
||||
if (!entry.has("seed")) {
|
||||
return;
|
||||
}
|
||||
if (entry.opt("seed") instanceof Number number) {
|
||||
try {
|
||||
new BigDecimal(number.toString()).longValueExact();
|
||||
return;
|
||||
} catch (ArithmeticException | NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
errors.add(path + ".seed must be an integer between " + Long.MIN_VALUE + " and " + Long.MAX_VALUE + ".");
|
||||
}
|
||||
|
||||
private static void validateEdits(File packFolder, String path, JSONObject entry, List<String> errors) {
|
||||
if (!entry.has("edit")) {
|
||||
return;
|
||||
}
|
||||
JSONArray edits = entry.optJSONArray("edit");
|
||||
if (edits == null) {
|
||||
errors.add(path + ".edit must be an array.");
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < edits.length(); index++) {
|
||||
String editPath = path + ".edit[" + index + "]";
|
||||
JSONObject edit = resolveObject(packFolder, edits.opt(index), "object-block-replacer", editPath, errors);
|
||||
if (edit == null) {
|
||||
continue;
|
||||
}
|
||||
validateBlockList(editPath + ".find", edit.optJSONArray("find"), errors);
|
||||
JSONObject replacement = resolveObject(packFolder, edit.opt("replace"), "palette",
|
||||
editPath + ".replace", errors);
|
||||
if (replacement != null) {
|
||||
validateBlockList(editPath + ".replace.palette", replacement.optJSONArray("palette"), errors);
|
||||
}
|
||||
validateNumber(editPath, edit, "chance", 0, 1, errors);
|
||||
validateBoolean(editPath, edit, "exact", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBlockList(String path, JSONArray blocks, List<String> errors) {
|
||||
if (blocks == null || blocks.length() == 0) {
|
||||
errors.add(path + " must be a nonempty array of block objects.");
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < blocks.length(); index++) {
|
||||
JSONObject block = blocks.optJSONObject(index);
|
||||
if (block == null || !(block.opt("block") instanceof String key) || key.isBlank()) {
|
||||
errors.add(path + "[" + index + "] must be a block object with a nonblank block key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject resolveObject(File packFolder, Object value, String snippetType,
|
||||
String path, List<String> errors) {
|
||||
if (value instanceof JSONObject object) {
|
||||
return object;
|
||||
}
|
||||
if (value instanceof String reference && reference.startsWith("snippet/")) {
|
||||
String base = "snippet/" + snippetType + "/";
|
||||
String key = reference.startsWith(base) ? reference : base + reference.substring("snippet/".length());
|
||||
Path root = packFolder.toPath().toAbsolutePath().normalize();
|
||||
Path snippet = resolveContained(root, key + ".json");
|
||||
if (snippet == null || !snippet.startsWith(root.resolve(base)) || !Files.isRegularFile(snippet)) {
|
||||
errors.add(path + " references missing " + snippetType + " snippet '" + reference + "'.");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new JSONObject(Files.readString(snippet, StandardCharsets.UTF_8));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
errors.add(path + " could not read snippet '" + reference + "': " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
errors.add(path + " must be an object or a " + snippetType + " snippet reference.");
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Path resolveContained(Path root, String relative) {
|
||||
try {
|
||||
Path path = root.resolve(relative).normalize();
|
||||
return path.startsWith(root) ? path : null;
|
||||
} catch (InvalidPathException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateNumber(String path, JSONObject owner, String field,
|
||||
double minimum, double maximum, List<String> errors) {
|
||||
if (!owner.has(field)) {
|
||||
return;
|
||||
}
|
||||
if (!(owner.opt(field) instanceof Number number) || !Double.isFinite(number.doubleValue())
|
||||
|| number.doubleValue() < minimum || number.doubleValue() > maximum) {
|
||||
errors.add(path + "." + field + " must be finite and between " + minimum + " and " + maximum + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateInteger(String path, JSONObject owner, String field, int minimum,
|
||||
int maximum, int fallback, List<String> errors) {
|
||||
Object raw = owner.has(field) ? owner.opt(field) : fallback;
|
||||
if (!(raw instanceof Number number) || !Double.isFinite(number.doubleValue())
|
||||
|| number.doubleValue() != Math.rint(number.doubleValue())
|
||||
|| number.doubleValue() < minimum || number.doubleValue() > maximum) {
|
||||
errors.add(path + "." + field + " must be an integer between " + minimum + " and " + maximum + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateBoolean(String path, JSONObject owner, String field, List<String> errors) {
|
||||
if (owner.has(field) && !(owner.opt(field) instanceof Boolean)) {
|
||||
errors.add(path + "." + field + " must be a boolean.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -56,6 +58,14 @@ public final class PackValidator {
|
||||
return validate(packFolder, false);
|
||||
}
|
||||
|
||||
public static void requireValidStaticObjects(File packFolder, String dimensionKey, JSONObject dimension) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
PackDimensionValidator.validateStaticObjects(packFolder, dimensionKey, dimension, errors);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join("; ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private static PackValidationResult validate(File packFolder, boolean validateLiveRegistries) {
|
||||
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
|
||||
List<String> blockingErrors = new ArrayList<>();
|
||||
|
||||
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.core.pack.PackExportClosure;
|
||||
import art.arcane.iris.engine.object.IrisMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisSpawner;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
@@ -132,6 +133,9 @@ public class IrisPackageCompiler {
|
||||
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
||||
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
||||
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
allPlacements.add(placement.toPlacement());
|
||||
}
|
||||
KSet<IrisMarker> markers = new KSet<>();
|
||||
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
||||
IrisMarker marker = dm.getMarkerLoader().load(markerKey);
|
||||
@@ -172,6 +176,10 @@ public class IrisPackageCompiler {
|
||||
j.setPlace(newNames);
|
||||
}
|
||||
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
placement.setObject(renameObjects.get(placement.getObject()));
|
||||
}
|
||||
|
||||
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
||||
StringBuilder gb = new StringBuilder();
|
||||
ChronoLatch cl = new ChronoLatch(1000);
|
||||
|
||||
@@ -841,7 +841,7 @@ public class SchemaBuilder {
|
||||
anyOf.put("anyOf", arr);
|
||||
anyOf.put("description", prop.getString("description"));
|
||||
anyOf.put("x-intellij-html-description", prop.getString("x-intellij-html-description"));
|
||||
anyOf.put("!required", type.isAnnotationPresent(Required.class));
|
||||
anyOf.put("!required", Boolean.TRUE.equals(prop.remove("!required")) || type.isAnnotationPresent(Required.class));
|
||||
|
||||
return anyOf;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionMode;
|
||||
import art.arcane.iris.engine.object.IrisDimensionModeType;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
@@ -87,6 +88,9 @@ final class EngineRuntimeBuilder {
|
||||
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.mode = createMode();
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
staticObjects.apply(engine, x, z, blocks));
|
||||
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
||||
|
||||
@@ -80,6 +80,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||
|
||||
private final transient AtomicCache<Position2> parallaxSize = new AtomicCache<>();
|
||||
private final transient AtomicCache<IrisStaticObjectLayer> staticObjectLayer = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
|
||||
private final transient AtomicCache<CNG> coordFracture = new AtomicCache<>();
|
||||
@@ -196,6 +197,9 @@ public class IrisDimension extends IrisRegistrant {
|
||||
@ArrayType(type = IrisImageMapBinding.class)
|
||||
@Desc("Bind reusable image-map resources to typed world-generation inputs")
|
||||
private KList<IrisImageMapBinding> imageMaps = new KList<>();
|
||||
@ArrayType(type = IrisStaticObject.class)
|
||||
@Desc("Static objects placed at absolute world coordinates in newly generated chunks. Each entry transforms its own copy; later entries win where objects overlap. Reloading does not rewrite existing chunks or repair player edits.")
|
||||
private KList<IrisStaticObject> staticObjects = new KList<>();
|
||||
@Desc("When true, sets the dimension's ambient light to maximum, making all blocks fully lit regardless of light level.")
|
||||
private boolean fullbright = false;
|
||||
@RegistryListResource(IrisDimension.class)
|
||||
@@ -299,6 +303,16 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return (int) getDimensionHeight().getMin();
|
||||
}
|
||||
|
||||
public IrisStaticObjectLayer getStaticObjectLayer(IrisData data) {
|
||||
return staticObjectLayer.aquire(() -> IrisStaticObjectLayer.compile(this, data));
|
||||
}
|
||||
|
||||
public IrisDimension setStaticObjects(KList<IrisStaticObject> staticObjects) {
|
||||
this.staticObjects = staticObjects;
|
||||
staticObjectLayer.reset();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, IrisDimensionCarvingEntry> getCarvingEntryIndex() {
|
||||
return carvingEntryIndex.aquire(() -> {
|
||||
Map<String, IrisDimensionCarvingEntry> index = new HashMap<>();
|
||||
|
||||
@@ -304,6 +304,10 @@ public class IrisObject extends IrisRegistrant {
|
||||
return IrisObjectTransforms.scaled(this, scale, interpolation);
|
||||
}
|
||||
|
||||
public IrisObject scaledAroundOrigin(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
return IrisObjectTransforms.scaledAroundOrigin(this, scale, interpolation);
|
||||
}
|
||||
|
||||
public void place(Location at) {
|
||||
readLock.lock();
|
||||
try {
|
||||
|
||||
@@ -514,7 +514,8 @@ final class IrisObjectPlacementRunner {
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
PlatformBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
|
||||
|
||||
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)) && !(newData.isCustom() || data.isCustom()))
|
||||
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
|
||||
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
|
||||
data = BlockDataMergeSupport.merge(data, newData);
|
||||
else
|
||||
data = newData;
|
||||
@@ -522,6 +523,8 @@ final class IrisObjectPlacementRunner {
|
||||
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
|
||||
if (t.isPresent()) {
|
||||
tile = t.get();
|
||||
} else if (!sameMaterial) {
|
||||
tile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,13 +555,13 @@ final class IrisObjectPlacementRunner {
|
||||
data = data.withProperty("waterlogged", "true");
|
||||
}
|
||||
|
||||
if (B.isVineBlock(data)) {
|
||||
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||
data = attachVineFaces(placer, data, xx, yy, zz);
|
||||
}
|
||||
|
||||
// Short-circuit order matters for cost only: the mantle read is paid solely
|
||||
// for vine blocks. Both operands are pure, so the value is unchanged.
|
||||
boolean wouldReplace = B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||
String material = IrisObjectShaping.materialKey(data);
|
||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
||||
|
||||
@@ -21,14 +21,19 @@ package art.arcane.iris.engine.object;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.data.VectorMap;
|
||||
import art.arcane.iris.util.common.math.AxisAlignedBB;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.util.common.math.IrisVector;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
import art.arcane.iris.util.project.interpolation.Interpolation3D;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
||||
@@ -69,18 +74,33 @@ final class IrisObjectTransforms {
|
||||
}
|
||||
|
||||
static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
return scaled(self, scale, interpolation, ScaleOrigin.DEFAULT);
|
||||
}
|
||||
|
||||
static IrisObject scaledAroundOrigin(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation) {
|
||||
if (!Double.isFinite(scale) || scale <= 0 || scale > 50) {
|
||||
throw new IllegalArgumentException("Object scale must be finite, greater than zero, and at most 50");
|
||||
}
|
||||
return scaled(self, scale, interpolation, ScaleOrigin.SAVED);
|
||||
}
|
||||
|
||||
private static IrisObject scaled(IrisObject self, double scale, IrisObjectPlacementScaleInterpolator interpolation,
|
||||
ScaleOrigin origin) {
|
||||
if (interpolation == null) {
|
||||
interpolation = IrisObjectPlacementScaleInterpolator.NONE;
|
||||
}
|
||||
boolean savedOrigin = origin == ScaleOrigin.SAVED;
|
||||
IrisVector sm1 = new IrisVector(scale - 1, scale - 1, scale - 1);
|
||||
scale = Math.max(0.001, Math.min(50, scale));
|
||||
if (scale < 1) {
|
||||
if (!savedOrigin && scale < 1) {
|
||||
scale = scale - 0.0001;
|
||||
}
|
||||
|
||||
IrisPosition l1 = self.getAABB().max();
|
||||
IrisPosition l2 = self.getAABB().min();
|
||||
VectorMap<PlatformBlockState> placeBlock = new VectorMap<>();
|
||||
VectorMap<TileData> placeTile = new VectorMap<>();
|
||||
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
|
||||
|
||||
IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
|
||||
if (self.getH() == 2) {
|
||||
@@ -93,33 +113,72 @@ final class IrisObjectTransforms {
|
||||
center = center.setZ(center.getBlockZ() + 0.5);
|
||||
}
|
||||
|
||||
IrisObject oo = new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
|
||||
IrisObject oo = savedOrigin ? createOriginScaledObject(self, scale)
|
||||
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
|
||||
oo.setLoadKey(self.getLoadKey());
|
||||
oo.setLoader(self.getLoader());
|
||||
oo.setLoadFile(self.getLoadFile());
|
||||
|
||||
boolean hasTiles;
|
||||
self.readLock.lock();
|
||||
try {
|
||||
for (var entry : self.blocks) {
|
||||
hasTiles = !self.states.isEmpty();
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
|
||||
PlatformBlockState bd = entry.getValue();
|
||||
placeBlock.put(entry.getKey().clone().add(IrisObject.HALF).subtract(center)
|
||||
.multiply(scale).add(sm1).toBlockVector(), bd);
|
||||
IrisBlockVector sourcePosition = entry.getKey();
|
||||
IrisBlockVector position = savedOrigin
|
||||
? scaledMinimum(sourcePosition, scale)
|
||||
: sourcePosition.clone().add(IrisObject.HALF).subtract(center)
|
||||
.multiply(scale).add(sm1).toBlockVector();
|
||||
placeBlock.put(position, bd);
|
||||
if (placeMax != null) {
|
||||
placeMax.put(position, scaledMaximum(sourcePosition, scale));
|
||||
}
|
||||
if (hasTiles) {
|
||||
TileData tile = self.states.get(entry.getKey());
|
||||
if (tile == null) {
|
||||
placeTile.remove(position);
|
||||
} else {
|
||||
placeTile.put(position, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
for (var entry : placeBlock) {
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : placeBlock) {
|
||||
IrisBlockVector v = entry.getKey();
|
||||
TileData tile = hasTiles ? placeTile.get(v) : null;
|
||||
if (scale > 1) {
|
||||
for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(v.clone().add(center), v.clone().add(center).add(sm1))) {
|
||||
IrisVector minimum = savedOrigin ? v : v.clone().add(center);
|
||||
IrisVector maximum = savedOrigin ? placeMax.get(v) : v.clone().add(center).add(sm1);
|
||||
for (IrisBlockVector vec : IrisObjectShaping.blocksBetweenTwoPoints(minimum, maximum)) {
|
||||
oo.blocks.put(vec, entry.getValue());
|
||||
if (hasTiles) {
|
||||
if (tile == null) {
|
||||
oo.states.remove(vec);
|
||||
} else {
|
||||
oo.states.put(vec, tile.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
oo.setUnsigned(v.getBlockX(), v.getBlockY(), v.getBlockZ(), entry.getValue());
|
||||
IrisBlockVector position = savedOrigin
|
||||
? v
|
||||
: oo.getSigned(v.getBlockX(), v.getBlockY(), v.getBlockZ());
|
||||
oo.blocks.put(position, entry.getValue());
|
||||
if (hasTiles) {
|
||||
if (tile == null) {
|
||||
oo.states.remove(position);
|
||||
} else {
|
||||
oo.states.put(position, tile.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<PlatformBlockState> scaledBlocks = oo.blocks;
|
||||
if (scale > 1) {
|
||||
switch (interpolation) {
|
||||
case TRILINEAR -> trilinear(oo, (int) Math.round(scale));
|
||||
@@ -128,9 +187,59 @@ final class IrisObjectTransforms {
|
||||
}
|
||||
}
|
||||
|
||||
removeInapplicableTiles(oo, scaledBlocks);
|
||||
return oo;
|
||||
}
|
||||
|
||||
private static IrisObject createOriginScaledObject(IrisObject source, double scale) {
|
||||
IrisBlockVector minimum = scaledMinimum(new IrisBlockVector(
|
||||
-source.getCenter().getX(), -source.getCenter().getY(), -source.getCenter().getZ()), scale);
|
||||
IrisBlockVector maximum = scaledMaximum(new IrisBlockVector(
|
||||
source.getW() - source.getCenter().getX() - 1,
|
||||
source.getH() - source.getCenter().getY() - 1,
|
||||
source.getD() - source.getCenter().getZ() - 1), scale);
|
||||
IrisObject object = new IrisObject(
|
||||
maximum.getBlockX() - minimum.getBlockX() + 1,
|
||||
maximum.getBlockY() - minimum.getBlockY() + 1,
|
||||
maximum.getBlockZ() - minimum.getBlockZ() + 1);
|
||||
object.setCenter(new Vector3i(-minimum.getBlockX(), -minimum.getBlockY(), -minimum.getBlockZ()));
|
||||
object.aabb.aquire(() -> new AxisAlignedBB(
|
||||
new IrisPosition(minimum.getBlockX(), minimum.getBlockY(), minimum.getBlockZ()),
|
||||
new IrisPosition(maximum.getBlockX(), maximum.getBlockY(), maximum.getBlockZ())));
|
||||
return object;
|
||||
}
|
||||
|
||||
private static IrisBlockVector scaledMinimum(IrisBlockVector position, double scale) {
|
||||
return new IrisBlockVector(Math.floor(position.getX() * scale),
|
||||
Math.floor(position.getY() * scale), Math.floor(position.getZ() * scale));
|
||||
}
|
||||
|
||||
private static IrisBlockVector scaledMaximum(IrisBlockVector position, double scale) {
|
||||
return new IrisBlockVector(Math.ceil((position.getX() + 1) * scale) - 1,
|
||||
Math.ceil((position.getY() + 1) * scale) - 1,
|
||||
Math.ceil((position.getZ() + 1) * scale) - 1);
|
||||
}
|
||||
|
||||
private static void removeInapplicableTiles(IrisObject object, VectorMap<PlatformBlockState> sourceBlocks) {
|
||||
Iterator<Map.Entry<IrisBlockVector, TileData>> iterator = object.states.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<IrisBlockVector, TileData> entry = iterator.next();
|
||||
PlatformBlockState block = object.blocks.get(entry.getKey());
|
||||
String tileKey = entry.getValue().getMaterialKey();
|
||||
if (tileKey == null) {
|
||||
tileKey = IrisObjectShaping.materialKey(sourceBlocks.get(entry.getKey()));
|
||||
} else {
|
||||
tileKey = tileKey.toLowerCase(Locale.ROOT);
|
||||
if (tileKey.indexOf(':') < 0) {
|
||||
tileKey = "minecraft:" + tileKey;
|
||||
}
|
||||
}
|
||||
if (block == null || !tileKey.equals(IrisObjectShaping.materialKey(block))) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void trilinear(IrisObject self, int rad) {
|
||||
self.writeLock.lock();
|
||||
try {
|
||||
@@ -366,4 +475,9 @@ final class IrisObjectTransforms {
|
||||
|
||||
private record NearestBlock(int x, int y, int z, int rank, PlatformBlockState state) {
|
||||
}
|
||||
|
||||
private enum ScaleOrigin {
|
||||
DEFAULT,
|
||||
SAVED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@Desc("One object placed at a fixed world position, with transformations and edits applied to a copy")
|
||||
public class IrisStaticObject {
|
||||
public static final int MAXIMUM_HORIZONTAL_POSITION = 29_999_984;
|
||||
public static final double MINIMUM_SCALE = 0.01D;
|
||||
public static final double MAXIMUM_SCALE = 50D;
|
||||
|
||||
@Required
|
||||
@RegistryListResource(IrisObject.class)
|
||||
@Desc("Object load key under objects/, without the .iob extension")
|
||||
private String object;
|
||||
@Required
|
||||
@Desc("Absolute world X, Y and Z for the object's saved origin. Rotation and scaling keep this origin fixed.")
|
||||
private IrisPosition position;
|
||||
@Desc("Fixed rotation around the saved object origin, in degrees. All axes default to zero.")
|
||||
private IrisStaticObjectRotation rotation = new IrisStaticObjectRotation();
|
||||
@MinNumber(MINIMUM_SCALE)
|
||||
@MaxNumber(MAXIMUM_SCALE)
|
||||
@Desc("Fixed size multiplier. 1 preserves the saved size, 0.5 halves it and 2 doubles it.")
|
||||
private double scale = 1D;
|
||||
@Desc("Interpolation used when enlarging the object. NONE preserves blocky shapes.")
|
||||
private IrisObjectPlacementScaleInterpolator scaleInterpolation = IrisObjectPlacementScaleInterpolator.NONE;
|
||||
@ArrayType(type = IrisObjectReplace.class)
|
||||
@Desc("Material and block-state replacements applied before rotation, without changing the source object")
|
||||
private KList<IrisObjectReplace> edit = new KList<>();
|
||||
@Desc("Replace blocks inside the object's transformed bounding box with air before placing its blocks")
|
||||
private boolean bore = false;
|
||||
@Desc("Fill enclosed rooms and pockets with air before placement, preserving their empty interiors")
|
||||
private boolean smartBore = false;
|
||||
@Desc("Fixed seed for block replacement palettes and probabilities, independent of the world seed and chunk generation order.")
|
||||
private long seed = 0L;
|
||||
|
||||
public void validate(int minY, int maxY) {
|
||||
if (object == null || object.isBlank()) {
|
||||
throw new IllegalArgumentException("object must be a nonblank object load key.");
|
||||
}
|
||||
if (position == null) {
|
||||
throw new IllegalArgumentException("position is required.");
|
||||
}
|
||||
if (position.getX() < -MAXIMUM_HORIZONTAL_POSITION || position.getX() > MAXIMUM_HORIZONTAL_POSITION
|
||||
|| position.getZ() < -MAXIMUM_HORIZONTAL_POSITION || position.getZ() > MAXIMUM_HORIZONTAL_POSITION) {
|
||||
throw new IllegalArgumentException("position.x and position.z must be between "
|
||||
+ -MAXIMUM_HORIZONTAL_POSITION + " and " + MAXIMUM_HORIZONTAL_POSITION + ".");
|
||||
}
|
||||
if (position.getY() < minY || position.getY() >= maxY) {
|
||||
throw new IllegalArgumentException("position.y must be within the dimension's world height "
|
||||
+ minY + " <= y < " + maxY + ".");
|
||||
}
|
||||
if (rotation == null) {
|
||||
throw new IllegalArgumentException("rotation must be an object.");
|
||||
}
|
||||
rotation.validate();
|
||||
if (!Double.isFinite(scale) || scale < MINIMUM_SCALE || scale > MAXIMUM_SCALE) {
|
||||
throw new IllegalArgumentException("scale must be finite and between "
|
||||
+ MINIMUM_SCALE + " and " + MAXIMUM_SCALE + ".");
|
||||
}
|
||||
if (scaleInterpolation == null) {
|
||||
throw new IllegalArgumentException("scaleInterpolation must be a supported interpolation mode.");
|
||||
}
|
||||
if (edit == null || edit.contains(null)) {
|
||||
throw new IllegalArgumentException("edit must be an array of block replacements.");
|
||||
}
|
||||
for (int index = 0; index < edit.size(); index++) {
|
||||
validateEdit(edit.get(index), index);
|
||||
}
|
||||
}
|
||||
|
||||
public IrisObjectPlacement toPlacement() {
|
||||
return new IrisObjectPlacement()
|
||||
.setPlace(new KList<>(object))
|
||||
.setMode(ObjectPlaceMode.STRUCTURE_PIECE)
|
||||
.setForcePlace(true)
|
||||
.setRotation(rotation.toRotation())
|
||||
.setScale(new IrisObjectScale().setSize(scale).setInterpolation(scaleInterpolation))
|
||||
.setEdit(new KList<>(edit))
|
||||
.setBore(bore)
|
||||
.setSmartBore(smartBore);
|
||||
}
|
||||
|
||||
private static void validateEdit(IrisObjectReplace replacement, int index) {
|
||||
if (replacement.getFind() == null || replacement.getFind().isEmpty()
|
||||
|| replacement.getFind().contains(null)) {
|
||||
throw new IllegalArgumentException("edit[" + index + "].find must contain block objects.");
|
||||
}
|
||||
IrisMaterialPalette palette = replacement.getReplace();
|
||||
if (palette == null || palette.getPalette() == null || palette.getPalette().isEmpty()
|
||||
|| palette.getPalette().contains(null)) {
|
||||
throw new IllegalArgumentException("edit[" + index + "].replace must contain a block palette.");
|
||||
}
|
||||
if (!Float.isFinite(replacement.getChance()) || replacement.getChance() < 0 || replacement.getChance() > 1) {
|
||||
throw new IllegalArgumentException("edit[" + index + "].chance must be between 0 and 1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Compiles fixed placements once per dimension snapshot and writes only the current chunk's cells.
|
||||
* Stored Y coordinates are offsets from the dimension floor; configured positions use absolute world Y.
|
||||
*/
|
||||
public final class IrisStaticObjectLayer {
|
||||
private static final IrisStaticObjectLayer EMPTY = new IrisStaticObjectLayer(Map.of());
|
||||
private static final List<Class<?>> REPLACED_METADATA = List.of(
|
||||
TileWrapper.class, Identifier.class, String.class, TreeBlockMaterial.class,
|
||||
MatterMarker.class, MatterStructurePOI.class);
|
||||
|
||||
private final Map<Long, Chunk> chunks;
|
||||
|
||||
private IrisStaticObjectLayer(Map<Long, Chunk> chunks) {
|
||||
this.chunks = chunks;
|
||||
}
|
||||
|
||||
public static IrisStaticObjectLayer compile(IrisDimension dimension, IrisData data) {
|
||||
Objects.requireNonNull(dimension.getStaticObjects(), "staticObjects must be a list");
|
||||
if (dimension.getStaticObjects().isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
Compilation compilation = new Compilation(dimension.getMaxHeight() - dimension.getMinHeight());
|
||||
for (int index = 0; index < dimension.getStaticObjects().size(); index++) {
|
||||
IrisStaticObject entry = dimension.getStaticObjects().get(index);
|
||||
try {
|
||||
Objects.requireNonNull(entry, "entry must not be null");
|
||||
entry.validate(dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
IrisObject object = data.getObjectLoader().load(entry.getObject());
|
||||
if (object == null) {
|
||||
throw new IllegalArgumentException("Object '" + entry.getObject() + "' could not be loaded");
|
||||
}
|
||||
if (entry.getScale() != 1D) {
|
||||
object = object.scaledAroundOrigin(entry.getScale(), entry.getScaleInterpolation());
|
||||
} else if (entry.isSmartBore()) {
|
||||
object = object.copy();
|
||||
}
|
||||
IrisPosition position = entry.getPosition();
|
||||
object.place(position.getX(), position.getY() - dimension.getMinHeight(),
|
||||
position.getZ(), compilation, entry.toPlacement(), new RNG(entry.getSeed()), data);
|
||||
} catch (RuntimeException failure) {
|
||||
throw new IllegalArgumentException("Invalid staticObjects[" + index + "] in dimension '"
|
||||
+ dimension.getLoadKey() + "': " + failure.getMessage(), failure);
|
||||
}
|
||||
}
|
||||
return compilation.freeze();
|
||||
}
|
||||
|
||||
public List<Block> blocks(int chunkX, int chunkZ) {
|
||||
Chunk chunk = chunks.get(chunkKey(chunkX, chunkZ));
|
||||
return chunk == null ? List.of() : chunk.ordered();
|
||||
}
|
||||
|
||||
/** Tests world X/Z and internal Y, with the dimension minimum already subtracted. */
|
||||
public boolean contains(int x, int y, int z) {
|
||||
if (chunks.isEmpty() || y < 0) {
|
||||
return false;
|
||||
}
|
||||
Chunk chunk = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
return chunk != null && chunk.blocks().containsKey(Compilation.blockKey(x, y, z));
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return chunks.isEmpty();
|
||||
}
|
||||
|
||||
public void apply(Engine engine, int x, int z, Hunk<PlatformBlockState> output) {
|
||||
List<Block> blocks = blocks(x >> 4, z >> 4);
|
||||
if (blocks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4);
|
||||
chunk.use();
|
||||
try {
|
||||
for (Block block : blocks) {
|
||||
Matter section = chunk.getOrCreate(block.y() >> 4);
|
||||
for (Class<?> type : REPLACED_METADATA) {
|
||||
MatterSlice<?> slice = section.getSlice(type);
|
||||
if (slice != null) {
|
||||
slice.set(block.x(), block.y() & 15, block.z(), null);
|
||||
}
|
||||
}
|
||||
PlatformBlockState state = block.state();
|
||||
if (state.isCustom()) {
|
||||
section.slice(Identifier.class).set(block.x(), block.y() & 15, block.z(),
|
||||
Identifier.fromString(state.deferredPlacementKey()));
|
||||
engine.getMantle().getMantle().flag(x >> 4, z >> 4, MantleFlag.CUSTOM_ACTIVE, true);
|
||||
}
|
||||
if (block.tile() != null) {
|
||||
section.slice(TileWrapper.class).set(block.x(), block.y() & 15, block.z(),
|
||||
new TileWrapper(block.tile().clone()));
|
||||
}
|
||||
output.set(block.x(), block.y(), block.z(), state.placementBaseState());
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static long chunkKey(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
public record Block(int x, int y, int z, PlatformBlockState state, TileData tile) {
|
||||
}
|
||||
|
||||
private record Chunk(Map<Integer, Block> blocks, List<Block> ordered) {
|
||||
}
|
||||
|
||||
private static final class Compilation implements IObjectPlacer {
|
||||
private final int height;
|
||||
private final Map<Long, Map<Integer, Block>> chunks = new HashMap<>();
|
||||
|
||||
private Compilation(int height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
private IrisStaticObjectLayer freeze() {
|
||||
Map<Long, Chunk> compiled = new HashMap<>(chunks.size());
|
||||
for (Map.Entry<Long, Map<Integer, Block>> chunk : chunks.entrySet()) {
|
||||
Map<Integer, Block> blocks = Map.copyOf(chunk.getValue());
|
||||
compiled.put(chunk.getKey(), new Chunk(blocks, List.copyOf(blocks.values())));
|
||||
}
|
||||
return new IrisStaticObjectLayer(Map.copyOf(compiled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, PlatformBlockState state) {
|
||||
if (y < 0 || y >= height) {
|
||||
throw new IllegalArgumentException("Transformed object extends outside the dimension height");
|
||||
}
|
||||
if (Math.abs((long) x) > 29999984L || Math.abs((long) z) > 29999984L) {
|
||||
throw new IllegalArgumentException("Transformed object extends outside the world coordinate limits");
|
||||
}
|
||||
Objects.requireNonNull(state, "Object block must not be null");
|
||||
if (state.isCustom() && (state.deferredPlacementKey() == null || state.placementBaseState() == null)) {
|
||||
throw new IllegalArgumentException("Custom object block has no placement data: " + state.key());
|
||||
}
|
||||
Map<Integer, Block> chunk = chunks.computeIfAbsent(chunkKey(x >> 4, z >> 4), key -> new HashMap<>());
|
||||
chunk.put(blockKey(x, y, z), new Block(x & 15, y, z & 15, state, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState get(int x, int y, int z) {
|
||||
Block block = block(x, y, z);
|
||||
return block == null ? IrisObject.States.AIR : block.state();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
Block block = block(x, y, z);
|
||||
if (block != null) {
|
||||
chunks.get(chunkKey(x >> 4, z >> 4)).put(blockKey(x, y, z),
|
||||
new Block(block.x(), block.y(), block.z(), block.state(), tile.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
throw new IllegalStateException("Static objects cannot sample terrain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return getHighest(x, z, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return get(x, y, z).isSolid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int x, int y, int z, T data) {
|
||||
throw new IllegalStateException("Static objects cannot create placement markers");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int x, int y, int z, Class<T> type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Block block(int x, int y, int z) {
|
||||
Map<Integer, Block> chunk = chunks.get(chunkKey(x >> 4, z >> 4));
|
||||
return chunk == null ? null : chunk.get(blockKey(x, y, z));
|
||||
}
|
||||
|
||||
private static int blockKey(int x, int y, int z) {
|
||||
return (y << 8) | ((z & 15) << 4) | (x & 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@Desc("Fixed object rotation in degrees around its saved origin")
|
||||
public class IrisStaticObjectRotation {
|
||||
@MinNumber(-360)
|
||||
@MaxNumber(360)
|
||||
@Desc("Fixed X-axis rotation in degrees, including negative and fractional angles")
|
||||
private double x = 0;
|
||||
@MinNumber(-360)
|
||||
@MaxNumber(360)
|
||||
@Desc("Fixed Y-axis rotation in degrees, including negative and fractional angles")
|
||||
private double y = 0;
|
||||
@MinNumber(-360)
|
||||
@MaxNumber(360)
|
||||
@Desc("Fixed Z-axis rotation in degrees, including negative and fractional angles")
|
||||
private double z = 0;
|
||||
|
||||
public void validate() {
|
||||
validateAngle("rotation.x", x);
|
||||
validateAngle("rotation.y", y);
|
||||
validateAngle("rotation.z", z);
|
||||
}
|
||||
|
||||
public IrisObjectRotation toRotation() {
|
||||
validate();
|
||||
return new IrisObjectRotation()
|
||||
.setEnabled(x != 0 || y != 0 || z != 0)
|
||||
.setXAxis(fixedAxis(x))
|
||||
.setYAxis(fixedAxis(y))
|
||||
.setZAxis(fixedAxis(z));
|
||||
}
|
||||
|
||||
private static IrisAxisRotationClamp fixedAxis(double angle) {
|
||||
return new IrisAxisRotationClamp(angle != 0, false, angle, angle, 0);
|
||||
}
|
||||
|
||||
private static void validateAngle(String field, double angle) {
|
||||
if (!Double.isFinite(angle) || angle < -360 || angle > 360) {
|
||||
throw new IllegalArgumentException(field + " must be finite and between -360 and 360 degrees.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package art.arcane.iris.core.loader;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ResourceLoaderStaticObjectValidationTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisPlatform previousPlatform;
|
||||
private IrisSettings previousSettings;
|
||||
private File pack;
|
||||
private IrisData data;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisPlatforms.unbind();
|
||||
pack = temporaryFolder.newFolder("pack");
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.dataFolder()).thenReturn(pack);
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
data = mock(IrisData.class);
|
||||
when(data.getDataFolder()).thenReturn(pack);
|
||||
when(data.getGson()).thenReturn(new Gson());
|
||||
write("objects/tower.iob", "");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisSettings.settings = previousSettings;
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedInlineOriginNeverReachesDeserialization() throws Exception {
|
||||
File dimension = write("dimensions/main.json", """
|
||||
{"staticObjects":[{"object":"tower","position":{"x":"wrong","y":100,"z":-100}}]}
|
||||
""");
|
||||
|
||||
assertNull(loader().loadFile(dimension, "main"));
|
||||
verify(data, never()).getGson();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void malformedPositionSnippetRejectsReloadAndPreservesThePreviousDimension() throws Exception {
|
||||
File dimension = write("dimensions/main.json", """
|
||||
{"staticObjects":[{"object":"tower","position":{"x":100,"y":100,"z":-100}}]}
|
||||
""");
|
||||
ResourceLoader<IrisDimension> loader = loader();
|
||||
IrisDimension previous = loader.loadFile(dimension, "main");
|
||||
assertNotNull(previous);
|
||||
write("snippet/position-3d/tower.json", "{\"x\":\"wrong\",\"y\":100}");
|
||||
write("dimensions/main.json", """
|
||||
{"staticObjects":[{"object":"tower","position":"snippet/tower"}]}
|
||||
""");
|
||||
|
||||
assertNull(loader.loadFile(dimension, "main"));
|
||||
assertEquals(new IrisPosition(100, 100, -100), previous.getStaticObjects().getFirst().getPosition());
|
||||
verify(data, times(1)).getGson();
|
||||
}
|
||||
|
||||
private ResourceLoader<IrisDimension> loader() {
|
||||
return new ResourceLoader<>(pack, data, "dimensions", "Dimension", IrisDimension.class,
|
||||
ResourceLoader.Options.datapackCompiler());
|
||||
}
|
||||
|
||||
private File write(String relative, String content) throws Exception {
|
||||
Path path = pack.toPath().resolve(relative);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content);
|
||||
return path.toFile();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -49,6 +50,19 @@ public class PackExportClosureTest {
|
||||
assertTrue(PackExportClosure.collectObjectKeys(placements).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectsStaticObjectKeysWithoutDuplicates() {
|
||||
KList<IrisStaticObject> placements = new KList<>();
|
||||
placements.add(new IrisStaticObject().setObject("landmarks/tower"));
|
||||
placements.add(new IrisStaticObject().setObject("landmarks/tower"));
|
||||
placements.add(new IrisStaticObject().setObject("landmarks/bridge"));
|
||||
placements.add(new IrisStaticObject().setObject(""));
|
||||
placements.add((IrisStaticObject) null);
|
||||
|
||||
assertEquals(List.of("landmarks/bridge", "landmarks/tower"),
|
||||
PackExportClosure.collectStaticObjectKeys(placements).stream().sorted().toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
|
||||
* folders were silently omitted from exports, leaving dangling entitySpawners references.
|
||||
@@ -61,6 +75,9 @@ public class PackExportClosureTest {
|
||||
assertTrue("Bukkit packager must write markers/", bukkit.contains("\"markers/\""));
|
||||
assertTrue("Bukkit packager must export region objects alongside biome objects",
|
||||
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
|
||||
assertTrue("Bukkit packager must export static objects", bukkit.contains("dimension.getStaticObjects()"));
|
||||
assertTrue("Bukkit obfuscation must rewrite static object references",
|
||||
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
|
||||
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
|
||||
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
|
||||
assertTrue("Bukkit packager must validate before opening or mutating package state",
|
||||
@@ -74,6 +91,8 @@ public class PackExportClosureTest {
|
||||
assertTrue("modded packager must write markers/", modded.contains("\"markers\""));
|
||||
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
|
||||
assertTrue("modded packager must export region objects", modded.contains("region.getObjects()"));
|
||||
assertTrue("modded packager must export static objects",
|
||||
modded.contains("PackExportClosure.collectStaticObjectKeys(dimension.getStaticObjects())"));
|
||||
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
|
||||
assertTrue("modded packager must validate before mutating package state",
|
||||
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PackStaticObjectValidatorTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void acceptsAbsentEmptyAndCompleteStaticObjects() throws Exception {
|
||||
File pack = pack();
|
||||
assertEquals(List.of(), validate(pack, "{}"));
|
||||
assertEquals(List.of(), validate(pack, "{\"staticObjects\":[]}"));
|
||||
assertEquals(List.of(), validate(pack, """
|
||||
{"staticObjects":[{
|
||||
"object":"landmarks/tower","position":{"x":100,"y":100,"z":-100},
|
||||
"rotation":{"x":-22.5,"y":90,"z":-180},"scale":0.5,
|
||||
"scaleInterpolation":"TRILINEAR","seed":9223372036854775807,"bore":true,"smartBore":true,
|
||||
"edit":[{"find":[{"block":"stone"}],"replace":{"palette":[{"block":"gold_block"}]}}]
|
||||
}]}
|
||||
"""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMalformedContainersMissingObjectsAndMissingOrigins() throws Exception {
|
||||
File pack = pack();
|
||||
assertEquals(List.of("Dimension 'main'.staticObjects must be an array."),
|
||||
validate(pack, "{\"staticObjects\":null}"));
|
||||
assertEquals(List.of("Dimension 'main'.staticObjects[0] must be an object."),
|
||||
validate(pack, "{\"staticObjects\":[null]}"));
|
||||
List<String> errors = validate(pack, "{\"staticObjects\":[{\"object\":\"missing\"}]}");
|
||||
assertEquals(2, errors.size());
|
||||
assertContains(errors, ".object references missing object 'missing'");
|
||||
assertContains(errors, ".position must be an object");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checksAllScalarTypesAndLimits() throws Exception {
|
||||
List<String> errors = validate(pack(), """
|
||||
{"staticObjects":[{
|
||||
"object":"landmarks/tower","position":{"x":29999985,"y":320,"z":0.5},
|
||||
"rotation":{"x":null,"y":-361,"z":"90"},"scale":0,
|
||||
"scaleInterpolation":"CUBIC","seed":1.5,"bore":null,"smartBore":"true","edit":null
|
||||
}]}
|
||||
""");
|
||||
assertEquals(12, errors.size());
|
||||
assertContains(errors, ".position.y must be an integer between -64 and 319");
|
||||
assertContains(errors, ".rotation.y must be finite");
|
||||
assertContains(errors, ".seed must be an integer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesHeightPositionAndReplacementSnippets() throws Exception {
|
||||
File pack = pack();
|
||||
write(pack, "snippet/range/tall.json", "{\"min\":-128,\"max\":512}");
|
||||
write(pack, "snippet/position-3d/tower.json", "{\"x\":100,\"y\":-128,\"z\":-100}");
|
||||
write(pack, "snippet/object-block-replacer/gilded.json",
|
||||
"{\"find\":[{\"block\":\"stone\"}],\"replace\":\"snippet/gold\"}");
|
||||
write(pack, "snippet/palette/gold.json", "{\"palette\":[{\"block\":\"gold_block\"}]}");
|
||||
|
||||
assertEquals(List.of(), validate(pack, """
|
||||
{"dimensionHeight":"snippet/tall","staticObjects":[{
|
||||
"object":"landmarks/tower","position":"snippet/tower","edit":["snippet/gilded"]
|
||||
}]}
|
||||
"""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsInvalidEditsAndUnresolvedSnippets() throws Exception {
|
||||
List<String> errors = validate(pack(), """
|
||||
{"staticObjects":[{
|
||||
"object":"landmarks/tower","position":"snippet/missing",
|
||||
"edit":[null,{"find":[],"replace":{"palette":[]},"chance":2,"exact":"yes"}]
|
||||
}]}
|
||||
""");
|
||||
assertEquals(6, errors.size());
|
||||
assertContains(errors, "references missing position-3d snippet");
|
||||
assertContains(errors, ".edit[1].find must be a nonempty array");
|
||||
assertContains(errors, ".edit[1].replace.palette must be a nonempty array");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionAdmissionRejectsMissingStaticObjectsBeforeRegions() throws Exception {
|
||||
File pack = pack();
|
||||
write(pack, "dimensions/main.json", """
|
||||
{"regions":[],"staticObjects":[{"object":"missing","position":{"y":100}}]}
|
||||
""");
|
||||
List<String> errors = new ArrayList<>();
|
||||
File dimension = new File(pack, "dimensions/main.json");
|
||||
|
||||
PackDimensionValidator.validateDimensions(pack, new File[]{dimension}, errors, new ArrayList<>());
|
||||
|
||||
assertContains(errors, ".object references missing object 'missing'");
|
||||
assertContains(errors, "declares no regions");
|
||||
}
|
||||
|
||||
private File pack() throws Exception {
|
||||
File pack = temporaryFolder.newFolder();
|
||||
write(pack, "objects/landmarks/tower.iob", "");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static List<String> validate(File pack, String json) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
PackDimensionValidator.validateStaticObjects(pack, "main", new JSONObject(json), errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void assertContains(List<String> errors, String fragment) {
|
||||
assertTrue(errors.toString(), errors.stream().anyMatch(error -> error.contains(fragment)));
|
||||
}
|
||||
|
||||
private static void write(File root, String relative, String content) throws Exception {
|
||||
Path path = root.toPath().resolve(relative);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisStaticObjectSchemaTest {
|
||||
private IrisPlatform previousPlatform;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.blockTypeKeys()).thenReturn(List.of());
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previousPlatform != null) {
|
||||
IrisPlatforms.bind(previousPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionExposesAnOptionalTypedStaticObjectList() throws Exception {
|
||||
Field field = IrisDimension.class.getDeclaredField("staticObjects");
|
||||
|
||||
assertEquals(IrisStaticObject.class, field.getAnnotation(ArrayType.class).type());
|
||||
assertTrue(new IrisDimension().getStaticObjects().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schemaRequiresTheObjectAndOriginAndExposesFixedTransforms() {
|
||||
JSONObject schema = new SchemaBuilder(IrisStaticObject.class, schemaData()).construct();
|
||||
JSONObject properties = schema.getJSONObject("properties");
|
||||
JSONObject definitions = schema.getJSONObject("definitions");
|
||||
JSONObject rotation = definitions.getJSONObject(properties.getJSONObject("rotation")
|
||||
.getString("$ref").substring("#/definitions/".length())).getJSONObject("properties");
|
||||
|
||||
assertEquals(2, schema.getJSONArray("required").length());
|
||||
assertEquals("object", schema.getJSONArray("required").getString(0));
|
||||
assertEquals("position", schema.getJSONArray("required").getString(1));
|
||||
assertEquals("#/definitions/erzobjects", properties.getJSONObject("object").getString("$ref"));
|
||||
assertEquals("landmarks/tower", definitions.getJSONObject("erzobjects").getJSONArray("enum").getString(0));
|
||||
assertEquals(IrisStaticObject.MINIMUM_SCALE, properties.getJSONObject("scale").getDouble("minimum"), 0D);
|
||||
assertEquals(IrisStaticObject.MAXIMUM_SCALE, properties.getJSONObject("scale").getDouble("maximum"), 0D);
|
||||
assertEquals("integer", properties.getJSONObject("seed").getString("type"));
|
||||
for (String axis : List.of("x", "y", "z")) {
|
||||
assertEquals("number", rotation.getJSONObject(axis).getString("type"));
|
||||
assertEquals(-360D, rotation.getJSONObject(axis).getDouble("minimum"), 0D);
|
||||
assertEquals(360D, rotation.getJSONObject(axis).getDouble("maximum"), 0D);
|
||||
}
|
||||
assertTrue(properties.has("edit"));
|
||||
assertTrue(properties.has("bore"));
|
||||
assertTrue(properties.has("smartBore"));
|
||||
assertFalse(properties.has("chance"));
|
||||
assertFalse(properties.has("density"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData schemaData() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisExpression> expressionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBlockData> blockLoader = mock(ResourceLoader.class);
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
loaders.put(IrisObject.class, objectLoader);
|
||||
loaders.put(IrisExpression.class, expressionLoader);
|
||||
when(data.getLoaders()).thenReturn(loaders);
|
||||
when(data.getBlockLoader()).thenReturn(blockLoader);
|
||||
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>());
|
||||
when(objectLoader.getPossibleKeys()).thenReturn(new String[]{"landmarks/tower"});
|
||||
when(objectLoader.getFolderName()).thenReturn("objects");
|
||||
when(objectLoader.getResourceTypeName()).thenReturn("Object");
|
||||
when(expressionLoader.getPossibleKeys()).thenReturn(new String[0]);
|
||||
when(expressionLoader.getFolderName()).thenReturn("expressions");
|
||||
when(expressionLoader.getResourceTypeName()).thenReturn("Expression");
|
||||
when(blockLoader.getPossibleKeys()).thenReturn(new String[0]);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
+53
@@ -7,6 +7,8 @@ import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -16,14 +18,18 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -188,6 +194,52 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
assertEquals("Object placement data is required.", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rawPlacementPreservesVineFacesAndReplacesSolidDestination() {
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
placer.set(0, ANCHOR_Y, 0, solid);
|
||||
placer.set(1, ANCHOR_Y, 0, solid);
|
||||
PlatformBlockState vine = state("minecraft:vine[east=false,north=true]", false);
|
||||
when(vine.isVineBlock()).thenReturn(true);
|
||||
IrisObject object = new IrisObject(1, 1, 1);
|
||||
object.setUnsigned(0, 0, 0, vine);
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
placement.setRotation(IrisObjectRotation.of(0, 0, 0));
|
||||
|
||||
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertSame(vine, placer.get(0, ANCHOR_Y, 0));
|
||||
verify(vine, never()).withProperty(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changingMaterialWithAnEditClearsOriginalTileData() {
|
||||
RecordingPlacer placer = new RecordingPlacer(null);
|
||||
PlatformBlockState chest = state("minecraft:chest", true);
|
||||
IrisObject object = new IrisObject(1, 1, 1);
|
||||
object.setUnsigned(0, 0, 0, chest);
|
||||
object.setUnsignedTile(0, 0, 0, new TileData("minecraft:chest", new KMap<>()));
|
||||
IrisObjectReplace edit = mock(IrisObjectReplace.class);
|
||||
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
|
||||
when(edit.getChance()).thenReturn(1F);
|
||||
when(edit.getFind(data)).thenReturn(new KList<>(chest));
|
||||
when(edit.getReplace(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(solid);
|
||||
when(edit.getReplace()).thenReturn(palette);
|
||||
when(palette.getTile(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class)))
|
||||
.thenReturn(Optional.empty());
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
placement.getEdit().add(edit);
|
||||
|
||||
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
|
||||
|
||||
assertSame(solid, placer.get(0, ANCHOR_Y, 0));
|
||||
assertNull(placer.getData(0, ANCHOR_Y, 0, TileData.class));
|
||||
assertEquals(1, object.getStates().size());
|
||||
}
|
||||
|
||||
private IrisObjectPlacement placement() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
|
||||
@@ -322,6 +374,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
|
||||
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
setData(x, y, z, tile);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.util.common.math.Vector3i;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisObjectTransformsTileTest {
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
Map<String, PlatformBlockState> states = new HashMap<>();
|
||||
when(registries.block(anyString())).thenAnswer(invocation -> {
|
||||
String key = invocation.getArgument(0);
|
||||
return states.computeIfAbsent(key, IrisObjectTransformsTileTest::state);
|
||||
});
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enlargedTilesFollowEveryExpandedBlockAndCloneProperties() {
|
||||
IrisObject source = new IrisObject(1, 1, 1);
|
||||
TileData original = tile("minecraft:chest", "saved inventory");
|
||||
source.setUnsigned(0, 0, 0, state("minecraft:chest"));
|
||||
source.setUnsignedTile(0, 0, 0, original);
|
||||
|
||||
IrisObject scaled = source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(8, scaled.getBlocks().size());
|
||||
assertEquals(8, scaled.getStates().size());
|
||||
Set<TileData> copies = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> block : scaled.getBlocks()) {
|
||||
TileData copy = scaled.getStates().get(block.getKey());
|
||||
assertNotNull(copy);
|
||||
assertNotSame(original, copy);
|
||||
assertNotSame(original.getProperties(), copy.getProperties());
|
||||
assertEquals(original, copy);
|
||||
assertTrue(copies.add(copy));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reducedTilesStayWithTheWinningBlocks() {
|
||||
IrisObject source = new IrisObject(7, 1, 1);
|
||||
for (int x = 0; x < 7; x++) {
|
||||
String material = x % 2 == 0 ? "minecraft:chest" : "minecraft:barrel";
|
||||
source.setUnsigned(x, 0, 0, state(material));
|
||||
source.setUnsignedTile(x, 0, 0, tile(material, material));
|
||||
}
|
||||
|
||||
IrisObject scaled = source.scaled(0.5, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertTrue(scaled.getBlocks().size() < source.getBlocks().size());
|
||||
assertEquals(scaled.getBlocks().size(), scaled.getStates().size());
|
||||
for (Map.Entry<IrisBlockVector, PlatformBlockState> block : scaled.getBlocks()) {
|
||||
TileData copy = scaled.getStates().get(block.getKey());
|
||||
assertNotNull(copy);
|
||||
assertEquals(block.getValue().materialKey(), copy.getMaterialKey());
|
||||
assertEquals(block.getValue().materialKey(), copy.getProperties().get("name"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interpolatedTilesOnlyRemainOnTheirMaterial() {
|
||||
IrisObject source = new IrisObject(3, 3, 3);
|
||||
source.setUnsigned(1, 1, 1, state("minecraft:chest"));
|
||||
source.setUnsignedTile(1, 1, 1, tile("minecraft:chest", "saved inventory"));
|
||||
source.setUnsigned(2, 1, 1, state("minecraft:stone"));
|
||||
for (IrisObjectPlacementScaleInterpolator interpolation : new IrisObjectPlacementScaleInterpolator[] {
|
||||
IrisObjectPlacementScaleInterpolator.TRILINEAR,
|
||||
IrisObjectPlacementScaleInterpolator.TRICUBIC,
|
||||
IrisObjectPlacementScaleInterpolator.TRIHERMITE
|
||||
}) {
|
||||
IrisObject scaled = source.scaled(2, interpolation);
|
||||
|
||||
assertFalse(scaled.getStates().isEmpty());
|
||||
for (Map.Entry<IrisBlockVector, TileData> tile : scaled.getStates()) {
|
||||
PlatformBlockState block = scaled.getBlocks().get(tile.getKey());
|
||||
assertNotNull(block);
|
||||
assertEquals(tile.getValue().getMaterialKey(), block.materialKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompatibleSourceTileDoesNotSurviveScaling() {
|
||||
IrisObject source = new IrisObject(1, 1, 1);
|
||||
source.setUnsigned(0, 0, 0, state("minecraft:stone"));
|
||||
source.setUnsignedTile(0, 0, 0, tile("minecraft:chest", "saved inventory"));
|
||||
|
||||
IrisObject scaled = source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertTrue(scaled.getStates().isEmpty());
|
||||
assertEquals(1, source.getStates().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enlargedAsymmetricObjectKeepsItsSavedOrigin() {
|
||||
IrisObject source = asymmetricObject();
|
||||
|
||||
IrisObject scaled = source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(new Vector3i(10, 2, 4), scaled.getCenter());
|
||||
assertEquals(14, scaled.getW());
|
||||
assertEquals(10, scaled.getH());
|
||||
assertEquals(6, scaled.getD());
|
||||
assertEquals("minecraft:chest", scaled.getBlocks().get(new IrisBlockVector(0, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:chest", scaled.getBlocks().get(new IrisBlockVector(1, 1, 1)).materialKey());
|
||||
assertEquals("minecraft:stone", scaled.getBlocks().get(new IrisBlockVector(-8, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(2, 4, 0)).materialKey());
|
||||
assertEquals("origin", scaled.getStates().get(new IrisBlockVector(0, 0, 0)).getProperties().get("name"));
|
||||
assertEquals(-10, scaled.getAABB().min().getX());
|
||||
assertEquals(3, scaled.getAABB().max().getX());
|
||||
assertEquals(new Vector3i(5, 1, 2), source.getCenter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reducedAsymmetricObjectKeepsItsSavedOriginAndTiles() {
|
||||
IrisObject source = asymmetricObject();
|
||||
|
||||
IrisObject scaled = source.scaledAroundOrigin(0.5, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals("minecraft:chest", scaled.getBlocks().get(new IrisBlockVector(0, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:stone", scaled.getBlocks().get(new IrisBlockVector(-2, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(0, 1, 0)).materialKey());
|
||||
assertEquals("origin", scaled.getStates().get(new IrisBlockVector(0, 0, 0)).getProperties().get("name"));
|
||||
assertEquals(3, scaled.getBlocks().size());
|
||||
assertEquals(2, scaled.getStates().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalEnlargementScalesBothSidesOfTheSavedOrigin() {
|
||||
IrisObject source = new IrisObject(5, 1, 1);
|
||||
source.setUnsigned(0, 0, 0, state("minecraft:stone"));
|
||||
source.setUnsigned(2, 0, 0, state("minecraft:chest"));
|
||||
source.setUnsigned(4, 0, 0, state("minecraft:barrel"));
|
||||
|
||||
IrisObject scaled = source.scaledAroundOrigin(1.5, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals("minecraft:stone", scaled.getBlocks().get(new IrisBlockVector(-3, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:stone", scaled.getBlocks().get(new IrisBlockVector(-2, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:chest", scaled.getBlocks().get(new IrisBlockVector(0, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(3, 0, 0)).materialKey());
|
||||
assertEquals("minecraft:barrel", scaled.getBlocks().get(new IrisBlockVector(4, 0, 0)).materialKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void untypedTilesRemainAttachedToTheirScaledSourceMaterial() {
|
||||
IrisObject source = new IrisObject(1, 1, 1);
|
||||
TileData tile = mock(TileData.class);
|
||||
when(tile.clone()).thenReturn(tile);
|
||||
source.setUnsigned(0, 0, 0, state("minecraft:chest"));
|
||||
source.setUnsignedTile(0, 0, 0, tile);
|
||||
|
||||
IrisObject scaled = source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertEquals(8, scaled.getStates().size());
|
||||
}
|
||||
|
||||
private static IrisObject asymmetricObject() {
|
||||
IrisObject source = new IrisObject(7, 5, 3);
|
||||
source.setCenter(new Vector3i(5, 1, 2));
|
||||
source.setUnsigned(5, 1, 2, state("minecraft:chest"));
|
||||
source.setUnsignedTile(5, 1, 2, tile("minecraft:chest", "origin"));
|
||||
source.setUnsigned(1, 1, 2, state("minecraft:stone"));
|
||||
source.setUnsigned(6, 3, 2, state("minecraft:barrel"));
|
||||
source.setUnsignedTile(6, 3, 2, tile("minecraft:barrel", "upper"));
|
||||
return source;
|
||||
}
|
||||
|
||||
private static TileData tile(String material, String name) {
|
||||
KMap<String, Object> properties = new KMap<>();
|
||||
properties.put("name", name);
|
||||
return new TileData(material, properties);
|
||||
}
|
||||
|
||||
private static PlatformBlockState state(String key) {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(key);
|
||||
when(state.isAir()).thenReturn(key.contains("air"));
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisStaticObjectLayerTest {
|
||||
private final Map<String, PlatformBlockState> states = new HashMap<>();
|
||||
private IrisData data;
|
||||
private ResourceLoader<IrisObject> loader;
|
||||
private IrisObjectRotation.StateRotator previousRotator;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenAnswer(call -> state(call.getArgument(0)));
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
previousRotator = IrisObjectRotation.bindPlatformRotator((rotation, block, x, y, z) -> block);
|
||||
data = mock(IrisData.class);
|
||||
loader = mock(ResourceLoader.class);
|
||||
when(data.getObjectLoader()).thenReturn(loader);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
IrisObjectRotation.restorePlatformRotator(previousRotator);
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyConfigurationDoesNotLoadObjectsOrTouchWorldData() {
|
||||
IrisStaticObjectLayer layer = IrisStaticObjectLayer.compile(new IrisDimension(), data);
|
||||
Engine engine = mock(Engine.class);
|
||||
layer.apply(engine, 0, 0, Hunk.newArrayHunk(16, 384, 16));
|
||||
|
||||
assertTrue(layer.isEmpty());
|
||||
assertFalse(layer.contains(0, 0, 0));
|
||||
verifyNoInteractions(engine, loader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatedNegativeCoordinatesAndMinusOneYUseExactWorldOrigin() {
|
||||
IrisObject object = object("cross", 5, 3, 5);
|
||||
object.getBlocks().put(new IrisBlockVector(-2, -1, 0), state("minecraft:gold_block"));
|
||||
object.getBlocks().put(new IrisBlockVector(2, 1, 0), state("minecraft:diamond_block"));
|
||||
object.getBlocks().put(new IrisBlockVector(0, 0, 2), state("minecraft:emerald_block"));
|
||||
IrisStaticObject entry = entry("cross", 15, -1, -16)
|
||||
.setRotation(new IrisStaticObjectRotation().setY(90));
|
||||
IrisStaticObjectLayer layer = compile(entry);
|
||||
|
||||
assertTrue(layer.contains(15, 62, -14));
|
||||
assertTrue(layer.contains(15, 64, -18));
|
||||
assertTrue(layer.contains(17, 63, -16));
|
||||
assertEquals(1, layer.blocks(0, -1).size());
|
||||
assertEquals(1, layer.blocks(0, -2).size());
|
||||
assertEquals(1, layer.blocks(1, -1).size());
|
||||
assertEquals(62, layer.blocks(0, -1).getFirst().y());
|
||||
assertEquals(3, object.getBlocks().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void laterEntryAndBoreClearEarlierBlocksAndTiles() {
|
||||
IrisObject chest = object("chest", 1, 1, 1);
|
||||
chest.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:chest"));
|
||||
chest.getStates().put(new IrisBlockVector(0, 0, 0), new TileData("minecraft:chest", new KMap<>()));
|
||||
IrisObject replacement = object("replacement", 3, 1, 1);
|
||||
replacement.getBlocks().put(new IrisBlockVector(-1, 0, 0), state("minecraft:stone"));
|
||||
replacement.getBlocks().put(new IrisBlockVector(1, 0, 0), state("minecraft:stone"));
|
||||
IrisStaticObjectLayer layer = compile(entry("chest", 0, 100, 0),
|
||||
entry("replacement", 0, 100, 0).setBore(true));
|
||||
|
||||
IrisStaticObjectLayer.Block center = block(layer, 0, 164, 0);
|
||||
assertSame(IrisObject.States.AIR, center.state());
|
||||
assertNull(center.tile());
|
||||
assertEquals(1, chest.getStates().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkWritesStayLocalAndRemoveReplacedTileMetadata() {
|
||||
IrisObject object = object("line", 3, 1, 1);
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
object.getBlocks().put(new IrisBlockVector(x, 0, 0), state("minecraft:stone"));
|
||||
}
|
||||
IrisStaticObjectLayer layer = compile(entry("line", 15, 0, -1));
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter section = mock(Matter.class);
|
||||
MatterSlice<TileWrapper> tiles = mock(MatterSlice.class);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
when(mantle.getChunk(anyInt(), anyInt())).thenReturn(chunk);
|
||||
when(chunk.getOrCreate(anyInt())).thenReturn(section);
|
||||
when(section.getSlice(TileWrapper.class)).thenReturn(tiles);
|
||||
Hunk<PlatformBlockState> left = Hunk.newArrayHunk(16, 384, 16);
|
||||
Hunk<PlatformBlockState> right = Hunk.newArrayHunk(16, 384, 16);
|
||||
|
||||
layer.apply(engine, 16, -16, right);
|
||||
layer.apply(engine, 0, -16, left);
|
||||
|
||||
assertSame(state("minecraft:stone"), left.get(14, 64, 15));
|
||||
assertSame(state("minecraft:stone"), left.get(15, 64, 15));
|
||||
assertSame(state("minecraft:stone"), right.get(0, 64, 15));
|
||||
assertNull(right.get(1, 64, 15));
|
||||
verify(tiles).set(0, 0, 15, null);
|
||||
verify(tiles).set(14, 0, 15, null);
|
||||
verify(tiles).set(15, 0, 15, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementKeepsSavedTilesWithoutSharingTheirInstances() {
|
||||
IrisObject object = object("chest", 1, 1, 1);
|
||||
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:chest"));
|
||||
TileData tile = new TileData("minecraft:chest", new KMap<>());
|
||||
object.getStates().put(new IrisBlockVector(0, 0, 0), tile);
|
||||
IrisStaticObjectLayer layer = compile(entry("chest", 100, 100, -100));
|
||||
|
||||
assertEquals(tile, block(layer, 100, 164, -100).tile());
|
||||
assertNotSame(tile, block(layer, 100, 164, -100).tile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingObjectsAndTransformedHeightOverflow() {
|
||||
assertThrows(IllegalArgumentException.class, () -> compile(entry("missing", 0, 0, 0)));
|
||||
IrisObject object = object("tall", 1, 3, 1);
|
||||
object.getBlocks().put(new IrisBlockVector(0, 1, 0), state("minecraft:stone"));
|
||||
|
||||
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
|
||||
() -> compile(entry("tall", 0, 319, 0)));
|
||||
assertTrue(failure.getMessage().contains("staticObjects[0]"));
|
||||
assertTrue(failure.getMessage().contains("outside the dimension height"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedCompilationDoesNotChangeSavedBlocksOrPlacement() {
|
||||
IrisObject object = object("line", 3, 1, 1);
|
||||
object.getBlocks().put(new IrisBlockVector(-1, 0, 0), state("minecraft:stone"));
|
||||
object.getBlocks().put(new IrisBlockVector(1, 0, 0), state("minecraft:stone"));
|
||||
IrisStaticObject entry = entry("line", 100, 100, -100).setBore(true).setSeed(948L);
|
||||
IrisStaticObjectLayer first = compile(entry);
|
||||
IrisStaticObjectLayer second = compile(entry);
|
||||
|
||||
assertEquals(first.blocks(6, -7), second.blocks(6, -7));
|
||||
assertEquals(2, object.getBlocks().size());
|
||||
}
|
||||
|
||||
private IrisStaticObjectLayer compile(IrisStaticObject... entries) {
|
||||
return IrisStaticObjectLayer.compile(new IrisDimension().setStaticObjects(new KList<>(entries)), data);
|
||||
}
|
||||
|
||||
private IrisObject object(String key, int width, int height, int depth) {
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
object.setLoadKey(key);
|
||||
object.setLoader(data);
|
||||
when(loader.load(key)).thenReturn(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
private static IrisStaticObject entry(String key, int x, int y, int z) {
|
||||
return new IrisStaticObject().setObject(key).setPosition(new IrisPosition(x, y, z));
|
||||
}
|
||||
|
||||
private static IrisStaticObjectLayer.Block block(IrisStaticObjectLayer layer, int x, int y, int z) {
|
||||
List<IrisStaticObjectLayer.Block> blocks = layer.blocks(x >> 4, z >> 4);
|
||||
return blocks.stream().filter(block -> block.x() == (x & 15) && block.y() == y && block.z() == (z & 15))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private PlatformBlockState state(String key) {
|
||||
return states.computeIfAbsent(key.toLowerCase(), value -> {
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
when(block.key()).thenReturn(value);
|
||||
when(block.materialKey()).thenReturn(value);
|
||||
when(block.isAir()).thenReturn(value.contains("air"));
|
||||
when(block.isSolid()).thenReturn(!value.contains("air"));
|
||||
when(block.isOccluding()).thenReturn(!value.contains("air"));
|
||||
when(block.placementBaseState()).thenReturn(block);
|
||||
return block;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.Gson;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisStaticObjectTest {
|
||||
@Test
|
||||
public void defaultsKeepTheSavedOrientationAndOrigin() {
|
||||
IrisStaticObject object = validObject();
|
||||
object.validate(-64, 320);
|
||||
IrisObjectPlacement placement = object.toPlacement();
|
||||
|
||||
assertEquals(List.of("landmarks/tower"), placement.getPlace());
|
||||
assertEquals(ObjectPlaceMode.STRUCTURE_PIECE, placement.getMode());
|
||||
assertTrue(placement.isForcePlace());
|
||||
assertFalse(placement.getRotation().isEnabled());
|
||||
assertFalse(placement.getScale().shouldScale());
|
||||
assertEquals(new IrisPosition(100, 100, -100), object.getPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fixedRotationsPreserveSignedFractionalAnglesForEverySpin() {
|
||||
IrisStaticObject object = validObject().setRotation(new IrisStaticObjectRotation()
|
||||
.setX(-22.5).setY(90).setZ(-180));
|
||||
object.validate(-64, 320);
|
||||
IrisObjectRotation rotation = object.toPlacement().getRotation();
|
||||
|
||||
for (int spin : new int[]{0, 17, -222, 359}) {
|
||||
assertEquals(Math.toRadians(-22.5), rotation.getXRotation(spin), 0D);
|
||||
assertEquals(Math.toRadians(90), rotation.getYRotation(spin), 0D);
|
||||
assertEquals(Math.toRadians(-180), rotation.getZRotation(spin), 0D);
|
||||
}
|
||||
assertFalse(rotation.getXAxis().isForceLock());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementsKeepEditsAndFixedScaleWithoutSharingMutableLists() {
|
||||
IrisObjectReplace edit = new IrisObjectReplace().setFind(new KList<>(new IrisBlockData("stone")));
|
||||
IrisStaticObject object = validObject()
|
||||
.setScale(2.5)
|
||||
.setScaleInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR)
|
||||
.setEdit(new KList<>(edit))
|
||||
.setBore(true)
|
||||
.setSmartBore(true);
|
||||
object.validate(-64, 320);
|
||||
IrisObjectPlacement placement = object.toPlacement();
|
||||
|
||||
assertEquals(2.5, placement.getScale().getSize(), 0D);
|
||||
assertEquals(IrisObjectPlacementScaleInterpolator.TRILINEAR, placement.getScale().getInterpolation());
|
||||
assertTrue(placement.isBore());
|
||||
assertTrue(placement.isSmartBore());
|
||||
assertEquals(object.getEdit(), placement.getEdit());
|
||||
assertNotSame(object.getEdit(), placement.getEdit());
|
||||
placement.getEdit().clear();
|
||||
assertEquals(1, object.getEdit().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsMissingFieldsInvalidTransformsAndOutOfWorldOrigins() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new IrisStaticObject().validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class, () -> validObject().setPosition(null).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(Double.NaN).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(0).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class, () -> validObject().setScale(51).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class, () -> validObject().setRotation(null).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> validObject().setRotation(new IrisStaticObjectRotation().setY(-361)).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> validObject().setPosition(new IrisPosition(0, 320, 0)).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> validObject().setPosition(new IrisPosition(-29_999_985, 0, 0)).validate(-64, 320));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> validObject().setEdit(new KList<>(new IrisObjectReplace())).validate(-64, 320));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jsonRoundTripKeepsTheLongSeedAndAllFixedSettings() {
|
||||
IrisStaticObject source = validObject().setSeed(Long.MAX_VALUE)
|
||||
.setRotation(new IrisStaticObjectRotation().setY(-45.5)).setScale(0.5).setBore(true);
|
||||
Gson gson = new Gson();
|
||||
IrisStaticObject copy = gson.fromJson(gson.toJson(source), IrisStaticObject.class);
|
||||
|
||||
copy.validate(-64, 320);
|
||||
assertEquals(source, copy);
|
||||
assertEquals(Long.MAX_VALUE, copy.getSeed());
|
||||
}
|
||||
|
||||
private static IrisStaticObject validObject() {
|
||||
return new IrisStaticObject().setObject("landmarks/tower").setPosition(new IrisPosition(100, 100, -100));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user