Shared object placement layout

- Object placement bakes split into one per-object layout (palette, tiles, canopy) and per-variant offsets
- Variants beyond the per-object limit are built transiently instead of clearing the variant map
- High-variant golden and no-rebuild tests
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 04:00:16 -04:00
1 parent d7cad729e4
commit bf9e65252c
4 files changed
+244 -121

No files matched your search

@@ -4,9 +4,16 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.testsupport.KeyedBlockState;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ObjectPlacementBakeTest {
@Test
@@ -65,6 +72,57 @@ public class ObjectPlacementBakeTest {
assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
}
@Test
public void highVariantPlacementsKeepCachedVariantsAndNeverRebuildTheLayout() {
IrisObject object = object();
IrisObjectRotation pine = new IrisObjectRotation();
pine.setEnabled(true);
pine.setXAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
pine.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 270D, 90D));
pine.setZAxis(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
IrisBlockVector translate = new IrisBlockVector(0D, -6D, 0D);
Random random = new Random(1444L);
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(pine, 0, 0, 0), false, translate);
Map<ObjectPlacementBake.Key, ObjectPlacementBake> settled = null;
Set<ObjectPlacementBake.Key> distinct = new HashSet<>();
for (int placement = 0; placement < 4000; placement++) {
SpinKernel spin = new SpinKernel(pine, random.nextInt(360), random.nextInt(360), random.nextInt(360));
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, false, translate);
distinct.add(ObjectPlacementBake.Key.of(spin, false, translate));
assertSame(first.palette(), bake.palette());
assertTrue(object.placementBakes.size() <= 32);
assertTransformed(object, spin, translate, bake);
if (placement == 2000) {
settled = new HashMap<>(object.placementBakes);
}
}
assertTrue(distinct.size() > 40);
assertEquals(32, settled.size());
assertEquals(settled.keySet(), object.placementBakes.keySet());
for (Map.Entry<ObjectPlacementBake.Key, ObjectPlacementBake> entry : settled.entrySet()) {
assertSame(entry.getValue(), object.placementBakes.get(entry.getKey()));
}
}
private static void assertTransformed(IrisObject object, SpinKernel spin, IrisBlockVector translate, ObjectPlacementBake bake) {
double[] exact = bake.exact(object, spin);
int index = 0;
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector expected = source.clone();
spin.rotate(expected);
expected.add(translate);
assertEquals((int) Math.round(expected.getX()), bake.offsetX(index));
assertEquals((int) Math.round(expected.getY()), bake.offsetY(index));
assertEquals((int) Math.round(expected.getZ()), bake.offsetZ(index));
assertEquals(expected.getBlockY(), bake.layerY(index));
assertEquals(expected.getX(), exact[index * 3], 0D);
assertEquals(expected.getY(), exact[index * 3 + 1], 0D);
assertEquals(expected.getZ(), exact[index * 3 + 2], 0D);
index++;
}
assertEquals(bake.count(), index);
}
private static IrisObject object() {
IrisObject object = new IrisObject(5, 5, 5);
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
@@ -65,6 +65,7 @@ public class ObjectPlacementGoldenTest {
private static final int SEEDS = 5;
private static final long GOLDEN = 0x408f9d4113015897L;
private static final long MARKER_GOLDEN = 0x60b4bbf1144b239cL;
private static final long HIGH_VARIANT_GOLDEN = 0xf017622340ecac42L;
private static final List<String> PALETTE = List.of(
"minecraft:stone",
@@ -229,6 +230,34 @@ public class ObjectPlacementGoldenTest {
assertEquals(Long.toHexString(MARKER_GOLDEN), Long.toHexString(combined));
}
@Test
public void highVariantPlacementsMatchTheRecordedBlockStream() {
Random random = new Random(1444L);
IrisObjectRotation pine = rotation(new IrisAxisRotationClamp(true, false, -9D, 9D, 1D),
new IrisAxisRotationClamp(true, false, 0D, 270D, 90D), new IrisAxisRotationClamp(true, false, -9D, 9D, 1D));
long combined = 29L;
int writes = 0;
for (int index = 0; index < 16; index++) {
IrisObject object = randomObject(random, index);
object.setLoader(objectData);
IrisObjectPlacement placement = randomPlacement(random).toPlacement("pine");
placement.setRotation(pine);
for (int seed = 0; seed < 150; seed++) {
RNG rng = new RNG(index * 104_729L + seed);
GoldenPlacer placer = new GoldenPlacer(engine, (seed & 1) == 0);
int y = seed % 3 == 0 ? 90 + (seed & 7) : -1;
int result = object.place(300 + seed, y, -400 + index * 7, placer, placement, rng, null, null, data);
long hash = mix(placer.hash, result);
hash = mix(hash, rng.nextLong());
combined = mix(combined, hash);
writes += placer.writes;
}
assertTrue(object.placementBakes.size() <= 32);
}
assertTrue(writes > 10_000);
assertEquals(Long.toHexString(HIGH_VARIANT_GOLDEN), Long.toHexString(combined));
}
private static long mix(long hash, long value) {
long mixed = (hash ^ value) * 0x9E3779B97F4A7C15L;
return mixed ^ (mixed >>> 29);