Baked object placement

- Bake rotated block offsets, palette indexes, tiles and canopy bits per object and spin kernel
- Resolve decay, rotation, waterlog and air facts once per palette entry per placement
- Skip the destination read for waterloggable states that cannot become waterlogged
- Move SpinKernel to its own file
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 23:50:55 -04:00
1 parent b526b480a7
commit 943c8d67e5
6 files changed
+763 -320

No files matched your search

@@ -61,6 +61,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant {
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>(); protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>(); transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>(); transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
@Getter @Getter
@Setter @Setter
private transient TreeGrounding treeGrounding; private transient TreeGrounding treeGrounding;
@@ -52,6 +52,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import java.util.Comparator; import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -474,128 +475,52 @@ final class IrisObjectPlacementRunner {
int topLayer = Integer.MIN_VALUE; int topLayer = Integer.MIN_VALUE;
int vacuumLowest = Integer.MAX_VALUE; int vacuumLowest = Integer.MAX_VALUE;
int vacuumHighest = Integer.MIN_VALUE; int vacuumHighest = Integer.MIN_VALUE;
self.readLock.lock(); ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null);
PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz,
preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials);
double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null;
String[] markers = config.getMarkers().isNotEmpty() && placer.getEngine() != null
? resolveMarkers(config, rng, x, yv, z, rdata, bake) : null;
int[] tileBlocks = bake.tileBlocks();
TileData[] tileValues = bake.tileValues();
int tileCursor = 0;
int count = bake.count();
KMap<IrisBlockVector, String> markers = null; for (int index = 0; index < count; index++) {
int entry = bake.paletteIndex(index);
try {
VectorMap<NativeBlockState> blocks = self.blocks;
VectorMap<TileData> states = self.states;
TreeGrounding tree = self.getTreeGrounding();
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
boolean hasStates = !states.isEmpty();
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
markers = new KMap<>();
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
.collect(KList.collector());
// Marker selection persists into the mantle, so it must be seed-deterministic.
// Derive a side stream keyed on the placement position instead of consuming the
// caller's rng: consuming draws there would shift every later placement in the
// chunk and invalidate existing worlds/goldenhashes.
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
int markerIndex = 0;
for (IrisObjectMarker j : config.getMarkers()) {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++;
if (marker == null || marker.isCompatExcluded()) {
continue;
}
int max = j.getMaximumMarkers();
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
if (max <= 0) {
break;
}
NativeBlockState data = blocks.get(i);
if (data == null) {
continue;
}
for (NativeBlockState k : j.getMark(rdata)) {
if (max <= 0) {
break;
}
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
if (!marker.isEmptyAbove() || (a && fff)) {
markers.put(i, j.getMarker());
max--;
}
}
}
}
}
}
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
while (cursor.next()) {
IrisBlockVector g = cursor.key();
NativeBlockState d;
TileData tile = null; TileData tile = null;
if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) {
try { tile = tileValues[tileCursor++];
d = cursor.value();
if (hasStates) {
tile = states.get(g);
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)");
d = IrisObject.States.air();
} }
if (d == null) { if (stilting && palette.stiltLayer(entry)) {
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)"); int layer = bake.layerY(index);
d = IrisObject.States.air(); if (layer < lowest) {
lowest = layer;
} }
if (layer > topLayer) {
if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) { topLayer = layer;
d = IrisObject.States.vairDebug();
}
NativeBlockState data = d;
IrisBlockVector i = g.clone();
spin.rotate(i);
if (ceilingHang) {
i.setY(-i.getBlockY());
}
if (translating) {
i.add(translateOffset);
}
if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
if (i.getBlockY() < lowest) {
lowest = i.getBlockY();
}
if (i.getBlockY() > topLayer) {
topLayer = i.getBlockY();
} }
} }
if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) { NativeBlockState data = palette.decayed(entry);
data = data.withProperty("persistent", "true"); boolean edited = false;
}
if (hasEdits) { if (hasEdits) {
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
for (IrisObjectReplace j : config.getEdit()) { for (IrisObjectReplace j : config.getEdit()) {
if (rng.chance(j.getChance())) { if (rng.chance(j.getChance())) {
for (NativeBlockState k : j.getFind(rdata)) { for (NativeBlockState k : j.getFind(rdata)) {
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)); boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
if (sameMaterial && !(newData.isCustom() || data.isCustom())) if (sameMaterial && !(newData.isCustom() || data.isCustom()))
data = BlockDataMergeSupport.merge(data, newData); data = BlockDataMergeSupport.merge(data, newData);
else else
data = newData; data = newData;
edited = true;
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata); Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
if (t.isPresent()) { if (t.isPresent()) {
@@ -609,18 +534,21 @@ final class IrisObjectPlacementRunner {
} }
} }
data = config.getRotation().rotate(data, spinx, spiny, spinz); StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry);
if (data == null) { NativeBlockState rotated = facts.rotated();
if (rotated == null) {
continue; continue;
} }
xx = x + (int) Math.round(i.getX()); xx = x + bake.offsetX(index);
int yy = y + bake.offsetY(index);
int yy = y + (int) Math.round(i.getY()); zz = z + bake.offsetZ(index);
zz = z + (int) Math.round(i.getZ());
if (warped) { if (warped) {
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); double exactX = exact[index * 3];
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
} }
if (paint) { if (paint) {
@@ -628,8 +556,8 @@ final class IrisObjectPlacementRunner {
if (surface == IrisPaintSurfaceProjection.MISSING) { if (surface == IrisPaintSurfaceProjection.MISSING) {
continue; continue;
} }
if (!B.isVineBlock(data)) { if (!B.isVineBlock(rotated)) {
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand; yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand;
} }
} }
@@ -637,13 +565,18 @@ final class IrisObjectPlacementRunner {
continue; continue;
} }
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ()); boolean canopy = bake.canopy(index);
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) { if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
continue; continue;
} }
try { try {
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate); data = rotated;
if (facts.waterloggable()) {
data = waterlogCandidate || facts.authoredWaterlogged()
? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate)
: facts.dry();
}
if (!rawStructurePiece && B.isVineBlock(data)) { if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz); data = attachVineFaces(placer, data, xx, yy, zz);
@@ -652,9 +585,7 @@ final class IrisObjectPlacementRunner {
// Short-circuit order matters for cost only: the mantle read is paid solely // 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. // for vine blocks. Both operands are pure, so the value is unchanged.
boolean wouldReplace = !rawStructurePiece && 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 place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace);
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
if (data.isCustom() || place) { if (data.isCustom() || place) {
placer.set(xx, yy, zz, data); placer.set(xx, yy, zz, data);
@@ -668,8 +599,8 @@ final class IrisObjectPlacementRunner {
if (tile != null) { if (tile != null) {
placer.setTile(xx, yy, zz, tile); placer.setTile(xx, yy, zz, tile);
} }
if (markers != null && markers.containsKey(g)) { if (markers != null && markers[index] != null) {
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); placer.setData(xx, yy, zz, new MatterMarker(markers[index]));
} }
if (listener != null) { if (listener != null) {
listener.accept(new BlockPosition(xx, yy, zz), data); listener.accept(new BlockPosition(xx, yy, zz), data);
@@ -687,9 +618,6 @@ final class IrisObjectPlacementRunner {
} }
} }
} }
} finally {
self.readLock.unlock();
}
if (grounding != null) { if (grounding != null) {
for (TreeGrounding.Extension extension : grounding.extensions()) { for (TreeGrounding.Extension extension : grounding.extensions()) {
@@ -705,9 +633,6 @@ final class IrisObjectPlacementRunner {
} }
if (stilting) { if (stilting) {
self.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = self.blocks;
IrisStiltSettings settings = stiltSettings; IrisStiltSettings settings = stiltSettings;
double erodeCentroidX = 0; double erodeCentroidX = 0;
@@ -715,38 +640,21 @@ final class IrisObjectPlacementRunner {
double erodeMaxDist = 1; double erodeMaxDist = 1;
if (eroding) { if (eroding) {
int centroidCount = 0; int centroidCount = 0;
VectorMap<NativeBlockState>.Cursor centroidCursor = blocks.cursor(); for (int index = 0; index < count; index++) {
while (centroidCursor.next()) { if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
IrisBlockVector rot = centroidCursor.key().clone(); erodeCentroidX += exact[index * 3];
spin.rotate(rot); erodeCentroidZ += exact[index * 3 + 2];
if (translating) {
rot.add(translateOffset);
}
if (rot.getBlockY() == lowest) {
NativeBlockState bd = centroidCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
erodeCentroidX += rot.getX();
erodeCentroidZ += rot.getZ();
centroidCount++; centroidCount++;
} }
} }
}
if (centroidCount > 0) { if (centroidCount > 0) {
erodeCentroidX /= centroidCount; erodeCentroidX /= centroidCount;
erodeCentroidZ /= centroidCount; erodeCentroidZ /= centroidCount;
} }
VectorMap<NativeBlockState>.Cursor spreadCursor = blocks.cursor(); for (int index = 0; index < count; index++) {
while (spreadCursor.next()) { if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
IrisBlockVector rot = spreadCursor.key().clone(); double dx = exact[index * 3] - erodeCentroidX;
spin.rotate(rot); double dz = exact[index * 3 + 2] - erodeCentroidZ;
if (translating) {
rot.add(translateOffset);
}
if (rot.getBlockY() == lowest) {
NativeBlockState bd = spreadCursor.value();
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
double dx = rot.getX() - erodeCentroidX;
double dz = rot.getZ() - erodeCentroidZ;
double dist = Math.sqrt(dx * dx + dz * dz); double dist = Math.sqrt(dx * dx + dz * dz);
if (dist > erodeMaxDist) { if (dist > erodeMaxDist) {
erodeMaxDist = dist; erodeMaxDist = dist;
@@ -754,54 +662,29 @@ final class IrisObjectPlacementRunner {
} }
} }
} }
}
VectorMap<NativeBlockState>.Cursor stiltCursor = blocks.cursor(); int targetLayer = ceilingHang ? topLayer : lowest;
while (stiltCursor.next()) { for (int index = 0; index < count; index++) {
IrisBlockVector g = stiltCursor.key(); int entry = bake.paletteIndex(index);
NativeBlockState sourceData; if (!palette.shouldStilt(entry)) {
try {
sourceData = stiltCursor.value();
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)");
sourceData = IrisObject.States.air();
}
if (sourceData == null) {
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)");
sourceData = IrisObject.States.air();
}
if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
continue; continue;
} }
NativeBlockState d = sourceData; NativeBlockState d;
if (settings != null && settings.getPalette() != null) { if (settings != null && settings.getPalette() != null) {
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
} else { } else {
String mat = IrisObjectShaping.materialKey(d); d = palette.stiltDefault(entry);
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
d = IrisObject.States.dirt();
}
} }
IrisBlockVector i = g.clone(); if (bake.layerY(index) != targetLayer)
spin.rotate(i);
if (ceilingHang) {
i.setY(-i.getBlockY());
}
if (translating) {
i.add(translateOffset);
}
int targetLayer = ceilingHang ? topLayer : lowest;
if (i.getBlockY() != targetLayer)
continue; continue;
d = config.getRotation().rotate(d, spinx, spiny, spinz); d = palette.rotate(d);
if (hasEdits) { if (hasEdits) {
double exactX = exact[index * 3];
double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
for (IrisObjectReplace j : config.getEdit()) { for (IrisObjectReplace j : config.getEdit()) {
if (rng.chance(j.getChance())) { if (rng.chance(j.getChance())) {
for (NativeBlockState k : j.getFind(rdata)) { for (NativeBlockState k : j.getFind(rdata)) {
@@ -809,7 +692,7 @@ final class IrisObjectPlacementRunner {
continue; continue;
} }
if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) { if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) {
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) {
d = BlockDataMergeSupport.merge(d, newData); d = BlockDataMergeSupport.merge(d, newData);
@@ -822,15 +705,18 @@ final class IrisObjectPlacementRunner {
} }
} }
if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials)) if (d == null || !palette.shouldStiltPlaced(d))
continue; continue;
xx = x + (int) Math.round(i.getX()); xx = x + bake.offsetX(index);
zz = z + (int) Math.round(i.getZ()); zz = z + bake.offsetZ(index);
if (warped) { if (warped) {
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); double exactX = exact[index * 3];
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); double exactY = exact[index * 3 + 1];
double exactZ = exact[index * 3 + 2];
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
} }
if (organic) { if (organic) {
@@ -891,8 +777,6 @@ final class IrisObjectPlacementRunner {
int highest = placer.getHighest(xx, zz, self.getLoader(), true); int highest = placer.getHighest(xx, zz, self.getLoader(), true);
int lowerBound = highest - 1; int lowerBound = highest - 1;
if (settings != null) { if (settings != null) {
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
@@ -901,8 +785,8 @@ final class IrisObjectPlacementRunner {
} }
if (eroding) { if (eroding) {
double dx = i.getX() - erodeCentroidX; double dx = exact[index * 3] - erodeCentroidX;
double dz = i.getZ() - erodeCentroidZ; double dz = exact[index * 3 + 2] - erodeCentroidZ;
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
normalizedDist = Math.min(normalizedDist, 1.0); normalizedDist = Math.min(normalizedDist, 1.0);
int totalDepth = (lowest + y) - lowerBound; int totalDepth = (lowest + y) - lowerBound;
@@ -933,10 +817,6 @@ final class IrisObjectPlacementRunner {
} }
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
} }
}
} finally {
self.readLock.unlock();
} }
} }
@@ -974,9 +854,76 @@ final class IrisObjectPlacementRunner {
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) { if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
return state; return state;
} }
if (!enabled && !"true".equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
return IrisProceduralBlocks.normalizeWaterlogging(state, null, false);
}
return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled); return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled);
} }
private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata,
ObjectPlacementBake bake) {
self.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = self.blocks;
KMap<IrisBlockVector, String> markers = new KMap<>();
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
.collect(KList.collector());
// Marker selection persists into the mantle, so it must be seed-deterministic.
// Derive a side stream keyed on the placement position instead of consuming the
// caller's rng: consuming draws there would shift every later placement in the
// chunk and invalidate existing worlds/goldenhashes.
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
int markerIndex = 0;
for (IrisObjectMarker j : config.getMarkers()) {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++;
if (marker == null || marker.isCompatExcluded()) {
continue;
}
int max = j.getMaximumMarkers();
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
if (max <= 0) {
break;
}
NativeBlockState data = blocks.get(i);
if (data == null) {
continue;
}
for (NativeBlockState k : j.getMark(rdata)) {
if (max <= 0) {
break;
}
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
if (!marker.isEmptyAbove() || (a && fff)) {
markers.put(i, j.getMarker());
max--;
}
}
}
}
}
String[] resolved = new String[bake.count()];
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (index < resolved.length && cursor.next()) {
resolved[index++] = markers.get(cursor.key());
}
return resolved;
} finally {
self.readLock.unlock();
}
}
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset, private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
CNG warp, double half, int x, int y, int z) { CNG warp, double half, int x, int y, int z) {
KList<IrisBlockVector> offsets = new KList<>(); KList<IrisBlockVector> offsets = new KList<>();
@@ -1337,152 +1284,159 @@ final class IrisObjectPlacementRunner {
return result; return result;
} }
/** private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged,
* Precomputed vector rotation for a single placement. NativeBlockState dry, boolean air) {
* <p> private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false);
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
* the value the per-call form would have produced.
*/
private static final class SpinKernel {
private static final int MODE_NONE = 0;
private static final int MODE_FLIP = 1;
private static final int MODE_QUARTER = 2;
private static final int MODE_THREE_QUARTER = 3;
private static final int MODE_ANGLE = 4;
private final boolean rotates;
private final int xMode;
private final int yMode;
private final int zMode;
private final double xCos;
private final double xSin;
private final double yCos;
private final double ySin;
private final double zCos;
private final double zSin;
private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
rotates = rotation.canRotate();
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
xCos = Math.cos(xAngle);
xSin = Math.sin(xAngle);
zCos = Math.cos(zAngle);
zSin = Math.sin(zAngle);
yCos = Math.cos(yAngle);
ySin = Math.sin(yAngle);
} }
private static int modeOf(IrisAxisRotationClamp clamp) { private static final class PlacementStates {
if (!clamp.isLocked()) { private static final Object ROTATED_AWAY = new Object();
return MODE_ANGLE; private static final byte UNKNOWN = 0;
private static final byte NO = 1;
private static final byte YES = 2;
private final NativeBlockState[] palette;
private final IrisObjectRotation rotation;
private final int spinx;
private final int spiny;
private final int spinz;
private final boolean preventingDecay;
private final boolean debugSmartBore;
private final Set<String> excludedStiltMaterials;
private final NativeBlockState[] decayed;
private final StateFacts[] facts;
private final byte[] stiltLayers;
private final byte[] stilts;
private final NativeBlockState[] stiltDefaults;
private IdentityHashMap<NativeBlockState, Object> rotations;
private IdentityHashMap<NativeBlockState, StateFacts> editedFacts;
private IdentityHashMap<NativeBlockState, Boolean> placedStilts;
private PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz,
boolean preventingDecay, boolean debugSmartBore, Set<String> excludedStiltMaterials) {
this.palette = palette;
this.rotation = rotation;
this.spinx = spinx;
this.spiny = spiny;
this.spinz = spinz;
this.preventingDecay = preventingDecay;
this.debugSmartBore = debugSmartBore;
this.excludedStiltMaterials = excludedStiltMaterials;
this.decayed = new NativeBlockState[palette.length];
this.facts = new StateFacts[palette.length];
this.stiltLayers = new byte[palette.length];
this.stilts = new byte[palette.length];
this.stiltDefaults = new NativeBlockState[palette.length];
} }
if (Math.abs(clamp.getMax()) % 360D == 180D) { private NativeBlockState source(int entry) {
return MODE_FLIP; NativeBlockState state = palette[entry];
return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state;
} }
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) { private boolean stiltLayer(int entry) {
return MODE_QUARTER; byte known = stiltLayers[entry];
if (known == UNKNOWN) {
known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO;
stiltLayers[entry] = known;
}
return known == YES;
} }
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) { private NativeBlockState decayed(int entry) {
return MODE_THREE_QUARTER; NativeBlockState resolved = decayed[entry];
if (resolved == null) {
NativeBlockState data = source(entry);
resolved = preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance")
&& "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))
? data.withProperty("persistent", "true") : data;
decayed[entry] = resolved;
}
return resolved;
} }
return MODE_ANGLE; private StateFacts facts(int entry) {
StateFacts resolved = facts[entry];
if (resolved == null) {
resolved = computeFacts(decayed(entry));
facts[entry] = resolved;
}
return resolved;
} }
/** private StateFacts factsOf(NativeBlockState state) {
* Rotates in place. Axis order (X, then Z, then Y) is load bearing. if (editedFacts == null) {
*/ editedFacts = new IdentityHashMap<>();
private void rotate(IrisBlockVector v) { }
if (!rotates) { StateFacts resolved = editedFacts.get(state);
return; if (resolved == null) {
resolved = computeFacts(state);
editedFacts.put(state, resolved);
}
return resolved;
} }
switch (xMode) { private StateFacts computeFacts(NativeBlockState state) {
case MODE_FLIP -> { NativeBlockState rotated = rotate(state);
v.setZ(-v.getZ()); if (rotated == null) {
v.setY(-v.getY()); return StateFacts.ROTATED_AWAY;
}
case MODE_QUARTER -> {
double z = v.getZ();
v.setZ(v.getY());
v.setY(-z);
}
case MODE_THREE_QUARTER -> {
double z = v.getZ();
v.setZ(-v.getY());
v.setY(z);
}
case MODE_ANGLE -> {
double y = xCos * v.getY() - xSin * v.getZ();
double z = xSin * v.getY() + xCos * v.getZ();
v.setY(y);
v.setZ(z);
}
default -> {
} }
boolean waterloggable = IrisProceduralBlocks.hasProperty(rotated, "waterlogged");
boolean authored = waterloggable && "true".equals(IrisProceduralBlocks.propertyValue(rotated, "waterlogged"));
NativeBlockState dry = waterloggable ? IrisProceduralBlocks.normalizeWaterlogging(rotated, null, false) : rotated;
String material = IrisObjectShaping.materialKey(rotated);
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
return new StateFacts(rotated, waterloggable, authored, dry, air);
} }
switch (zMode) { private NativeBlockState rotate(NativeBlockState state) {
case MODE_FLIP -> { if (state == null) {
v.setY(-v.getY()); return null;
v.setX(-v.getX());
} }
case MODE_QUARTER -> { if (rotations == null) {
double y = v.getY(); rotations = new IdentityHashMap<>();
v.setY(v.getX());
v.setX(-y);
} }
case MODE_THREE_QUARTER -> { Object memo = rotations.get(state);
double y = v.getY(); if (memo == null) {
v.setY(-v.getX()); NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz);
v.setX(y); memo = rotated == null ? ROTATED_AWAY : rotated;
} rotations.put(state, memo);
case MODE_ANGLE -> {
double x = zCos * v.getX() - zSin * v.getY();
double y = zSin * v.getX() + zCos * v.getY();
v.setX(x);
v.setY(y);
}
default -> {
} }
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
} }
switch (yMode) { private boolean shouldStilt(int entry) {
case MODE_FLIP -> { byte known = stilts[entry];
v.setX(-v.getX()); if (known == UNKNOWN) {
v.setZ(-v.getZ()); known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO;
stilts[entry] = known;
} }
case MODE_QUARTER -> { return known == YES;
double x = v.getX();
v.setX(v.getZ());
v.setZ(-x);
} }
case MODE_THREE_QUARTER -> {
double x = v.getX(); private NativeBlockState stiltDefault(int entry) {
v.setX(-v.getZ()); NativeBlockState resolved = stiltDefaults[entry];
v.setZ(x); if (resolved == null) {
NativeBlockState state = palette[entry];
String material = IrisObjectShaping.materialKey(state);
resolved = material.equals("minecraft:grass_block") || material.equals("minecraft:mycelium")
|| material.equals("minecraft:podzol") || material.equals("minecraft:dirt_path")
? IrisObject.States.dirt() : state;
stiltDefaults[entry] = resolved;
} }
case MODE_ANGLE -> { return resolved;
double x = yCos * v.getX() + ySin * v.getZ();
double z = -ySin * v.getX() + yCos * v.getZ();
v.setX(x);
v.setZ(z);
} }
default -> {
private boolean shouldStiltPlaced(NativeBlockState state) {
if (placedStilts == null) {
placedStilts = new IdentityHashMap<>();
} }
Boolean known = placedStilts.get(state);
if (known == null) {
known = IrisObjectShaping.shouldStilt(state, excludedStiltMaterials);
placedStilts.put(state, known);
} }
return known;
} }
} }
@@ -0,0 +1,259 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
final class ObjectPlacementBake {
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
private final long blocksRevision;
private final long statesRevision;
private final TreeGrounding grounding;
private final int count;
private final int[] offsetX;
private final int[] offsetY;
private final int[] offsetZ;
private final int[] layerY;
private final short[] paletteIndexes;
private final NativeBlockState[] palette;
private final int[] tileBlocks;
private final TileData[] tileValues;
private final long[] canopy;
private final Key key;
private volatile double[] exact;
private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count,
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes,
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
this.key = key;
this.blocksRevision = blocksRevision;
this.statesRevision = statesRevision;
this.grounding = grounding;
this.count = count;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.offsetZ = offsetZ;
this.layerY = layerY;
this.paletteIndexes = paletteIndexes;
this.palette = palette;
this.tileBlocks = tileBlocks;
this.tileValues = tileValues;
this.canopy = canopy;
}
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
Key key = Key.of(spin, ceilingHang, translateOffset);
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
ObjectPlacementBake cached = variants.get(key);
if (cached != null && cached.matches(object)) {
return cached;
}
ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset);
if (built.matches(object)) {
if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) {
variants.clear();
}
variants.put(key, built);
}
return built;
}
private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
object.readLock.lock();
try {
VectorMap<NativeBlockState> blocks = object.blocks;
VectorMap<TileData> states = object.states;
long blocksRevision = blocks.modificationRevision();
long statesRevision = states.modificationRevision();
TreeGrounding grounding = object.getTreeGrounding();
boolean hasStates = !states.isEmpty();
IntArrayList x = new IntArrayList();
IntArrayList y = new IntArrayList();
IntArrayList z = new IntArrayList();
IntArrayList layer = new IntArrayList();
IntArrayList indexes = new IntArrayList();
IntArrayList tileBlocks = new IntArrayList();
List<TileData> tileValues = new ArrayList<>();
List<NativeBlockState> palette = new ArrayList<>();
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
IntArrayList canopyBlocks = new IntArrayList();
boolean layerDiffers = false;
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
int index = 0;
while (cursor.next()) {
IrisBlockVector source = cursor.key();
NativeBlockState state = cursor.value();
if (state == null) {
state = IrisObject.States.air();
}
transform(source, rotated, spin, ceilingHang, translateOffset);
int roundedY = (int) Math.round(rotated.getY());
int floorY = rotated.getBlockY();
x.add((int) Math.round(rotated.getX()));
y.add(roundedY);
z.add((int) Math.round(rotated.getZ()));
layer.add(floorY);
layerDiffers |= floorY != roundedY;
Integer slot = paletteSlots.get(state);
if (slot == null) {
slot = palette.size();
palette.add(state);
paletteSlots.put(state, slot);
}
indexes.add(slot);
if (hasStates) {
TileData tile = states.get(source);
if (tile != null) {
tileBlocks.add(index);
tileValues.add(tile);
}
}
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
canopyBlocks.add(index);
}
index++;
}
if (palette.size() > Short.MAX_VALUE) {
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
+ Short.MAX_VALUE + " distinct block states");
}
short[] paletteIndexes = new short[index];
for (int block = 0; block < index; block++) {
paletteIndexes[block] = (short) indexes.getInt(block);
}
long[] canopy = null;
if (!canopyBlocks.isEmpty()) {
canopy = new long[(index + 63) >>> 6];
for (int block : canopyBlocks) {
canopy[block >>> 6] |= 1L << (block & 63);
}
}
return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index,
x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null,
paletteIndexes, palette.toArray(new NativeBlockState[0]),
tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
} finally {
object.readLock.unlock();
}
}
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
IrisBlockVector translateOffset) {
target.setX(source.getX());
target.setY(source.getY());
target.setZ(source.getZ());
spin.rotate(target);
if (ceilingHang) {
target.setY(-target.getBlockY());
}
if (translateOffset != null) {
target.add(translateOffset);
}
}
boolean matches(IrisObject object) {
return object.blocks.modificationRevision() == blocksRevision
&& object.states.modificationRevision() == statesRevision
&& object.getTreeGrounding() == grounding;
}
int count() {
return count;
}
int offsetX(int index) {
return offsetX[index];
}
int offsetY(int index) {
return offsetY[index];
}
int offsetZ(int index) {
return offsetZ[index];
}
int layerY(int index) {
return layerY == null ? offsetY[index] : layerY[index];
}
int paletteIndex(int index) {
return paletteIndexes[index];
}
NativeBlockState[] palette() {
return palette;
}
int[] tileBlocks() {
return tileBlocks;
}
TileData[] tileValues() {
return tileValues;
}
boolean canopy(int index) {
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
}
double[] exact(IrisObject object, SpinKernel spin) {
double[] resolved = exact;
if (resolved != null) {
return resolved;
}
object.readLock.lock();
try {
resolved = new double[count * 3];
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
int index = 0;
while (cursor.next() && index < count) {
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
resolved[index * 3] = rotated.getX();
resolved[index * 3 + 1] = rotated.getY();
resolved[index * 3 + 2] = rotated.getZ();
index++;
}
if (index != count) {
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
}
} finally {
object.readLock.unlock();
}
exact = resolved;
return resolved;
}
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
long translateZ) {
static Key of(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
boolean translating = translateOffset != null;
return new Key(spin.rotates, spin.xMode, spin.yMode, spin.zMode,
Double.doubleToLongBits(spin.xCos), Double.doubleToLongBits(spin.xSin),
Double.doubleToLongBits(spin.yCos), Double.doubleToLongBits(spin.ySin),
Double.doubleToLongBits(spin.zCos), Double.doubleToLongBits(spin.zSin), ceilingHang, translating,
translating ? Double.doubleToLongBits(translateOffset.getX()) : 0L,
translating ? Double.doubleToLongBits(translateOffset.getY()) : 0L,
translating ? Double.doubleToLongBits(translateOffset.getZ()) : 0L);
}
IrisBlockVector translateOffset() {
return translating ? new IrisBlockVector(Double.longBitsToDouble(translateX),
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
}
}
}
@@ -0,0 +1,152 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.geometry.IrisBlockVector;
/**
* Precomputed vector rotation for a single placement.
* <p>
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
* the value the per-call form would have produced.
*/
final class SpinKernel {
static final int MODE_NONE = 0;
static final int MODE_FLIP = 1;
static final int MODE_QUARTER = 2;
static final int MODE_THREE_QUARTER = 3;
static final int MODE_ANGLE = 4;
final boolean rotates;
final int xMode;
final int yMode;
final int zMode;
final double xCos;
final double xSin;
final double yCos;
final double ySin;
final double zCos;
final double zSin;
SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
rotates = rotation.canRotate();
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
xCos = Math.cos(xAngle);
xSin = Math.sin(xAngle);
zCos = Math.cos(zAngle);
zSin = Math.sin(zAngle);
yCos = Math.cos(yAngle);
ySin = Math.sin(yAngle);
}
private static int modeOf(IrisAxisRotationClamp clamp) {
if (!clamp.isLocked()) {
return MODE_ANGLE;
}
if (Math.abs(clamp.getMax()) % 360D == 180D) {
return MODE_FLIP;
}
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
return MODE_QUARTER;
}
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
return MODE_THREE_QUARTER;
}
return MODE_ANGLE;
}
/**
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
*/
void rotate(IrisBlockVector v) {
if (!rotates) {
return;
}
switch (xMode) {
case MODE_FLIP -> {
v.setZ(-v.getZ());
v.setY(-v.getY());
}
case MODE_QUARTER -> {
double z = v.getZ();
v.setZ(v.getY());
v.setY(-z);
}
case MODE_THREE_QUARTER -> {
double z = v.getZ();
v.setZ(-v.getY());
v.setY(z);
}
case MODE_ANGLE -> {
double y = xCos * v.getY() - xSin * v.getZ();
double z = xSin * v.getY() + xCos * v.getZ();
v.setY(y);
v.setZ(z);
}
default -> {
}
}
switch (zMode) {
case MODE_FLIP -> {
v.setY(-v.getY());
v.setX(-v.getX());
}
case MODE_QUARTER -> {
double y = v.getY();
v.setY(v.getX());
v.setX(-y);
}
case MODE_THREE_QUARTER -> {
double y = v.getY();
v.setY(-v.getX());
v.setX(y);
}
case MODE_ANGLE -> {
double x = zCos * v.getX() - zSin * v.getY();
double y = zSin * v.getX() + zCos * v.getY();
v.setX(x);
v.setY(y);
}
default -> {
}
}
switch (yMode) {
case MODE_FLIP -> {
v.setX(-v.getX());
v.setZ(-v.getZ());
}
case MODE_QUARTER -> {
double x = v.getX();
v.setX(v.getZ());
v.setZ(-x);
}
case MODE_THREE_QUARTER -> {
double x = v.getX();
v.setX(-v.getZ());
v.setZ(x);
}
case MODE_ANGLE -> {
double x = yCos * v.getX() + ySin * v.getZ();
double z = -ySin * v.getX() + yCos * v.getZ();
v.setX(x);
v.setZ(z);
}
default -> {
}
}
}
}
@@ -791,7 +791,7 @@ public class IrisObjectPlacementRunnerRegressionTest {
object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data); object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L), data);
assertEquals(1, placer.blockReads); assertEquals(enabled || authored ? 1 : 0, placer.blockReads);
assertEquals(1, placer.writes().size()); assertEquals(1, placer.writes().size());
assertSame(submerged && (enabled || authored) ? wet : dry, assertSame(submerged && (enabled || authored) ? wet : dry,
placer.writes().get(0).state()); placer.writes().get(0).state());
@@ -0,0 +1,76 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.testsupport.KeyedBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
public class ObjectPlacementBakeTest {
@Test
public void spinsResolvingToTheSameKernelShareOneBake() {
IrisObject object = object();
IrisObjectRotation rotation = new IrisObjectRotation();
ObjectPlacementBake first = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 10, 0), false, null);
ObjectPlacementBake second = ObjectPlacementBake.of(object, new SpinKernel(rotation, 3, 80, 9), false, null);
ObjectPlacementBake turned = ObjectPlacementBake.of(object, new SpinKernel(rotation, 0, 100, 0), false, null);
assertSame(first, second);
assertNotSame(first, turned);
assertEquals(3, first.count());
}
@Test
public void bakedOffsetsMatchTheKernelTransformInCursorOrder() {
IrisObject object = object();
IrisObjectRotation rotation = IrisObjectRotation.of(0D, 90D, 0D);
SpinKernel spin = new SpinKernel(rotation, 0, 0, 0);
IrisBlockVector translate = new IrisBlockVector(1.5D, -2D, 0.25D);
ObjectPlacementBake bake = ObjectPlacementBake.of(object, spin, true, translate);
double[] exact = bake.exact(object, spin);
int index = 0;
for (IrisBlockVector source : object.getBlocks().keys()) {
IrisBlockVector expected = source.clone();
spin.rotate(expected);
expected.setY(-expected.getBlockY());
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);
}
@Test
public void editedObjectsRebake() {
IrisObject object = object();
SpinKernel spin = new SpinKernel(new IrisObjectRotation(), 0, 0, 0);
ObjectPlacementBake before = ObjectPlacementBake.of(object, spin, false, null);
object.getBlocks().put(new IrisBlockVector(1, 1, 1), new KeyedBlockState("minecraft:dirt"));
ObjectPlacementBake after = ObjectPlacementBake.of(object, spin, false, null);
assertNotSame(before, after);
assertEquals(4, after.count());
assertSame(after, ObjectPlacementBake.of(object, spin, false, null));
}
private static IrisObject object() {
IrisObject object = new IrisObject(5, 5, 5);
KeyedBlockState stone = new KeyedBlockState("minecraft:stone");
object.getBlocks().put(new IrisBlockVector(-2, 0, 1), stone);
object.getBlocks().put(new IrisBlockVector(0, -1, 2), stone);
object.getBlocks().put(new IrisBlockVector(2, 2, -2), new KeyedBlockState("minecraft:oak_log[axis=y]"));
return object;
}
}