From 943c8d67e56cf314d22959a97163cb55ac2fffa9 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 21 Oct 2026 23:50:55 -0400 Subject: [PATCH] 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 --- .../iris/structure/object/IrisObject.java | 2 + .../object/IrisObjectPlacementRunner.java | 1024 ++++++++--------- .../structure/object/ObjectPlacementBake.java | 259 +++++ .../iris/structure/object/SpinKernel.java | 152 +++ ...isObjectPlacementRunnerRegressionTest.java | 2 +- .../object/ObjectPlacementBakeTest.java | 76 ++ 6 files changed, 979 insertions(+), 536 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java create mode 100644 core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java create mode 100644 core/src/test/java/art/arcane/iris/structure/object/ObjectPlacementBakeTest.java diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java index 239bb6e3a..9b0d1fd52 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java @@ -61,6 +61,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; @@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant { protected transient AtomicCache aabb = new AtomicCache<>(); transient final AtomicCache> surfaceSupportOffsets = new AtomicCache<>(); transient final AtomicCache floatingFootprint = new AtomicCache<>(); + transient final ConcurrentHashMap placementBakes = new ConcurrentHashMap<>(); @Getter @Setter private transient TreeGrounding treeGrounding; diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java index 6a7a4b651..f23c63143 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java @@ -52,6 +52,7 @@ import art.arcane.volmlib.util.matter.MatterMarker; import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -474,59 +475,58 @@ final class IrisObjectPlacementRunner { int topLayer = Integer.MIN_VALUE; int vacuumLowest = Integer.MAX_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 markers = null; + for (int index = 0; index < count; index++) { + int entry = bake.paletteIndex(index); + TileData tile = null; + if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) { + tile = tileValues[tileCursor++]; + } - try { - VectorMap blocks = self.blocks; - VectorMap 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 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 (stilting && palette.stiltLayer(entry)) { + int layer = bake.layerY(index); + if (layer < lowest) { + lowest = layer; + } + if (layer > topLayer) { + topLayer = layer; + } + } + NativeBlockState data = palette.decayed(entry); + boolean edited = false; + if (hasEdits) { + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (NativeBlockState k : j.getFind(rdata)) { 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))); + NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata); - if (!marker.isEmptyAbove() || (a && fff)) { - markers.put(i, j.getMarker()); - max--; + boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)); + if (sameMaterial && !(newData.isCustom() || data.isCustom())) + data = BlockDataMergeSupport.merge(data, newData); + else + data = newData; + edited = true; + + Optional t = j.getReplace().getTile(rng, x, y, z, rdata); + if (t.isPresent()) { + tile = t.get(); + } else if (!sameMaterial) { + tile = null; } } } @@ -534,161 +534,89 @@ final class IrisObjectPlacementRunner { } } - VectorMap.Cursor cursor = blocks.cursor(); - while (cursor.next()) { - IrisBlockVector g = cursor.key(); - NativeBlockState d; - TileData tile = null; + StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry); + NativeBlockState rotated = facts.rotated(); + if (rotated == null) { + continue; + } + xx = x + bake.offsetX(index); + int yy = y + bake.offsetY(index); + zz = z + bake.offsetZ(index); - try { - 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 (warped) { + double exactX = exact[index * 3]; + 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 (d == null) { - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)"); - d = IrisObject.States.air(); - } - - if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) { - 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"))) { - data = data.withProperty("persistent", "true"); - } - - if (hasEdits) { - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (NativeBlockState k : j.getFind(rdata)) { - 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); - - boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)); - if (sameMaterial && !(newData.isCustom() || data.isCustom())) - data = BlockDataMergeSupport.merge(data, newData); - else - data = newData; - - Optional t = j.getReplace().getTile(rng, x, y, z, rdata); - if (t.isPresent()) { - tile = t.get(); - } else if (!sameMaterial) { - tile = null; - } - } - } - } - } - } - - data = config.getRotation().rotate(data, spinx, spiny, spinz); - if (data == null) { + if (paint) { + int surface = paintProjection.surfaceY(xx, zz); + if (surface == IrisPaintSurfaceProjection.MISSING) { continue; } - xx = x + (int) Math.round(i.getX()); - - int yy = y + (int) Math.round(i.getY()); - zz = z + (int) Math.round(i.getZ()); + if (!B.isVineBlock(rotated)) { + yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand; + } + } + + if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { + continue; + } + + boolean canopy = bake.canopy(index); + if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) { + continue; + } + + try { + data = rotated; + if (facts.waterloggable()) { + data = waterlogCandidate || facts.authoredWaterlogged() + ? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate) + : facts.dry(); + } - if (warped) { - xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); - zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); + if (!rawStructurePiece && B.isVineBlock(data)) { + data = attachVineFaces(placer, data, xx, yy, zz); } - if (paint) { - int surface = paintProjection.surfaceY(xx, zz); - if (surface == IrisPaintSurfaceProjection.MISSING) { - continue; + // 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 = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz)); + boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace); + + if (data.isCustom() || place) { + placer.set(xx, yy, zz, data); + if (heightmap != null) { + Position2 pos = new Position2(xx, zz); + Integer currentHeight = heightmap.get(pos); + if (currentHeight == null || currentHeight < yy) { + heightmap.put(pos, yy); + } + } + if (tile != null) { + placer.setTile(xx, yy, zz, tile); } - if (!B.isVineBlock(data)) { - yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand; + if (markers != null && markers[index] != null) { + placer.setData(xx, yy, zz, new MatterMarker(markers[index])); } - } - - if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { - continue; - } - - boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ()); - if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) { - continue; - } - - try { - data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate); - - if (!rawStructurePiece && B.isVineBlock(data)) { - data = attachVineFaces(placer, data, xx, yy, zz); + if (listener != null) { + listener.accept(new BlockPosition(xx, yy, zz), data); } - - // 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 = !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); - - if (data.isCustom() || place) { - placer.set(xx, yy, zz, data); - if (heightmap != null) { - Position2 pos = new Position2(xx, zz); - Integer currentHeight = heightmap.get(pos); - if (currentHeight == null || currentHeight < yy) { - heightmap.put(pos, yy); - } - } - if (tile != null) { - placer.setTile(xx, yy, zz, tile); - } - if (markers != null && markers.containsKey(g)) { - placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); - } - if (listener != null) { - listener.accept(new BlockPosition(xx, yy, zz), data); - } - if (vacuuming && yy < vacuumLowest) { - vacuumLowest = yy; - } - if (vacuuming && yy > vacuumHighest) { - vacuumHighest = yy; - } + if (vacuuming && yy < vacuumLowest) { + vacuumLowest = yy; } - } finally { - if (canopy) { - placer.endTreeCanopy(); + if (vacuuming && yy > vacuumHighest) { + vacuumHighest = yy; } + } + } finally { + if (canopy) { + placer.endTreeCanopy(); } } - } finally { - self.readLock.unlock(); } if (grounding != null) { @@ -705,238 +633,190 @@ final class IrisObjectPlacementRunner { } if (stilting) { - self.readLock.lock(); - try { - VectorMap blocks = self.blocks; - IrisStiltSettings settings = stiltSettings; + IrisStiltSettings settings = stiltSettings; - double erodeCentroidX = 0; - double erodeCentroidZ = 0; - double erodeMaxDist = 1; - if (eroding) { - int centroidCount = 0; - VectorMap.Cursor centroidCursor = blocks.cursor(); - while (centroidCursor.next()) { - IrisBlockVector rot = centroidCursor.key().clone(); - spin.rotate(rot); - 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++; - } - } + double erodeCentroidX = 0; + double erodeCentroidZ = 0; + double erodeMaxDist = 1; + if (eroding) { + int centroidCount = 0; + for (int index = 0; index < count; index++) { + if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) { + erodeCentroidX += exact[index * 3]; + erodeCentroidZ += exact[index * 3 + 2]; + centroidCount++; } - if (centroidCount > 0) { - erodeCentroidX /= centroidCount; - erodeCentroidZ /= centroidCount; - } - VectorMap.Cursor spreadCursor = blocks.cursor(); - while (spreadCursor.next()) { - IrisBlockVector rot = spreadCursor.key().clone(); - spin.rotate(rot); - 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); - if (dist > erodeMaxDist) { - erodeMaxDist = dist; - } - } + } + if (centroidCount > 0) { + erodeCentroidX /= centroidCount; + erodeCentroidZ /= centroidCount; + } + for (int index = 0; index < count; index++) { + if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) { + double dx = exact[index * 3] - erodeCentroidX; + double dz = exact[index * 3 + 2] - erodeCentroidZ; + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > erodeMaxDist) { + erodeMaxDist = dist; } } } + } - VectorMap.Cursor stiltCursor = blocks.cursor(); - while (stiltCursor.next()) { - IrisBlockVector g = stiltCursor.key(); - NativeBlockState sourceData; - 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(); - } + int targetLayer = ceilingHang ? topLayer : lowest; + for (int index = 0; index < count; index++) { + int entry = bake.paletteIndex(index); + if (!palette.shouldStilt(entry)) { + continue; + } - 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(); - } + NativeBlockState d; + if (settings != null && settings.getPalette() != null) { + d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); + } else { + d = palette.stiltDefault(entry); + } - if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) { - continue; - } + if (bake.layerY(index) != targetLayer) + continue; + d = palette.rotate(d); - NativeBlockState d = sourceData; - if (settings != null && settings.getPalette() != null) { - d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); - } else { - String mat = IrisObjectShaping.materialKey(d); - 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(); - spin.rotate(i); - if (ceilingHang) { - i.setY(-i.getBlockY()); - } - if (translating) { - i.add(translateOffset); - } - int targetLayer = ceilingHang ? topLayer : lowest; - if (i.getBlockY() != targetLayer) - continue; - d = config.getRotation().rotate(d, spinx, spiny, spinz); - - if (hasEdits) { - for (IrisObjectReplace j : config.getEdit()) { - if (rng.chance(j.getChance())) { - for (NativeBlockState k : j.getFind(rdata)) { - if (d == null) { - continue; - } - 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); - - if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { - d = BlockDataMergeSupport.merge(d, newData); - } else { - d = newData; - } - } - } - } - } - } - - if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials)) - continue; - - xx = x + (int) Math.round(i.getX()); - zz = z + (int) Math.round(i.getZ()); - - if (warped) { - xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); - zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); - } - - if (organic) { - int startY = targetLayer + y; - int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; - int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; - double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; - long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); - int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; - - if (ceilingHang) { - int scan = 0; - int solidY = startY + 1; - while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { - solidY++; - scan++; - } - int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; - int total = topBound - startY; - for (int j = startY; j <= topBound; j++) { - if (scratch > 0 && total > 0) { - double ratio = (double) (j - startY) / total; - if (ratio > (1.0 - scratch)) { - long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); - double skipChance = (ratio - (1.0 - scratch)) / scratch; - if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { - continue; - } - } - } - placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); - } - } else { - int scan = 0; - int solidY = startY - 1; - while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { - solidY--; - scan++; - } - int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; - int total = startY - bottomBound; - for (int j = startY; j >= bottomBound; j--) { - if (scratch > 0 && total > 0) { - double ratio = (double) (startY - j) / total; - if (ratio > (1.0 - scratch)) { - long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); - double skipChance = (ratio - (1.0 - scratch)) / scratch; - if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { - continue; - } - } - } - placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); - } - } - continue; - } - - int highest = placer.getHighest(xx, zz, self.getLoader(), true); - - - - int lowerBound = highest - 1; - if (settings != null) { - lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); - if (settings.getYMax() != 0) - lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); - } - - if (eroding) { - double dx = i.getX() - erodeCentroidX; - double dz = i.getZ() - erodeCentroidZ; - double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; - normalizedDist = Math.min(normalizedDist, 1.0); - int totalDepth = (lowest + y) - lowerBound; - int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); - lowerBound = (lowest + y) - erodeDepth; - } - - for (int j = lowest + y; j > lowerBound; j--) { - NativeBlockState fluidState = placer.get(xx, j, zz); - if (B.isFluid(fluidState)) { - break; - } - if (eroding) { - int depth = (lowest + y) - j; - int totalDepth = (lowest + y) - lowerBound; - double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; - if (depthRatio > 0.4) { - long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); - double skipChance = (depthRatio - 0.4) / 0.6; - if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { + if (hasEdits) { + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; + for (IrisObjectReplace j : config.getEdit()) { + if (rng.chance(j.getChance())) { + for (NativeBlockState k : j.getFind(rdata)) { + if (d == null) { continue; } + if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) { + NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata); + + if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { + d = BlockDataMergeSupport.merge(d, newData); + } else { + d = newData; + } + } } } + } + } - if (B.isVineBlock(d)) { - d = attachVineFaces(placer, d, xx, j, zz); + if (d == null || !palette.shouldStiltPlaced(d)) + continue; + + xx = x + bake.offsetX(index); + zz = z + bake.offsetZ(index); + + if (warped) { + double exactX = exact[index * 3]; + 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) { + int startY = targetLayer + y; + int maxScan = settings != null ? Math.max(1, settings.getOrganicMaxScan()) : 48; + int jitterMax = settings != null ? Math.max(0, settings.getOrganicJitter()) : 3; + double scratch = settings != null ? Math.max(0, Math.min(1, settings.getOrganicScratch())) : 0.55; + long colHash = ((long) xx * 341873128712L) ^ ((long) zz * 132897987541L); + int jitter = jitterMax > 0 ? (int) (Math.abs(colHash) % (jitterMax + 1)) : 0; + + if (ceilingHang) { + int scan = 0; + int solidY = startY + 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY++; + scan++; + } + int topBound = (scan < maxScan ? solidY - 1 : startY + Math.min(maxScan, 8)) - jitter; + int total = topBound - startY; + for (int j = startY; j <= topBound; j++) { + if (scratch > 0 && total > 0) { + double ratio = (double) (j - startY) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); + } + } else { + int scan = 0; + int solidY = startY - 1; + while (scan < maxScan && !placer.isSolid(xx, solidY, zz)) { + solidY--; + scan++; + } + int bottomBound = (scan < maxScan ? solidY + 1 : startY - Math.min(maxScan, 8)) + jitter; + int total = startY - bottomBound; + for (int j = startY; j >= bottomBound; j--) { + if (scratch > 0 && total > 0) { + double ratio = (double) (startY - j) / total; + if (ratio > (1.0 - scratch)) { + long sh = ((long) xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L); + double skipChance = (ratio - (1.0 - scratch)) / scratch; + if ((Math.abs(sh) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); + } + } + continue; + } + + int highest = placer.getHighest(xx, zz, self.getLoader(), true); + + int lowerBound = highest - 1; + if (settings != null) { + lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); + if (settings.getYMax() != 0) + lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); + } + + if (eroding) { + double dx = exact[index * 3] - erodeCentroidX; + double dz = exact[index * 3 + 2] - erodeCentroidZ; + double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; + normalizedDist = Math.min(normalizedDist, 1.0); + int totalDepth = (lowest + y) - lowerBound; + int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); + lowerBound = (lowest + y) - erodeDepth; + } + + for (int j = lowest + y; j > lowerBound; j--) { + NativeBlockState fluidState = placer.get(xx, j, zz); + if (B.isFluid(fluidState)) { + break; + } + if (eroding) { + int depth = (lowest + y) - j; + int totalDepth = (lowest + y) - lowerBound; + double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; + if (depthRatio > 0.4) { + long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); + double skipChance = (depthRatio - 0.4) / 0.6; + if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } } - placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); } + if (B.isVineBlock(d)) { + d = attachVineFaces(placer, d, xx, j, zz); + } + 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")) { 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); } + private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata, + ObjectPlacementBake bake) { + self.readLock.lock(); + try { + VectorMap blocks = self.blocks; + KMap markers = new KMap<>(); + KList 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.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 transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset, CNG warp, double half, int x, int y, int z) { KList offsets = new KList<>(); @@ -1337,152 +1284,159 @@ final class IrisObjectPlacementRunner { return result; } - /** - * Precomputed vector rotation for a single placement. - *

- * 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 record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged, + NativeBlockState dry, boolean air) { + private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false); + } - 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 static final class PlacementStates { + private static final Object ROTATED_AWAY = new Object(); + private static final byte UNKNOWN = 0; + private static final byte NO = 1; + private static final byte YES = 2; - 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; + 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 excludedStiltMaterials; + private final NativeBlockState[] decayed; + private final StateFacts[] facts; + private final byte[] stiltLayers; + private final byte[] stilts; + private final NativeBlockState[] stiltDefaults; + private IdentityHashMap rotations; + private IdentityHashMap editedFacts; + private IdentityHashMap placedStilts; - 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 PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz, + boolean preventingDecay, boolean debugSmartBore, Set 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]; } - 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; + private NativeBlockState source(int entry) { + NativeBlockState state = palette[entry]; + return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state; } - /** - * Rotates in place. Axis order (X, then Z, then Y) is load bearing. - */ - private void rotate(IrisBlockVector v) { - if (!rotates) { - return; + private boolean stiltLayer(int entry) { + byte known = stiltLayers[entry]; + if (known == UNKNOWN) { + known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO; + stiltLayers[entry] = known; } + return known == YES; + } - 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 -> { - } + private NativeBlockState decayed(int entry) { + 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; + } - 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 -> { - } + private StateFacts facts(int entry) { + StateFacts resolved = facts[entry]; + if (resolved == null) { + resolved = computeFacts(decayed(entry)); + facts[entry] = resolved; } + return resolved; + } - 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 -> { - } + private StateFacts factsOf(NativeBlockState state) { + if (editedFacts == null) { + editedFacts = new IdentityHashMap<>(); } + StateFacts resolved = editedFacts.get(state); + if (resolved == null) { + resolved = computeFacts(state); + editedFacts.put(state, resolved); + } + return resolved; + } + + private StateFacts computeFacts(NativeBlockState state) { + NativeBlockState rotated = rotate(state); + if (rotated == null) { + return StateFacts.ROTATED_AWAY; + } + 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); + } + + private NativeBlockState rotate(NativeBlockState state) { + if (state == null) { + return null; + } + if (rotations == null) { + rotations = new IdentityHashMap<>(); + } + Object memo = rotations.get(state); + if (memo == null) { + NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz); + memo = rotated == null ? ROTATED_AWAY : rotated; + rotations.put(state, memo); + } + return memo == ROTATED_AWAY ? null : (NativeBlockState) memo; + } + + private boolean shouldStilt(int entry) { + byte known = stilts[entry]; + if (known == UNKNOWN) { + known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO; + stilts[entry] = known; + } + return known == YES; + } + + private NativeBlockState stiltDefault(int entry) { + NativeBlockState resolved = stiltDefaults[entry]; + 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; + } + return resolved; + } + + 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; } } diff --git a/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java b/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java new file mode 100644 index 000000000..58470a119 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java @@ -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 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 blocks = object.blocks; + VectorMap 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 tileValues = new ArrayList<>(); + List palette = new ArrayList<>(); + IdentityHashMap paletteSlots = new IdentityHashMap<>(); + IntArrayList canopyBlocks = new IntArrayList(); + boolean layerDiffers = false; + IrisBlockVector rotated = new IrisBlockVector(0, 0, 0); + VectorMap.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.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; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java b/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java new file mode 100644 index 000000000..abaa1598c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java @@ -0,0 +1,152 @@ +package art.arcane.iris.structure.object; + +import art.arcane.iris.generation.geometry.IrisBlockVector; + +/** + * Precomputed vector rotation for a single placement. + *

+ * 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 -> { + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/structure/object/IrisObjectPlacementRunnerRegressionTest.java b/core/src/test/java/art/arcane/iris/structure/object/IrisObjectPlacementRunnerRegressionTest.java index 4aed16c6f..f867da5af 100644 --- a/core/src/test/java/art/arcane/iris/structure/object/IrisObjectPlacementRunnerRegressionTest.java +++ b/core/src/test/java/art/arcane/iris/structure/object/IrisObjectPlacementRunnerRegressionTest.java @@ -791,7 +791,7 @@ public class IrisObjectPlacementRunnerRegressionTest { 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()); assertSame(submerged && (enabled || authored) ? wet : dry, placer.writes().get(0).state()); diff --git a/core/src/test/java/art/arcane/iris/structure/object/ObjectPlacementBakeTest.java b/core/src/test/java/art/arcane/iris/structure/object/ObjectPlacementBakeTest.java new file mode 100644 index 000000000..1a51d26be --- /dev/null +++ b/core/src/test/java/art/arcane/iris/structure/object/ObjectPlacementBakeTest.java @@ -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; + } +}