This commit is contained in:
Brian Neumann-Fopiano
2026-07-16 12:26:01 -04:00
parent 55b7460b9a
commit 699247b1de
327 changed files with 30138 additions and 3934 deletions
@@ -19,6 +19,8 @@
package art.arcane.iris.probe;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
@@ -122,7 +124,7 @@ public final class GenerationProbe {
}
public static void main(String[] args) throws Exception {
IrisPlatforms.bind(new StubPlatform());
StubPlatform.bindGenerationStateHandlers();
StubPlatform.verbose(true);
StubPlatform.errorSink(REPORTED::add);
IrisServices.register(PreservationRegistry.class, new InertPreservation());
@@ -132,6 +134,8 @@ public final class GenerationProbe {
File packSource = new File(args[0]);
int radius = args.length > 1 ? Integer.parseInt(args[1]) : 2;
int centerChunkX = args.length > 2 ? Integer.parseInt(args[2]) : 0;
int centerChunkZ = args.length > 3 ? Integer.parseInt(args[3]) : 0;
if (!packSource.isDirectory()) {
System.out.println("[genprobe] pack folder not found: " + packSource.getAbsolutePath());
System.exit(1);
@@ -141,8 +145,25 @@ public final class GenerationProbe {
File pack = clonePack(packSource, workRoot);
System.out.println("[genprobe] pack: " + packSource.getAbsolutePath());
System.out.println("[genprobe] work copy: " + pack.getAbsolutePath());
System.out.println("[genprobe] center chunk: " + centerChunkX + "," + centerChunkZ);
System.out.println("[genprobe] radius: " + radius + " (" + ((2 * radius + 1) * (2 * radius + 1)) + " chunks)");
PackValidationResult validation = PackValidator.validate(pack);
for (String warning : validation.getWarnings()) {
System.out.println("[genprobe] pack warning: " + warning);
}
if (!validation.isLoadable()) {
for (String error : validation.getBlockingErrors()) {
System.out.println("[genprobe] pack error: " + error);
}
System.out.println("[genprobe] FAIL: pack validation blocked generation");
System.exit(1);
return;
}
System.out.println("[genprobe] pack validation: PASS");
IrisPlatforms.bind(new StubPlatform());
Engine engine;
try {
IrisData data = IrisData.get(pack);
@@ -184,8 +205,8 @@ public final class GenerationProbe {
int ok = 0;
int failed = 0;
for (int cz = -radius; cz <= radius; cz++) {
for (int cx = -radius; cx <= radius; cx++) {
for (int cz = centerChunkZ - radius; cz <= centerChunkZ + radius; cz++) {
for (int cx = centerChunkX - radius; cx <= centerChunkX + radius; cx++) {
String at = cx + "," + cz;
drainReported();
List<Throwable> failures = new ArrayList<>();
@@ -18,6 +18,9 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.object.BlockDataMergeSupport;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
@@ -30,9 +33,12 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import art.arcane.iris.util.common.math.IrisBlockVector;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
@@ -49,6 +55,25 @@ public final class StubPlatform implements IrisPlatform {
ERROR_SINK = sink;
}
public static void bindGenerationStateHandlers() {
IrisObjectRotation.bindPlatformRotator(StubPlatform::rotateState);
BlockDataMergeSupport.bindPlatformMerger(StubPlatform::mergeStates);
TileData.bindPlatformReader(StubTileData::read);
TileData.bindPlatformFactory(StubTileData::fromProperties);
}
static PlatformBlockState rotateForTest(IrisObjectRotation rotation, PlatformBlockState state) {
return rotateState(rotation, state, 0, 0, 0);
}
static PlatformBlockState mergeForTest(PlatformBlockState base, PlatformBlockState update) {
return mergeStates(base, update);
}
static PlatformBlockState blockStateForTest(String key) {
return StubBlockState.of(key);
}
private final StubRegistries registries = new StubRegistries();
private final StubScheduler scheduler = new StubScheduler();
private final StubStructureHooks structureHooks = new StubStructureHooks();
@@ -193,7 +218,9 @@ public final class StubPlatform implements IrisPlatform {
@Override
public PlatformBlockState withProperty(String name, String value) {
return this;
ParsedState parsed = ParsedState.parse(key);
parsed.properties().put(normalizeProperty(name), normalizeProperty(value));
return of(parsed.serialize());
}
@Override
@@ -202,6 +229,184 @@ public final class StubPlatform implements IrisPlatform {
}
}
private static PlatformBlockState rotateState(IrisObjectRotation rotation, PlatformBlockState state,
int spinX, int spinY, int spinZ) {
if (state == null || rotation == null || !rotation.canRotate()) {
return state;
}
ParsedState parsed = ParsedState.parse(state.key());
Map<String, String> properties = parsed.properties();
if (properties.containsKey("facing")) {
properties.put("facing", rotateFace(rotation, properties.get("facing"), spinX, spinY, spinZ));
} else if (properties.containsKey("rotation")) {
properties.put("rotation", rotateSegment(rotation, properties.get("rotation"), spinX, spinY, spinZ));
} else if (properties.containsKey("axis")) {
properties.put("axis", rotateAxis(rotation, properties.get("axis"), spinX, spinY, spinZ));
} else {
rotateFaceProperties(rotation, properties, spinX, spinY, spinZ);
}
return StubBlockState.of(parsed.serialize());
}
private static PlatformBlockState mergeStates(PlatformBlockState base, PlatformBlockState update) {
if (base == null) {
return update;
}
if (update == null) {
return base;
}
ParsedState parsedBase = ParsedState.parse(base.key());
ParsedState parsedUpdate = ParsedState.parse(update.key());
if (!parsedBase.blockKey().equals(parsedUpdate.blockKey())) {
return update;
}
parsedBase.properties().putAll(parsedUpdate.properties());
return StubBlockState.of(parsedBase.serialize());
}
private static String rotateFace(IrisObjectRotation rotation, String face,
int spinX, int spinY, int spinZ) {
IrisBlockVector vector = faceVector(face);
if (vector == null) {
return face;
}
return faceName(rotation.rotate(vector, spinX, spinY, spinZ));
}
private static String rotateAxis(IrisObjectRotation rotation, String axis,
int spinX, int spinY, int spinZ) {
IrisBlockVector vector = switch (axis) {
case "x" -> new IrisBlockVector(1, 0, 0);
case "y" -> new IrisBlockVector(0, 1, 0);
case "z" -> new IrisBlockVector(0, 0, 1);
default -> null;
};
if (vector == null) {
return axis;
}
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
double x = Math.abs(rotated.getX());
double y = Math.abs(rotated.getY());
double z = Math.abs(rotated.getZ());
if (x >= y && x >= z) {
return "x";
}
return y >= z ? "y" : "z";
}
private static String rotateSegment(IrisObjectRotation rotation, String value,
int spinX, int spinY, int spinZ) {
int segment;
try {
segment = Math.floorMod(Integer.parseInt(value), 16);
} catch (NumberFormatException e) {
return value;
}
double angle = segment * Math.PI * 2D / 16D;
IrisBlockVector vector = new IrisBlockVector(-Math.sin(angle), 0D, Math.cos(angle));
IrisBlockVector rotated = rotation.rotate(vector, spinX, spinY, spinZ);
if (Math.abs(rotated.getY()) > Math.max(Math.abs(rotated.getX()), Math.abs(rotated.getZ()))) {
return value;
}
double rotatedAngle = Math.atan2(-rotated.getX(), rotated.getZ());
int rotatedSegment = (int) Math.round(rotatedAngle * 16D / (Math.PI * 2D));
return Integer.toString(Math.floorMod(rotatedSegment, 16));
}
private static void rotateFaceProperties(IrisObjectRotation rotation, Map<String, String> properties,
int spinX, int spinY, int spinZ) {
List<String> faces = List.of("north", "east", "south", "west", "up", "down");
int present = 0;
for (String face : faces) {
if (properties.containsKey(face)) {
present++;
}
}
if (present < 2) {
return;
}
Map<String, String> rotated = new LinkedHashMap<>();
for (String face : faces) {
String value = properties.get(face);
if (value != null) {
rotated.put(rotateFace(rotation, face, spinX, spinY, spinZ), value);
}
}
for (String face : faces) {
if (properties.containsKey(face)) {
String defaultValue = "true".equals(properties.get(face)) || "false".equals(properties.get(face))
? "false" : "none";
properties.put(face, rotated.getOrDefault(face, defaultValue));
}
}
}
private static IrisBlockVector faceVector(String face) {
return switch (face) {
case "north" -> new IrisBlockVector(0, 0, -1);
case "east" -> new IrisBlockVector(1, 0, 0);
case "south" -> new IrisBlockVector(0, 0, 1);
case "west" -> new IrisBlockVector(-1, 0, 0);
case "up" -> new IrisBlockVector(0, 1, 0);
case "down" -> new IrisBlockVector(0, -1, 0);
default -> null;
};
}
private static String faceName(IrisBlockVector vector) {
double x = Math.abs(vector.getX());
double y = Math.abs(vector.getY());
double z = Math.abs(vector.getZ());
if (x >= y && x >= z) {
return vector.getX() >= 0D ? "east" : "west";
}
if (y >= z) {
return vector.getY() >= 0D ? "up" : "down";
}
return vector.getZ() >= 0D ? "south" : "north";
}
private static String normalizeProperty(String value) {
return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
}
private record ParsedState(String blockKey, LinkedHashMap<String, String> properties) {
private static ParsedState parse(String key) {
String normalized = key == null ? "minecraft:air" : key.trim().toLowerCase(Locale.ROOT);
int open = normalized.indexOf('[');
if (open < 0 || !normalized.endsWith("]")) {
return new ParsedState(normalized, new LinkedHashMap<>());
}
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
String body = normalized.substring(open + 1, normalized.length() - 1);
if (!body.isBlank()) {
for (String property : body.split(",")) {
int separator = property.indexOf('=');
if (separator > 0 && separator < property.length() - 1) {
properties.put(property.substring(0, separator), property.substring(separator + 1));
}
}
}
return new ParsedState(normalized.substring(0, open), properties);
}
private String serialize() {
if (properties.isEmpty()) {
return blockKey;
}
StringBuilder serialized = new StringBuilder(blockKey).append('[');
boolean first = true;
for (Map.Entry<String, String> entry : properties.entrySet()) {
if (!first) {
serialized.append(',');
}
serialized.append(entry.getKey()).append('=').append(entry.getValue());
first = false;
}
return serialized.append(']').toString();
}
}
private static final class StubBiome implements PlatformBiome {
private static final ConcurrentHashMap<String, StubBiome> CACHE = new ConcurrentHashMap<>();
private final String key;
@@ -0,0 +1,260 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.Strictness;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class StubTileData extends TileData {
private static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.setStrictness(Strictness.LENIENT)
.create();
private final String blockKey;
private final KMap<String, Object> tileProperties;
private final byte[] binary;
private StubTileData(String blockKey, KMap<String, Object> tileProperties, byte[] binary) {
super();
this.blockKey = normalizeBlockKey(blockKey);
this.tileProperties = tileProperties.copy();
this.binary = Arrays.copyOf(binary, binary.length);
}
public static StubTileData read(DataInputStream input) throws IOException {
if (!input.markSupported()) {
throw new IOException("Probe tile-data input does not support mark/reset");
}
input.mark(Integer.MAX_VALUE);
try {
return readModern(input);
} catch (IOException | RuntimeException error) {
input.reset();
return readLegacy(input);
} finally {
input.mark(0);
}
}
static StubTileData fromProperties(PlatformBlockState state, KMap<String, Object> properties) {
if (state == null) {
throw new IllegalArgumentException("Probe tile data requires a block state");
}
String blockKey = normalizeBlockKey(state.placementBaseState().key());
KMap<String, Object> copied = properties == null ? new KMap<>() : properties.copy();
return new StubTileData(blockKey, copied, encodeModern(blockKey, copied));
}
private static StubTileData readModern(DataInputStream input) throws IOException {
String blockKey = input.readUTF();
if (!isResourceKey(blockKey)) {
throw new IOException("Probe tile data has an invalid block key: " + blockKey);
}
KMap<String, Object> properties = properties(input.readUTF());
if (properties == null) {
throw new IOException("Probe tile data has invalid properties for " + blockKey);
}
return new StubTileData(blockKey, properties, encodeModern(blockKey, properties));
}
private static StubTileData readLegacy(DataInputStream input) throws IOException {
int type = input.readUnsignedShort();
return switch (type) {
case 0 -> readLegacySign(input, type);
case 1 -> readLegacySpawner(input, type);
case 2 -> readLegacyBanner(input, type);
case 3 -> readLegacyLootable(input, type);
default -> throw new IOException("Unknown probe tile-data type: " + type);
};
}
private static StubTileData readLegacySign(DataInputStream input, int type) throws IOException {
List<String> lines = List.of(
input.readUTF(), input.readUTF(), input.readUTF(), input.readUTF());
int color = input.readUnsignedByte();
KMap<String, Object> properties = new KMap<>();
properties.put("lines", lines);
properties.put("color", color);
byte[] binary = encode(output -> {
output.writeShort(type);
for (String line : lines) {
output.writeUTF(line);
}
output.writeByte(color);
});
return new StubTileData("minecraft:oak_sign", properties, binary);
}
private static StubTileData readLegacySpawner(DataInputStream input, int type) throws IOException {
input.mark(Integer.MAX_VALUE);
String entityKey = null;
try {
String candidate = input.readUTF();
if (isResourceKey(candidate)) {
entityKey = candidate;
} else {
input.reset();
}
} catch (IOException | RuntimeException error) {
input.reset();
}
int legacyOrdinal = -1;
if (entityKey == null) {
legacyOrdinal = input.readShort();
}
KMap<String, Object> properties = new KMap<>();
properties.put("entity", entityKey == null ? legacyOrdinal : entityKey);
String resolvedEntityKey = entityKey;
int resolvedLegacyOrdinal = legacyOrdinal;
byte[] binary = encode(output -> {
output.writeShort(type);
if (resolvedEntityKey == null) {
output.writeShort(resolvedLegacyOrdinal);
} else {
output.writeUTF(resolvedEntityKey);
}
});
return new StubTileData("minecraft:spawner", properties, binary);
}
private static StubTileData readLegacyBanner(DataInputStream input, int type) throws IOException {
int baseColor = input.readUnsignedByte();
int patternCount = input.readUnsignedByte();
input.mark(Integer.MAX_VALUE);
List<BannerPattern> patterns = new ArrayList<>(patternCount);
boolean keyed = true;
try {
for (int i = 0; i < patternCount; i++) {
int color = input.readUnsignedByte();
String pattern = input.readUTF();
if (!isResourceKey(pattern)) {
throw new IOException("Invalid probe banner pattern: " + pattern);
}
patterns.add(new BannerPattern(color, pattern, -1));
}
} catch (IOException | RuntimeException error) {
keyed = false;
patterns.clear();
input.reset();
for (int i = 0; i < patternCount; i++) {
patterns.add(new BannerPattern(input.readUnsignedByte(), null, input.readUnsignedByte()));
}
}
KMap<String, Object> properties = new KMap<>();
properties.put("baseColor", baseColor);
properties.put("patterns", patterns);
boolean resolvedKeyed = keyed;
byte[] binary = encode(output -> {
output.writeShort(type);
output.writeByte(baseColor);
output.writeByte(patternCount);
for (BannerPattern pattern : patterns) {
output.writeByte(pattern.color());
if (resolvedKeyed) {
output.writeUTF(pattern.key());
} else {
output.writeByte(pattern.legacyOrdinal());
}
}
});
return new StubTileData("minecraft:white_banner", properties, binary);
}
private static StubTileData readLegacyLootable(DataInputStream input, int type) throws IOException {
String blockKey = input.readUTF();
String lootTable = input.readUTF();
long seed = input.readLong();
if (!isResourceKey(blockKey)) {
throw new IOException("Probe lootable tile has an invalid block key: " + blockKey);
}
KMap<String, Object> properties = new KMap<>();
properties.put("LootTable", lootTable);
properties.put("LootTableSeed", seed);
byte[] binary = encode(output -> {
output.writeShort(type);
output.writeUTF(blockKey);
output.writeUTF(lootTable);
output.writeLong(seed);
});
return new StubTileData(blockKey, properties, binary);
}
private static byte[] encodeModern(String blockKey, KMap<String, Object> properties) {
try {
return encode(output -> {
output.writeUTF(blockKey);
output.writeUTF(GSON.toJson(properties));
});
} catch (IOException error) {
throw new IllegalStateException("Failed to encode probe tile data for " + blockKey, error);
}
}
private static byte[] encode(BinaryWriter writer) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
writer.write(output);
}
return bytes.toByteArray();
}
@SuppressWarnings("unchecked")
private static KMap<String, Object> properties(String json) {
return GSON.fromJson(json, KMap.class);
}
private static boolean isResourceKey(String key) {
if (key == null || key.isBlank()) {
return false;
}
return key.matches("[a-z0-9_.-]+:[a-z0-9/._-]+");
}
private static String normalizeBlockKey(String blockKey) {
int properties = blockKey.indexOf('[');
return properties < 0 ? blockKey : blockKey.substring(0, properties);
}
String blockKey() {
return blockKey;
}
@Override
public KMap<String, Object> getProperties() {
return tileProperties;
}
@Override
public void toBinary(DataOutputStream output) throws IOException {
output.write(binary);
}
@Override
public TileData clone() {
return new StubTileData(blockKey, tileProperties, binary);
}
@Override
public String toString() {
return blockKey + GSON.toJson(tileProperties);
}
@FunctionalInterface
private interface BinaryWriter {
void write(DataOutputStream output) throws IOException;
}
private record BannerPattern(int color, String key, int legacyOrdinal) {
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public final class StubPlatformStateTest {
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
IrisPlatforms.bind(new StubPlatform());
StubPlatform.bindGenerationStateHandlers();
}
@Test
public void rotatesFacingAxisAndConnectedFaces() {
IrisObjectRotation rotation = IrisObjectRotation.of(0, 90, 0);
assertEquals("minecraft:oak_stairs[facing=west,half=bottom]",
StubPlatform.rotateForTest(rotation, state("minecraft:oak_stairs[facing=north,half=bottom]")).key());
assertEquals("minecraft:oak_log[axis=x]",
StubPlatform.rotateForTest(rotation, state("minecraft:oak_log[axis=z]")).key());
assertEquals("minecraft:oak_fence[north=false,east=false,south=false,west=true]",
StubPlatform.rotateForTest(rotation,
state("minecraft:oak_fence[north=true,east=false,south=false,west=false]")).key());
}
@Test
public void propertyUpdatesAndMergesPreserveCanonicalState() {
PlatformBlockState base = state("minecraft:oak_leaves[distance=7,persistent=false]");
PlatformBlockState updated = base.withProperty("distance", "2").withProperty("persistent", "true");
assertEquals("minecraft:oak_leaves[distance=2,persistent=true]", updated.key());
PlatformBlockState merged = StubPlatform.mergeForTest(
base, state("minecraft:oak_leaves[persistent=true]"));
assertEquals("minecraft:oak_leaves[distance=7,persistent=true]", merged.key());
}
private PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block(key);
}
}
@@ -0,0 +1,87 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class StubTileDataTest {
@Test
public void modernTilePayloadRoundTripsWithoutBukkit() throws Exception {
KMap<String, Object> properties = new KMap<>();
properties.put("LootTable", "minecraft:chests/ancient_city");
StubTileData original = StubTileData.fromProperties(
StubPlatform.blockStateForTest("minecraft:chest[facing=north]"), properties);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
original.toBinary(output);
}
StubTileData decoded;
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
decoded = StubTileData.read(input);
}
assertEquals("minecraft:chest", decoded.blockKey());
assertEquals("minecraft:chests/ancient_city", decoded.getProperties().get("LootTable"));
}
@Test
public void legacyLootablePayloadPreservesItsCompleteFrame() throws Exception {
byte[] payload = encode(output -> {
output.writeShort(3);
output.writeUTF("minecraft:chest");
output.writeUTF("minecraft:chests/ancient_city");
output.writeLong(998877L);
});
StubTileData decoded = decode(payload);
assertEquals("minecraft:chest", decoded.blockKey());
assertEquals("minecraft:chests/ancient_city", decoded.getProperties().get("LootTable"));
assertArrayEquals(payload, encode(decoded::toBinary));
}
@Test
public void legacySpawnerAndBannerFramesRemainUnambiguous() throws Exception {
byte[] ordinalSpawner = encode(output -> {
output.writeShort(1);
output.writeShort(42);
});
byte[] keyedBanner = encode(output -> {
output.writeShort(2);
output.writeByte(11);
output.writeByte(1);
output.writeByte(3);
output.writeUTF("minecraft:base");
});
assertArrayEquals(ordinalSpawner, encode(decode(ordinalSpawner)::toBinary));
assertArrayEquals(keyedBanner, encode(decode(keyedBanner)::toBinary));
}
private static StubTileData decode(byte[] payload) throws Exception {
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload))) {
return StubTileData.read(input);
}
}
private static byte[] encode(BinaryWriter writer) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
writer.write(output);
}
return bytes.toByteArray();
}
@FunctionalInterface
private interface BinaryWriter {
void write(DataOutputStream output) throws Exception;
}
}