Grounded procedural trees

- Compact connected roots extend bounded gaps into supporting terrain
- Unsupported cliff placements leave no partial trees
- Root and canopy roles follow object scaling and rotation
- Canopies preserve solid trunk cells across chunk replay and saved continuations
- Branch wood replaces leaf clusters within each tree
- Regression tests cover slopes and overlapping tree placements
- Gameplay checks compare grounded spindle geometry across both packs
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-18 18:00:40 -04:00
1 parent 51c626c410
commit a6aeceff86
34 files changed
+2082 -216

No files matched your search

@@ -242,11 +242,11 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
private IrisTreeRootStyle rootStyle = IrisTreeRootStyle.BUTTRESS;
@MinNumber(0)
@Description("Override the root depth/reach in blocks. 0 means auto (scales with tree height).")
@Description("Root depth in blocks, also used for ground-contact extension up to 16 blocks. 0 scales with tree height.")
private int rootDepth = 0;
@MinNumber(0)
@Description("Override the root flare radius in blocks. 0 means auto.")
@Description("Horizontal reach of buttress and stilt roots. 0 uses a compact radius based on trunk width. Taproots ignore this field.")
private double rootFlare = 0;
@MinNumber(1)
@@ -40,6 +40,12 @@ public final class ProceduralTreeGenerator {
long baseSeed = rng.getSeed();
List<TreeTrunkBuilder.Limb> limbs = TreeTrunkBuilder.build(canvas, tree, height);
Set<TreeBlockCanvas.Vec> supportPositions = new HashSet<>();
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
if (position.y() == 0) {
supportPositions.add(position);
}
}
boolean wantEndpoints = tree.getDecorators() != null && !tree.getDecorators().isEmpty();
List<int[]> branchEndpoints = wantEndpoints ? new ArrayList<>() : null;
@@ -55,7 +61,13 @@ public final class ProceduralTreeGenerator {
}
if (tree.isRoots()) {
Set<TreeBlockCanvas.Vec> existingWood = new HashSet<>(canvas.getTrunk());
TreeRootBuilder.build(canvas, tree, height, baseSeed);
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
if (position.y() <= 0 && !existingWood.contains(position)) {
supportPositions.add(position);
}
}
}
if (tree.isPlausible()) {
@@ -82,10 +94,14 @@ public final class ProceduralTreeGenerator {
TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree);
return assemble(resolved);
int rootReach = tree.isRoots() ? tree.getRootDepth() > 0 ? tree.getRootDepth()
: Math.max(2, (int) Math.round(height * 0.18)) : 0;
return assemble(resolved, supportPositions, leafPositions, rootReach);
}
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved) {
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved,
Set<TreeBlockCanvas.Vec> supportPositions,
Set<TreeBlockCanvas.Vec> leafPositions, int rootReach) {
if (resolved.isEmpty()) {
return null;
}
@@ -105,21 +121,26 @@ public final class ProceduralTreeGenerator {
maxZ = Math.max(maxZ, v.z());
}
int w = maxX - minX + 1;
int w = 2 * Math.max(Math.abs(minX), Math.abs(maxX)) + 1;
int h = maxY - minY + 1;
int d = maxZ - minZ + 1;
int cx = w / 2;
int d = 2 * Math.max(Math.abs(minZ), Math.abs(maxZ)) + 1;
int cy = h / 2;
int cz = d / 2;
IrisObject object = new IrisObject(w, h, d);
List<TreeGrounding.Root> roots = new ArrayList<>(supportPositions.size());
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>(leafPositions.size());
for (Map.Entry<TreeBlockCanvas.Vec, NativeBlockState> entry : resolved.entrySet()) {
TreeBlockCanvas.Vec v = entry.getKey();
int nx = v.x() - minX - cx;
int ny = v.y() - cy + 1;
int nz = v.z() - minZ - cz;
object.getBlocks().put(new IrisBlockVector(nx, ny, nz), entry.getValue());
object.getBlocks().put(new IrisBlockVector(v.x(), ny, v.z()), entry.getValue());
if (supportPositions.contains(v)) {
roots.add(new TreeGrounding.Root(v.x(), ny, v.z(), entry.getValue()));
}
if (leafPositions.contains(v)) {
canopy.add(new TreeBlockCanvas.Vec(v.x(), ny, v.z()));
}
}
object.setTreeGrounding(TreeGrounding.create(roots, rootReach, canopy));
return object;
}
@@ -335,7 +335,9 @@ public final class TreeCanopyBuilder {
int x = (int) Math.round(ox + dx * t);
int y = (int) Math.round(oy + dy * t - sagBlocks * t * t);
int z = (int) Math.round(oz + dz * t);
if (!canvas.has(x, y, z)) {
TreeBlockCanvas.Cell existing = canvas.get(x, y, z);
if (existing == null || existing.role() == TreeBlockCanvas.Role.LEAF
|| existing.role() == TreeBlockCanvas.Role.SECONDARY_LEAF) {
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
}
tip = new int[]{x, y, z};
@@ -0,0 +1,122 @@
package art.arcane.iris.generation.decoration.tree;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.BlockPosition;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public record TreeGrounding(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
public TreeGrounding {
roots = List.copyOf(roots);
maxExtension = Math.max(0, Math.min(16, maxExtension));
canopy = Set.copyOf(canopy);
}
public static TreeGrounding create(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
Map<Long, Root> columns = new HashMap<>();
for (Root root : roots) {
long key = ((long) root.x() << 32) | (root.z() & 0xffffffffL);
Root previous = columns.get(key);
if (previous == null || root.y() < previous.y()) {
columns.put(key, root);
}
}
List<Root> lowest = new ArrayList<>(columns.values());
lowest.sort(Comparator.comparingInt(Root::x).thenComparingInt(Root::z));
return new TreeGrounding(lowest, maxExtension, canopy);
}
public boolean isCanopy(int x, int y, int z) {
return canopy.contains(new TreeBlockCanvas.Vec(x, y, z));
}
public KList<IrisBlockVector> supportOffsets() {
KList<IrisBlockVector> offsets = new KList<>();
for (Root root : roots) {
offsets.add(new IrisBlockVector(root.x(), root.y(), root.z()));
}
return offsets;
}
public Plan plan(IObjectPlacer placer, Placement placement) {
List<Extension> extensions = new ArrayList<>();
IrisObjectPlacement config = placement.config();
BlockPosition origin = placement.origin();
BlockPosition spin = placement.spin();
int solidDepth = Math.max(1, config.getSurfaceSupportDepth());
for (Root root : roots) {
IrisBlockVector offset = config.getRotation().rotate(
new IrisBlockVector(root.x(), root.y(), root.z()), spin.getX(), spin.getY(), spin.getZ());
offset = config.getTranslate().translate(offset, config.getRotation(), spin.getX(), spin.getY(), spin.getZ());
int x = origin.getX() + (int) Math.round(offset.getX());
int y = origin.getY() + (int) Math.round(offset.getY());
int z = origin.getZ() + (int) Math.round(offset.getZ());
int surface = placer.getHighest(x, z, placement.data(), true);
int contact = Math.min(surface, y - 1);
if (y - contact - 1 > maxExtension) {
return Plan.UNSUPPORTED;
}
for (int below = 0; below < solidDepth; below++) {
if (placer.isCarved(x, contact - below, z) || !placer.isSurfaceSolid(x, contact - below, z)) {
return Plan.UNSUPPORTED;
}
}
if (contact < y - 1) {
NativeBlockState state = config.getRotation().rotate(root.state(), spin.getX(), spin.getY(), spin.getZ());
if (state == null) {
return Plan.UNSUPPORTED;
}
Extension extension = new Extension(x, y - 1, contact, z, state);
if (intersectsStructure(placer, extension)) {
return Plan.UNSUPPORTED;
}
extensions.add(extension);
}
}
return new Plan(!roots.isEmpty(), List.copyOf(extensions));
}
private static boolean intersectsStructure(IObjectPlacer placer, Extension extension) {
Engine engine = placer.getEngine();
if (engine == null) {
return false;
}
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(
extension.x(), extension.z(), extension.x(), extension.z());
if (volumes == null) {
return false;
}
for (NativeStructureVolume volume : volumes) {
if (volume.intersects(extension.x(), extension.bottomY() + engine.getMinHeight(), extension.z(),
extension.x(), extension.topY() + engine.getMinHeight(), extension.z())) {
return true;
}
}
return false;
}
public record Root(int x, int y, int z, NativeBlockState state) {
}
public record Placement(IrisData data, IrisObjectPlacement config, BlockPosition origin, BlockPosition spin) {
}
public record Extension(int x, int topY, int bottomY, int z, NativeBlockState state) {
}
public record Plan(boolean supported, List<Extension> extensions) {
private static final Plan UNSUPPORTED = new Plan(false, List.of());
}
}
@@ -28,120 +28,152 @@ public final class TreeRootBuilder {
}
public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, long baseSeed) {
List<int[]> cells = new ArrayList<>();
List<TreeBlockCanvas.Vec> cells = new ArrayList<>();
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
if (v.y() == 0) {
cells.add(new int[]{v.x(), v.z()});
cells.add(v);
}
}
if (cells.isEmpty()) {
return;
}
cells.sort(TreeBlockCanvas.Vec::compareTo);
int depth = tree.getRootDepth() > 0 ? tree.getRootDepth() : autoDepth(height);
RNG rng = new RNG(baseSeed ^ 0x5009L);
double cx = 0;
double cz = 0;
for (int[] c : cells) {
cx += c[0];
cz += c[1];
for (TreeBlockCanvas.Vec cell : cells) {
cx += cell.x();
cz += cell.z();
}
cx /= cells.size();
cz /= cells.size();
double baseRadius = 1.0;
for (int[] c : cells) {
baseRadius = Math.max(baseRadius, Math.hypot(c[0] - cx, c[1] - cz) + 0.5);
for (TreeBlockCanvas.Vec cell : cells) {
baseRadius = Math.max(baseRadius, Math.hypot(cell.x() - cx, cell.z() - cz) + 0.5);
}
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(2.0, depth * 0.6);
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(0.75, baseRadius * 0.35);
RootShape shape = new RootShape(cells, cx, cz, baseRadius, depth, flare);
IrisTreeRootStyle style = tree.getRootStyle();
switch (style) {
case TAPROOT -> taproot(canvas, cells, cx, cz, baseRadius, depth);
case TAPROOT -> taproot(canvas, shape);
case BUTTRESS -> {
taproot(canvas, cells, cx, cz, baseRadius, depth);
buttress(canvas, cells, cx, cz, baseRadius, depth, flare, rng);
taproot(canvas, shape);
buttress(canvas, shape, rng);
}
case STILT -> {
taproot(canvas, cells, cx, cz, baseRadius, Math.min(depth, 3));
stilts(canvas, cx, cz, baseRadius, flare, height, cells.size(), rng);
stilts(canvas, shape, height, rng);
taproot(canvas, new RootShape(cells, cx, cz, baseRadius, Math.min(depth, 3), flare));
}
}
}
private static void taproot(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius, int depth) {
int icx = (int) Math.round(cx);
int icz = (int) Math.round(cz);
for (int k = 1; k <= depth; k++) {
double frac = k / (double) depth;
double keepR = baseRadius * (1.0 - 0.6 * frac);
for (int[] c : cells) {
if (Math.hypot(c[0] - cx, c[1] - cz) <= keepR + 1e-6) {
placeRoot(canvas, c[0], -k, c[1], TreeBlockCanvas.Axis.Y);
private static void taproot(TreeBlockCanvas canvas, RootShape shape) {
TreeBlockCanvas.Vec center = nearestCell(shape.cells(), shape.centerX(), shape.centerZ());
for (int k = 1; k <= shape.depth(); k++) {
double fraction = k / (double) shape.depth();
double keepRadius = shape.radius() * (1.0 - fraction);
for (TreeBlockCanvas.Vec cell : shape.cells()) {
if (Math.hypot(cell.x() - shape.centerX(), cell.z() - shape.centerZ()) <= keepRadius) {
placeRoot(canvas, cell.x(), -k, cell.z(), TreeBlockCanvas.Axis.Y);
}
}
placeRoot(canvas, icx, -k, icz, TreeBlockCanvas.Axis.Y);
placeRoot(canvas, center.x(), -k, center.z(), TreeBlockCanvas.Axis.Y);
}
}
private static void buttress(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius,
int depth, double flare, RNG rng) {
int nLegs = Math.max(4, Math.min(12, cells.size() + 2));
for (int li = 0; li < nLegs; li++) {
double ang = 2.0 * Math.PI * li / nLegs + rng.d(-0.25, 0.25);
double ca = Math.cos(ang);
double sa = Math.sin(ang);
int prevX = (int) Math.round(cx + baseRadius * ca);
int prevZ = (int) Math.round(cz + baseRadius * sa);
for (int k = 1; k <= depth; k++) {
double frac = k / (double) depth;
double r = baseRadius + (flare - baseRadius) * frac;
int x = (int) Math.round(cx + r * ca);
int z = (int) Math.round(cz + r * sa);
placeRoot(canvas, x, -k, z, TreeBlockCanvas.Axis.Y);
if (x != prevX || z != prevZ) {
placeRoot(canvas, prevX, -k, prevZ, TreeBlockCanvas.Axis.Y);
}
prevX = x;
prevZ = z;
private static void buttress(TreeBlockCanvas canvas, RootShape shape, RNG rng) {
int legs = Math.max(3, Math.min(8, shape.cells().size() + 2));
for (int leg = 0; leg < legs; leg++) {
double angle = 2.0 * Math.PI * leg / legs + rng.d(-0.25, 0.25);
double reach = shape.flare() * rng.d(0.8, 1.0);
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
int depth = legDepth(shape.depth(), leg, legs);
TreeBlockCanvas.Vec start = nearestCell(shape.cells(), x, z);
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
}
}
private static void stilts(TreeBlockCanvas canvas, RootShape shape, int height, RNG rng) {
int stiltTop = Math.min(height - 1, Math.max(2, Math.min(7, (int) Math.round(0.25 * height))));
List<TreeBlockCanvas.Vec> attachments = new ArrayList<>();
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
if (cell.y() == stiltTop) {
attachments.add(cell);
}
}
}
private static void stilts(TreeBlockCanvas canvas, double cx, double cz, double baseRadius, double flare,
int height, int cellCount, RNG rng) {
int stiltTop = Math.max(2, Math.min(7, (int) Math.round(0.25 * height)));
int nProps = Math.max(4, Math.min(10, cellCount + 2));
for (int p = 0; p < nProps; p++) {
double ang = 2.0 * Math.PI * p / nProps + rng.d(-0.25, 0.25);
double ca = Math.cos(ang);
double sa = Math.sin(ang);
int sx = (int) Math.round(cx + baseRadius * ca);
int sz = (int) Math.round(cz + baseRadius * sa);
int fx = (int) Math.round(cx + flare * ca);
int fz = (int) Math.round(cz + flare * sa);
rasterizeRoot(canvas, sx, stiltTop, sz, fx, -2, fz);
if (attachments.isEmpty()) {
attachments.addAll(shape.cells());
}
attachments.sort(TreeBlockCanvas.Vec::compareTo);
int props = Math.max(3, Math.min(8, shape.cells().size() + 2));
for (int prop = 0; prop < props; prop++) {
double angle = 2.0 * Math.PI * prop / props + rng.d(-0.25, 0.25);
double reach = shape.flare() * rng.d(0.8, 1.0);
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
int depth = legDepth(Math.min(shape.depth(), 3), prop, props);
TreeBlockCanvas.Vec start = nearestCell(attachments, x, z);
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
}
}
private static void rasterizeRoot(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez) {
int dx = ex - ox;
int dy = ey - oy;
int dz = ez - oz;
private static int legDepth(int depth, int leg, int count) {
return Math.max(1, (int) Math.round(depth * (0.55 + 0.45 * leg / Math.max(1, count - 1))));
}
private static TreeBlockCanvas.Vec nearestCell(List<TreeBlockCanvas.Vec> cells, double x, double z) {
TreeBlockCanvas.Vec nearest = cells.getFirst();
double distance = Double.POSITIVE_INFINITY;
for (TreeBlockCanvas.Vec cell : cells) {
double dx = cell.x() - x;
double dz = cell.z() - z;
double candidateDistance = dx * dx + dz * dz;
if (candidateDistance < distance) {
nearest = cell;
distance = candidateDistance;
}
}
return nearest;
}
private static void rasterizeRoot(TreeBlockCanvas canvas, TreeBlockCanvas.Vec start, TreeBlockCanvas.Vec end) {
int dx = end.x() - start.x();
int dy = end.y() - start.y();
int dz = end.z() - start.z();
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
for (int i = 0; i <= steps; i++) {
int x = start.x();
int y = start.y();
int z = start.z();
for (int i = 1; i <= steps; i++) {
double t = i / (double) steps;
int x = (int) Math.round(ox + dx * t);
int y = (int) Math.round(oy + dy * t);
int z = (int) Math.round(oz + dz * t);
placeRoot(canvas, x, y, z, axis);
int nextX = (int) Math.round(start.x() + dx * t);
int nextY = (int) Math.round(start.y() + dy * t);
int nextZ = (int) Math.round(start.z() + dz * t);
while (y != nextY) {
y += Integer.signum(nextY - y);
placeRoot(canvas, x, y, z, axis);
}
while (x != nextX) {
x += Integer.signum(nextX - x);
placeRoot(canvas, x, y, z, axis);
}
while (z != nextZ) {
z += Integer.signum(nextZ - z);
placeRoot(canvas, x, y, z, axis);
}
}
}
private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) {
if (!canvas.has(x, y, z)) {
TreeBlockCanvas.Cell cell = canvas.get(x, y, z);
if (cell == null || cell.role() != TreeBlockCanvas.Role.TRUNK && cell.role() != TreeBlockCanvas.Role.SECONDARY_TRUNK) {
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
}
}
@@ -149,4 +181,8 @@ public final class TreeRootBuilder {
private static int autoDepth(int height) {
return Math.max(2, Math.min(16, (int) Math.round(0.18 * height)));
}
private record RootShape(List<TreeBlockCanvas.Vec> cells, double centerX, double centerZ,
double radius, int depth, double flare) {
}
}
@@ -28,6 +28,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B;
import art.arcane.volmlib.util.collection.KList;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
@@ -49,6 +50,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
private final Long2IntOpenHashMap surfaceHeights;
private final Long2IntOpenHashMap caveCeilings;
private int blockWrites;
private BlockPosition canopyPosition;
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
this.delegate = delegate;
@@ -95,13 +97,36 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
}
}
for (BufferedMutation mutation : mutations) {
mutation.apply(delegate);
}
applyMutations();
discard();
return CommitResult.COMMITTED;
}
private void applyMutations() {
boolean canopyOpen = false;
boolean skipCanopy = false;
try {
for (BufferedMutation mutation : mutations) {
if (mutation.value() instanceof NativeBlockState) {
if (canopyOpen) {
delegate.endTreeCanopy();
canopyOpen = false;
}
skipCanopy = mutation.canopy()
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
canopyOpen = mutation.canopy() && !skipCanopy;
}
if (!mutation.canopy() || !skipCanopy) {
mutation.apply(delegate);
}
}
} finally {
if (canopyOpen) {
delegate.endTreeCanopy();
}
}
}
void discard() {
mutations.clear();
bufferedBlocks.clear();
@@ -139,11 +164,30 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state));
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
return false;
}
canopyPosition = position;
return true;
}
@Override
public void endTreeCanopy() {
canopyPosition = null;
}
private boolean isCanopy(int x, int y, int z) {
return canopyPosition != null && canopyPosition.getX() == x
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
}
@Override
public NativeBlockState get(int x, int y, int z) {
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
@@ -198,7 +242,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override
public void setTile(int x, int y, int z, TileData tile) {
if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile));
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
}
}
@@ -207,7 +251,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data));
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
if (data instanceof NativeBlockState state) {
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
@@ -291,24 +335,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
Object value();
boolean canopy();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
@@ -24,6 +24,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B;
import art.arcane.volmlib.util.collection.KList;
import org.jetbrains.annotations.Nullable;
@@ -43,6 +44,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
private final KList<BufferedMutation> mutations;
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
private int blockWrites;
private BlockPosition canopyPosition;
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
this.delegate = Objects.requireNonNull(delegate);
@@ -70,9 +72,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
return CommitResult.REJECTED_SUPPORT;
}
}
for (BufferedMutation mutation : mutations) {
mutation.apply(delegate);
}
applyMutations();
persistContinuation(engine);
discard();
return CommitResult.COMMITTED;
@@ -94,9 +94,17 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
}
IrisMatterSupport.ensureRegistered();
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
boolean skipCanopy = false;
for (BufferedMutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
if (mutation.value() instanceof NativeBlockState) {
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, mutation.x() & 15,
mutation.y(), mutation.z() & 15, mutation.canopy());
}
if (mutation.canopy() && skipCanopy) {
continue;
}
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
}
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
@@ -105,6 +113,31 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
}
private void applyMutations() {
boolean canopyOpen = false;
boolean skipCanopy = false;
try {
for (BufferedMutation mutation : mutations) {
if (mutation.value() instanceof NativeBlockState) {
if (canopyOpen) {
delegate.endTreeCanopy();
canopyOpen = false;
}
skipCanopy = mutation.canopy()
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
canopyOpen = mutation.canopy() && !skipCanopy;
}
if (!mutation.canopy() || !skipCanopy) {
mutation.apply(delegate);
}
}
} finally {
if (canopyOpen) {
delegate.endTreeCanopy();
}
}
}
void discard() {
mutations.clear();
bufferedBlocks.clear();
@@ -126,11 +159,30 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state));
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
return false;
}
canopyPosition = position;
return true;
}
@Override
public void endTreeCanopy() {
canopyPosition = null;
}
private boolean isCanopy(int x, int y, int z) {
return canopyPosition != null && canopyPosition.getX() == x
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
}
@Override
public NativeBlockState get(int x, int y, int z) {
NativeBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
@@ -176,7 +228,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
@Override
public void setTile(int x, int y, int z, TileData tile) {
if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile));
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
}
}
@@ -185,7 +237,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data));
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
if (data instanceof NativeBlockState state) {
bufferedBlocks.put(new PositionKey(x, y, z), state);
blockWrites++;
@@ -228,24 +280,26 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
Object value();
boolean canopy();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
@@ -24,6 +24,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
final class GoldenDebugObjectPlacer implements IObjectPlacer {
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
@@ -93,6 +94,16 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
delegate.set(x, y, z, d);
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
return delegate.beginTreeCanopy(position);
}
@Override
public void endTreeCanopy() {
delegate.endTreeCanopy();
}
@Override
public NativeBlockState get(int x, int y, int z) {
return delegate.get(x, y, z);
@@ -1,9 +1,11 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -12,6 +14,7 @@ import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.List;
import java.util.Map;
import java.util.BitSet;
import java.util.function.Supplier;
public final class ObjectContinuationPersistence {
@@ -68,6 +71,95 @@ public final class ObjectContinuationPersistence {
}
}
public static boolean beginBlock(Matter payload, int x, int y, int z, boolean canopy) {
if (canopy) {
MatterSlice<NativeBlockState> blocks = payload.getSlice(NativeBlockState.class);
NativeBlockState existing = blocks == null ? null : blocks.get(x, y, z);
if (existing != null && existing.isSolid()) {
return false;
}
payload.slice(TreeCanopy.class).set(x, y, z, TreeCanopy.INSTANCE);
} else {
MatterSlice<TreeCanopy> conditions = payload.getSlice(TreeCanopy.class);
if (conditions != null) {
conditions.set(x, y, z, null);
}
}
return true;
}
public static void restore(Mantle<Matter> mantle, int chunkX, int chunkZ, Matter continuation,
Hunk<NativeBlockState> blocks, boolean applyBlocks) {
BitSet blocked = blockedCanopy(continuation, blocks, applyBlocks);
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
if (states != null) {
states.iterateSync((x, y, z, state) -> {
if (blocked.get(cellIndex(x, y, z))) {
return;
}
Matter section = chunk.get(y >> 4);
if (section != null) {
clearMetadata(section, x, y & 15, z);
}
});
}
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
if (entry.getKey() == TreeCanopy.class) {
continue;
}
entry.getValue().iterateSync((x, y, z, value) -> {
if (!blocked.get(cellIndex(x, y, z))) {
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(x, y & 15, z, value);
}
});
}
}
} finally {
chunk.release();
}
if (applyBlocks && states != null) {
states.iterateSync((x, y, z, state) -> {
if (!blocked.get(cellIndex(x, y, z))) {
blocks.set(x, y, z, state);
}
});
}
}
private static BitSet blockedCanopy(Matter continuation, Hunk<NativeBlockState> blocks, boolean applyBlocks) {
BitSet blocked = new BitSet();
MatterSlice<TreeCanopy> conditions = continuation.getSlice(TreeCanopy.class);
if (conditions != null) {
conditions.iterateSync((x, y, z, condition) -> {
NativeBlockState existing = blocks.get(x, y, z);
if (!applyBlocks || existing != null && existing.isSolid()) {
blocked.set(cellIndex(x, y, z));
}
});
}
return blocked;
}
private static int cellIndex(int x, int y, int z) {
return (y << 8) | (z << 4) | x;
}
private static void clearMetadata(Matter section, int x, int y, int z) {
clearMetadata(section.getSlice(Identifier.class), x, y, z);
clearMetadata(section.getSlice(String.class), x, y, z);
clearMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
clearMetadata(section.getSlice(TileWrapper.class), x, y, z);
}
private static void clearMetadata(MatterSlice<?> slice, int x, int y, int z) {
if (slice != null) {
slice.set(x, y, z, null);
}
}
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
if (bundle == null) {
return false;
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.util.matter.MatterCavern;
@@ -34,6 +35,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final List<Mutation> mutations;
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private BlockPosition canopyPosition;
private boolean hydrologyGateKnown;
private int hydrologyGateChunkX;
private int hydrologyGateChunkZ;
@@ -82,7 +84,15 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
void apply(ObjectSourcePlan plan) {
boolean skipCanopy = false;
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
if (mutation.key().type() == NativeBlockState.class) {
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
skipCanopy = existing != null && existing.isSolid();
}
if (mutation.canopy() && skipCanopy) {
continue;
}
switch (mutation) {
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
@@ -158,12 +168,31 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cell.tile = CLEARED;
cell.identifier = identifier;
mutations.add(replayed != null
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
return;
}
setData(x, y, z, state);
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
if (!ObjectPassPlacer.super.beginTreeCanopy(position)) {
return false;
}
canopyPosition = position;
return true;
}
@Override
public void endTreeCanopy() {
canopyPosition = null;
}
private boolean isCanopy(int x, int y, int z) {
return canopyPosition != null && canopyPosition.getX() == x
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
}
@Override
public NativeBlockState get(int x, int y, int z) {
OverlayCell cell = overlayCell(x, y, z);
@@ -257,7 +286,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cell.put(type, data);
}
mutations.add(replayed != null
? replayed : new SetMutation(new DataKey(x, y, z, type), data));
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
}
@Override
@@ -476,10 +505,12 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
DataKey key();
boolean canopy();
void apply(IObjectPlacer placer);
}
record SetMutation(DataKey key, Object value) implements Mutation {
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
@Override
public int x() {
return key.x();
@@ -496,7 +527,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
}
record CustomBlockMutation(DataKey key, NativeBlockState state) implements Mutation {
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
@Override
public int x() {
return key.x();
@@ -32,7 +32,8 @@ final class ObjectSourcePlan {
private static final int PLACEMENT_DESTINATION_BYTES = 40;
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
private static final int CUSTOM_BIT = 1 << 8;
private static final int Y_SHIFT = 9;
private static final int CANOPY_BIT = 1 << 9;
private static final int Y_SHIFT = 10;
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
private final long[] destinations;
@@ -102,7 +103,7 @@ final class ObjectSourcePlan {
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | ((key.z() & 15) << 4) | (key.x() & 15);
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
}
values = table.values.toArray();
@@ -275,9 +276,9 @@ final class ObjectSourcePlan {
Object value = values[valueIndexes[start + index]];
decoded[index] = (cell & CUSTOM_BIT) != 0
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value)
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
}
return List.of(decoded);
}
@@ -285,10 +286,17 @@ final class ObjectSourcePlan {
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
boolean skipCanopy = false;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectDestinationTransaction.DataKey key = mutation.key();
int x = key.x() & 15;
int z = key.z() & 15;
if (key.type() == NativeBlockState.class) {
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, x, key.y(), z, mutation.canopy());
}
if (mutation.canopy() && skipCanopy) {
continue;
}
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
@@ -0,0 +1,5 @@
package art.arcane.iris.generation.mantle;
public enum TreeCanopy {
INSTANCE
}
@@ -23,11 +23,9 @@ import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.mantle.ObjectContinuationPersistence;
import art.arcane.iris.generation.stage.IrisDimensionStackActuator;
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedTerrainChunk;
@@ -36,13 +34,10 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface EngineMode extends Staged {
KList<EngineStage> getTerrainStages();
@@ -221,7 +216,7 @@ public interface EngineMode extends Staged {
recordNaturalTerrain(x, z, blocks, biomes, ctx);
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
ctx.beginContent();
List<Matter> continuations = restoreObjectContinuations(x, z);
List<Matter> continuations = restoreObjectContinuations(x, z, blocks);
if (!continuations.isEmpty()) {
ctx.setCaveTerrain(null);
}
@@ -229,16 +224,13 @@ public interface EngineMode extends Staged {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
for (Matter continuation : continuations) {
restoreObjectMetadata(x, z, continuation);
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> blocks.set(localX, y, localZ, state));
}
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
continuation, blocks, true);
}
}
}
private List<Matter> restoreObjectContinuations(int x, int z) {
private List<Matter> restoreObjectContinuations(int x, int z, Hunk<NativeBlockState> blocks) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
return List.of();
}
@@ -257,7 +249,8 @@ public interface EngineMode extends Staged {
if (continuation.getHeight() != getEngine().getHeight()) {
throw new IOException("Object continuation height does not match the world");
}
restoreObjectMetadata(x, z, continuation);
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
continuation, blocks, false);
}
return continuations;
} catch (IOException failure) {
@@ -265,42 +258,6 @@ public interface EngineMode extends Staged {
}
}
private void restoreObjectMetadata(int x, int z, Matter continuation) {
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
synchronized (chunk) {
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
if (states != null) {
states.iterateSync((localX, y, localZ, state) -> {
Matter section = chunk.get(y >> 4);
if (section != null) {
clearObjectMetadata(section, localX, y & 15, localZ);
}
});
}
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
entry.getValue().iterateSync((localX, y, localZ, value) ->
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(localX, y & 15, localZ, value));
}
}
} finally {
chunk.release();
}
}
private static void clearObjectMetadata(Matter section, int x, int y, int z) {
clearObjectMetadata(section.getSlice(Identifier.class), x, y, z);
clearObjectMetadata(section.getSlice(String.class), x, y, z);
clearObjectMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
clearObjectMetadata(section.getSlice(TileWrapper.class), x, y, z);
}
private static void clearObjectMetadata(MatterSlice<?> slice, int x, int y, int z) {
if (slice != null) {
slice.set(x, y, z, null);
}
}
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes, ChunkContext context) {
if (!(getEngine() instanceof IrisEngine irisEngine)) {
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import org.jetbrains.annotations.Nullable;
public class DecayControlPlacer implements IObjectPlacer {
@@ -47,6 +48,16 @@ public class DecayControlPlacer implements IObjectPlacer {
delegate.set(x, y, z, d);
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
return delegate.beginTreeCanopy(position);
}
@Override
public void endTreeCanopy() {
delegate.endTreeCanopy();
}
@Override
public NativeBlockState get(int x, int y, int z) {
return delegate.get(x, y, z);
@@ -22,6 +22,7 @@ import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.block.TileData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
public class HeightmapObjectPlacer implements IObjectPlacer {
@@ -47,6 +48,16 @@ public class HeightmapObjectPlacer implements IObjectPlacer {
oplacer.set(param1Int1, param1Int2, param1Int3, param1BlockData);
}
@Override
public boolean beginTreeCanopy(BlockPosition position) {
return oplacer.beginTreeCanopy(position);
}
@Override
public void endTreeCanopy() {
oplacer.endTreeCanopy();
}
public NativeBlockState get(int param1Int1, int param1Int2, int param1Int3) {
return oplacer.get(param1Int1, param1Int2, param1Int3);
}
@@ -24,6 +24,7 @@ import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition;
import org.jetbrains.annotations.Nullable;
public interface IObjectPlacer {
@@ -35,6 +36,14 @@ public interface IObjectPlacer {
NativeBlockState get(int x, int y, int z);
default boolean beginTreeCanopy(BlockPosition position) {
NativeBlockState existing = get(position.getX(), position.getY(), position.getZ());
return existing == null || !existing.isSolid();
}
default void endTreeCanopy() {
}
boolean isPreventingDecay();
boolean isCarved(int x, int y, int z);
@@ -20,6 +20,7 @@ package art.arcane.iris.structure.object;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.cave.CarveResult;
import art.arcane.iris.pack.value.IrisPosition;
@@ -144,6 +145,9 @@ public class IrisObject extends IrisRegistrant {
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
@Getter
@Setter
private transient TreeGrounding treeGrounding;
@Getter
VectorMap<NativeBlockState> blocks;
@Getter
VectorMap<TileData> states;
@@ -203,6 +207,7 @@ public class IrisObject extends IrisRegistrant {
copy.setLoader(getLoader());
copy.setLoadFile(getLoadFile());
copy.setCenter(getCenter().clone());
copy.setTreeGrounding(treeGrounding);
blocks.forEach(copy.blocks::put);
states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
@@ -349,6 +354,9 @@ public class IrisObject extends IrisRegistrant {
KList<IrisBlockVector> getSurfaceSupportOffsets() {
return surfaceSupportOffsets.aquire(() -> {
if (treeGrounding != null) {
return treeGrounding.supportOffsets();
}
readLock.lock();
try {
int lowestY = Integer.MAX_VALUE;
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.cave.CarveResult;
import art.arcane.iris.generation.cave.CarvingMode;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.structure.object.IrisSurfaceSupport;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
@@ -331,6 +332,18 @@ final class IrisObjectPlacementRunner {
return -1;
}
TreeGrounding.Plan grounding = null;
if (self.getTreeGrounding() != null && yv < 0 && config.getCarvingSupport().supportsSurface()
&& !config.isForcePlace() && !config.isFromBottom() && !paint && !ceilingHang
&& config.getMode() != ObjectPlaceMode.FLOATING && !rawStructurePiece) {
grounding = self.getTreeGrounding().plan(placer, new TreeGrounding.Placement(rdata, config,
new BlockPosition(x, y + yrand, z), new BlockPosition(spinx, spiny, spinz)));
if (!grounding.supported()) {
return -1;
}
stilting = false;
}
IrisPaintSurfaceProjection paintProjection = null;
KList<IrisBlockVector> paintSupport = null;
if (paint) {
@@ -467,6 +480,7 @@ final class IrisObjectPlacementRunner {
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();
@@ -622,42 +636,53 @@ final class IrisObjectPlacementRunner {
continue;
}
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
if (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz);
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ());
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
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));
String material = IrisObjectShaping.materialKey(data);
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
try {
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
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 (!rawStructurePiece && B.isVineBlock(data)) {
data = attachVineFaces(placer, data, xx, yy, zz);
}
// Short-circuit order matters for cost only: the mantle read is paid solely
// for vine blocks. Both operands are pure, so the value is unchanged.
boolean wouldReplace = !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 (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;
} finally {
if (canopy) {
placer.endTreeCanopy();
}
}
}
@@ -665,6 +690,19 @@ final class IrisObjectPlacementRunner {
self.readLock.unlock();
}
if (grounding != null) {
for (TreeGrounding.Extension extension : grounding.extensions()) {
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
extension.x(), rootY, extension.z(), waterlogCandidate);
placer.set(extension.x(), rootY, extension.z(), rootState);
if (listener != null) {
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
}
}
}
}
if (stilting) {
self.readLock.lock();
try {
@@ -20,6 +20,8 @@ package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -29,6 +31,9 @@ import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@@ -264,6 +269,19 @@ final class IrisObjectShaping {
self.shrinkOffset = offset;
self.blocks = b;
self.states = s;
if (self.getTreeGrounding() != null) {
List<TreeGrounding.Root> roots = new ArrayList<>();
for (TreeGrounding.Root root : self.getTreeGrounding().roots()) {
roots.add(new TreeGrounding.Root(root.x() + offset.getX(), root.y() + offset.getY(),
root.z() + offset.getZ(), root.state()));
}
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>();
for (TreeBlockCanvas.Vec leaf : self.getTreeGrounding().canopy()) {
canopy.add(new TreeBlockCanvas.Vec(leaf.x() + offset.getX(), leaf.y() + offset.getY(),
leaf.z() + offset.getZ()));
}
self.setTreeGrounding(TreeGrounding.create(roots, self.getTreeGrounding().maxExtension(), canopy));
}
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
}
@@ -23,6 +23,8 @@ import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.geometry.AxisAlignedBB;
import art.arcane.iris.generation.geometry.IrisBlockVector;
@@ -33,10 +35,12 @@ import art.arcane.volmlib.util.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
@@ -56,6 +60,8 @@ final class IrisObjectTransforms {
try {
VectorMap<NativeBlockState> d = new VectorMap<>();
VectorMap<TileData> dx = new VectorMap<>();
TreeGrounding grounding = self.getTreeGrounding();
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
@@ -64,6 +70,16 @@ final class IrisObjectTransforms {
}
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
d.put(destination, rotated);
if (canopy != null) {
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(destination.getBlockX(),
destination.getBlockY(), destination.getBlockZ());
IrisBlockVector source = entry.getKey();
if (grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
canopy.add(cell);
} else {
canopy.remove(cell);
}
}
TileData tile = self.states.get(entry.getKey());
if (tile == null) {
dx.remove(destination);
@@ -74,6 +90,17 @@ final class IrisObjectTransforms {
self.blocks = d;
self.states = dx;
if (grounding != null) {
List<TreeGrounding.Root> roots = new ArrayList<>();
for (TreeGrounding.Root root : grounding.roots()) {
IrisBlockVector position = r.rotate(new IrisBlockVector(root.x(), root.y(), root.z()), spinx, spiny, spinz);
NativeBlockState state = d.get(position);
if (state != null) {
roots.add(new TreeGrounding.Root(position.getBlockX(), position.getBlockY(), position.getBlockZ(), state));
}
}
self.setTreeGrounding(TreeGrounding.create(roots, grounding.maxExtension(), canopy));
}
IrisObjectShaping.shrinkwrap(self);
self.surfaceSupportOffsets.reset();
self.floatingFootprint.reset();
@@ -99,6 +126,8 @@ final class IrisObjectTransforms {
boolean savedOrigin = origin == ScaleOrigin.SAVED;
List<ScaledVoxel> voxels;
IrisObject output;
TreeGrounding grounding;
Vector3i sourceCenter;
self.readLock.lock();
try {
output = savedOrigin ? createOriginScaledObject(self, scale)
@@ -107,7 +136,8 @@ final class IrisObjectTransforms {
output.setLoader(self.getLoader());
output.setLoadFile(self.getLoadFile());
voxels = new ArrayList<>(self.blocks.size());
Vector3i sourceCenter = self.getCenter();
sourceCenter = self.getCenter().clone();
grounding = self.getTreeGrounding();
IrisBlockVector targetCenter = new IrisBlockVector(output.getCenter().getX(),
output.getCenter().getY(), output.getCenter().getZ());
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
@@ -124,7 +154,8 @@ final class IrisObjectTransforms {
}
TileData tile = self.states.get(sourcePosition);
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
tile == null ? null : tile.clone()));
tile == null ? null : tile.clone(), grounding != null && grounding.isCanopy(
sourcePosition.getBlockX(), sourcePosition.getBlockY(), sourcePosition.getBlockZ())));
}
} finally {
self.readLock.unlock();
@@ -134,8 +165,9 @@ final class IrisObjectTransforms {
.thenComparingInt(voxel -> voxel.source().getBlockY())
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
}
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
for (ScaledVoxel voxel : voxels) {
writeScaledVoxel(output, voxel);
writeScaledVoxel(output, voxel, canopy);
}
VectorMap<NativeBlockState> scaledBlocks = output.blocks;
@@ -147,10 +179,43 @@ final class IrisObjectTransforms {
}
}
removeInapplicableTiles(output, scaledBlocks);
if (grounding != null) {
canopy.removeIf(cell -> !output.blocks.containsKey(new IrisBlockVector(cell.x(), cell.y(), cell.z())));
scaleRoots(grounding, sourceCenter, output, scale, savedOrigin, canopy);
}
return output;
}
private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel) {
private static void scaleRoots(TreeGrounding grounding, Vector3i sourceCenter, IrisObject output,
double scale, boolean savedOrigin, Set<TreeBlockCanvas.Vec> canopy) {
List<TreeGrounding.Root> roots = new ArrayList<>();
Vector3i targetCenter = output.getCenter();
for (TreeGrounding.Root root : grounding.roots()) {
IrisBlockVector position = new IrisBlockVector(root.x(), root.y(), root.z());
if (!savedOrigin) {
position.add(new IrisBlockVector(sourceCenter.getX(), sourceCenter.getY(), sourceCenter.getZ()));
}
IrisBlockVector minimum = scaledMinimum(position, scale);
IrisBlockVector maximum = scaledMaximum(position, scale);
if (!savedOrigin) {
IrisBlockVector offset = new IrisBlockVector(targetCenter.getX(), targetCenter.getY(), targetCenter.getZ());
minimum.subtract(offset);
maximum.subtract(offset);
}
int y = minimum.getBlockY();
for (int x = minimum.getBlockX(); x <= maximum.getBlockX(); x++) {
for (int z = minimum.getBlockZ(); z <= maximum.getBlockZ(); z++) {
NativeBlockState state = output.blocks.get(new IrisBlockVector(x, y, z));
if (state != null) {
roots.add(new TreeGrounding.Root(x, y, z, state));
}
}
}
}
output.setTreeGrounding(TreeGrounding.create(roots, (int) Math.ceil(grounding.maxExtension() * scale), canopy));
}
private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel, Set<TreeBlockCanvas.Vec> canopy) {
IrisBlockVector minimum = voxel.minimum();
IrisBlockVector maximum = voxel.maximum();
IrisBlockVector position = minimum.clone();
@@ -161,6 +226,14 @@ final class IrisObjectTransforms {
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
position.setY(y);
output.blocks.put(position, voxel.block());
if (canopy != null) {
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(x, y, z);
if (voxel.canopy()) {
canopy.add(cell);
} else {
canopy.remove(cell);
}
}
if (voxel.tile() == null) {
output.states.remove(position);
} else {
@@ -468,7 +541,7 @@ final class IrisObjectTransforms {
}
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
NativeBlockState block, TileData tile) {
NativeBlockState block, TileData tile, boolean canopy) {
}
private enum ScaleOrigin {
@@ -42,6 +42,7 @@ public final class IrisMatterSupport {
IrisMatter.registerSliceType(new EntityMatter());
IrisMatter.registerSliceType(new CustomBlockOwnershipMatter());
IrisMatter.registerSliceType(new ObjectContinuationMatter());
IrisMatter.registerSliceType(new TreeCanopyMatter());
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
IrisMatter.registerSliceType(new PlatformBlockMatter());
@@ -0,0 +1,39 @@
package art.arcane.iris.world.storage.matter;
import art.arcane.iris.generation.mantle.TreeCanopy;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@Sliced
public final class TreeCanopyMatter extends RawMatter<TreeCanopy> {
public TreeCanopyMatter() {
this(1, 1, 1);
}
public TreeCanopyMatter(int width, int height, int depth) {
super(width, height, depth, TreeCanopy.class);
}
@Override
public Palette<TreeCanopy> getGlobalPalette() {
return null;
}
@Override
public void writeNode(TreeCanopy canopy, DataOutputStream output) throws IOException {
output.writeByte(1);
}
@Override
public TreeCanopy readNode(DataInputStream input) throws IOException {
if (input.readUnsignedByte() != 1) {
throw new IOException("Invalid tree canopy placement condition");
}
return TreeCanopy.INSTANCE;
}
}