Columnar object mutation log

- Log transaction writes in parallel arrays instead of mutation records
- Replay source plans straight from their packed cells without decoding records
- Build source plans from a log slice
- Journal commit originals in a packed-key set instead of a linked map
- Buffer cave object writes in parallel arrays
- Drop the now-unused import in the runner
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 01:04:22 -04:00
1 parent fd47240db9
commit ad73c33480
6 files changed
+410 -191

No files matched your search

@@ -30,11 +30,12 @@ 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;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
final class CaveObjectPlacementTransaction implements IObjectPlacer {
private static final int CACHE_MISS = Integer.MIN_VALUE;
private static final int POSITION_BITS = 21;
@@ -46,7 +47,16 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
private final int anchorY;
private final int minDepthBelowSurface;
private final int worldHeight;
private final KList<BufferedMutation> mutations;
private static final byte BLOCK = 0;
private static final byte TILE = 1;
private static final byte DATA = 2;
private int[] mutationX = new int[64];
private int[] mutationY = new int[64];
private int[] mutationZ = new int[64];
private Object[] mutationValues = new Object[64];
private byte[] mutationKinds = new byte[64];
private boolean[] mutationCanopies = new boolean[64];
private int mutationCount;
private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks;
private final Long2IntOpenHashMap surfaceHeights;
private final Long2IntOpenHashMap caveCeilings;
@@ -62,7 +72,6 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
this.anchorY = anchorY;
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
this.worldHeight = engine == null ? 0 : engine.getHeight();
this.mutations = new KList<>();
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
this.surfaceHeights = new Long2IntOpenHashMap();
this.caveCeilings = new Long2IntOpenHashMap();
@@ -81,35 +90,37 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
int previousX = 0;
int previousY = 0;
int previousZ = 0;
for (BufferedMutation mutation : mutations) {
if (previousChecked && mutation.x() == previousX && mutation.y() == previousY && mutation.z() == previousZ) {
for (int index = 0; index < mutationCount; index++) {
int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
if (previousChecked && x == previousX && y == previousY && z == previousZ) {
continue;
}
if (complex != null
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
&& !complex.allowsNewDiscreteContentAt(x, z)) {
discard();
return CommitResult.REJECTED_TRANSITION;
}
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) {
if (!isWithinBounds(x, y, z)) {
discard();
return CommitResult.REJECTED_BOUNDS;
}
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z());
SubterrainCell feature = engine.getSubterrainCell(x, y, z);
if (SubterrainRasterizer.protectsPlacement(feature)
|| feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) {
|| feature != null && feature.solid() && delegate.isSolid(x, y, z)) {
discard();
return CommitResult.REJECTED_SUBTERRAIN;
}
HydrologyCaveCell hydrology = delegate.getData(
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) {
discard();
return CommitResult.REJECTED_HYDROLOGY;
}
previousChecked = true;
previousX = mutation.x();
previousY = mutation.y();
previousZ = mutation.z();
previousX = x;
previousY = y;
previousZ = z;
}
applyMutations();
@@ -121,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
boolean canopyOpen = false;
boolean skipCanopy = false;
try {
for (BufferedMutation mutation : mutations) {
if (mutation.value() instanceof NativeBlockState) {
for (int index = 0; index < mutationCount; index++) {
int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
Object value = mutationValues[index];
boolean canopy = mutationCanopies[index];
if (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;
skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z));
canopyOpen = canopy && !skipCanopy;
}
if (!mutation.canopy() || !skipCanopy) {
mutation.apply(delegate);
if (!canopy || !skipCanopy) {
switch (mutationKinds[index]) {
case BLOCK -> delegate.set(x, y, z, (NativeBlockState) value);
case TILE -> delegate.setTile(x, y, z, (TileData) value);
default -> delegate.setData(x, y, z, value);
}
}
}
} finally {
@@ -143,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
}
void discard() {
mutations.clear();
Arrays.fill(mutationValues, 0, mutationCount, null);
mutationCount = 0;
bufferedBlocks.clear();
blockWrites = 0;
}
private void buffer(byte kind, int x, int y, int z, Object value) {
if (mutationCount == mutationValues.length) {
int capacity = mutationCount * 2;
mutationX = Arrays.copyOf(mutationX, capacity);
mutationY = Arrays.copyOf(mutationY, capacity);
mutationZ = Arrays.copyOf(mutationZ, capacity);
mutationValues = Arrays.copyOf(mutationValues, capacity);
mutationKinds = Arrays.copyOf(mutationKinds, capacity);
mutationCanopies = Arrays.copyOf(mutationCanopies, capacity);
}
mutationX[mutationCount] = x;
mutationY[mutationCount] = y;
mutationZ[mutationCount] = z;
mutationValues[mutationCount] = value;
mutationKinds[mutationCount] = kind;
mutationCanopies[mutationCount] = isCanopy(x, y, z);
mutationCount++;
}
int getCaveCeiling(int x, int z) {
long key = Cache.key(x, z);
int cached = caveCeilings.get(key);
@@ -179,7 +218,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (state == null) {
return;
}
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
buffer(BLOCK, x, y, z, state);
bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++;
}
@@ -257,7 +296,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, isCanopy(x, y, z)));
buffer(TILE, x, y, z, tile);
}
}
@@ -266,7 +305,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (data == null) {
return;
}
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
buffer(DATA, x, y, z, data);
if (data instanceof NativeBlockState state) {
bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++;
@@ -275,12 +314,11 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
for (int i = mutations.size() - 1; i >= 0; i--) {
BufferedMutation mutation = mutations.get(i);
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
for (int index = mutationCount - 1; index >= 0; index--) {
if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) {
continue;
}
Object value = mutation.value();
Object value = mutationValues[index];
if (type.isInstance(value)) {
return type.cast(value);
}
@@ -356,40 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
REJECTED_HYDROLOGY,
REJECTED_SUBTERRAIN
}
private interface BufferedMutation {
int x();
int y();
int z();
Object value();
boolean canopy();
void apply(IObjectPlacer placer);
}
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, 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, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
}
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
final class MutationLog {
private static final byte CUSTOM = 1;
private static final byte CANOPY = 2;
private int size;
private int[] x = new int[32];
private int[] y = new int[32];
private int[] z = new int[32];
private Object[] values = new Object[32];
private byte[] flags = new byte[32];
static MutationLog of(List<ObjectDestinationTransaction.Mutation> mutations) {
MutationLog log = new MutationLog();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
Objects.requireNonNull(mutation, "mutation");
ObjectDestinationTransaction.DataKey key = mutation.key();
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
Object value = Objects.requireNonNull(set.value(), "mutation value");
if (ObjectSourcePlan.typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
log.add(key.x(), key.y(), key.z(), value, false, set.canopy());
}
case ObjectDestinationTransaction.CustomBlockMutation block -> log.add(key.x(), key.y(), key.z(),
Objects.requireNonNull(block.state(), "custom block state"), true, block.canopy());
}
}
return log;
}
void add(int blockX, int blockY, int blockZ, Object value, boolean custom, boolean canopy) {
if (size == values.length) {
int capacity = size * 2;
x = Arrays.copyOf(x, capacity);
y = Arrays.copyOf(y, capacity);
z = Arrays.copyOf(z, capacity);
values = Arrays.copyOf(values, capacity);
flags = Arrays.copyOf(flags, capacity);
}
x[size] = blockX;
y[size] = blockY;
z[size] = blockZ;
values[size] = value;
flags[size] = (byte) ((custom ? CUSTOM : 0) | (canopy ? CANOPY : 0));
size++;
}
int size() {
return size;
}
int x(int index) {
return x[index];
}
int y(int index) {
return y[index];
}
int z(int index) {
return z[index];
}
Object value(int index) {
return values[index];
}
boolean custom(int index) {
return (flags[index] & CUSTOM) != 0;
}
boolean canopy(int index) {
return (flags[index] & CANOPY) != 0;
}
Class<?> type(int index) {
return custom(index) ? NativeBlockState.class : ObjectSourcePlan.typeOf(values[index]);
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
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;
@@ -18,10 +17,11 @@ import org.jetbrains.annotations.Nullable;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final int destinationChunkX;
private final int destinationChunkZ;
private final int worldHeight;
private final List<Mutation> mutations;
private final MutationLog log = new MutationLog();
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private BlockPosition canopyPosition;
@@ -51,7 +51,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
this.destinationChunkX = destinationChunkX;
this.destinationChunkZ = destinationChunkZ;
this.worldHeight = writer.getMantle().getWorldHeight();
this.mutations = new ArrayList<>();
this.overlay = new Long2ObjectOpenHashMap<>();
}
@@ -60,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
int mutationCheckpoint() {
return mutations.size();
return log.size();
}
ObjectSourcePlan sourcePlanSince(int checkpoint) {
if (checkpoint < 0 || checkpoint > mutations.size()) {
if (checkpoint < 0 || checkpoint > log.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
}
ArrayList<PlacementRange> accepted = new ArrayList<>();
@@ -73,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
return new ObjectSourcePlan(log, checkpoint, accepted);
}
@Override
public int beginObjectPlacement() {
return mutations.size();
return log.size();
}
@Override
public void endObjectPlacement(int checkpoint) {
if (checkpoint >= 0 && checkpoint < mutations.size()) {
placements.add(new PlacementRange(checkpoint, mutations.size()));
if (checkpoint >= 0 && checkpoint < log.size()) {
placements.add(new PlacementRange(checkpoint, log.size()));
}
}
void apply(ObjectSourcePlan plan) {
int slot = plan.destinationSlot(destinationChunkX, destinationChunkZ);
if (slot < 0) {
return;
}
int baseX = destinationChunkX << 4;
int baseZ = destinationChunkZ << 4;
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;
int end = plan.slotEnd(slot);
for (int index = plan.slotStart(slot); index < end; index++) {
int cell = plan.cellAt(index);
int x = baseX | ObjectSourcePlan.cellX(cell);
int y = ObjectSourcePlan.cellY(cell);
int z = baseZ | ObjectSourcePlan.cellZ(cell);
Object value = plan.valueAt(index);
boolean custom = ObjectSourcePlan.isCustom(cell);
boolean canopy = ObjectSourcePlan.isCanopy(cell);
if (custom || value instanceof NativeBlockState) {
NativeBlockState existing = canopy ? get(x, y, z) : null;
skipCanopy = existing != null && existing.isSolid();
}
if (mutation.canopy() && skipCanopy) {
if (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);
if (custom) {
set(x, y, z, (NativeBlockState) value, true, canopy);
} else {
setData(x, y, z, value, true, canopy);
}
}
}
private void commitFenced() {
LinkedHashMap<DataKey, Object> originals = new LinkedHashMap<>();
CommitJournal journal = new CommitJournal();
try {
for (Mutation mutation : mutations) {
if (!isDestination(mutation.x(), mutation.z())) {
for (int position = 0; position < log.size(); position++) {
int x = log.x(position);
int z = log.z(position);
if (!isDestination(x, z)) {
continue;
}
captureOriginal(originals, mutation.key());
if (mutation.key().type() == NativeBlockState.class) {
TileWrapper tile = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class);
if (tile != null) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile);
int y = log.y(position);
Object value = log.value(position);
boolean custom = log.custom(position);
Class<?> type = log.type(position);
if (!journal.captured(x, y, z, type)) {
journal.capture(x, y, z, type, publishedOrCleared(x, y, z, type));
}
if (type == NativeBlockState.class) {
if (!journal.captured(x, y, z, TileWrapper.class)) {
TileWrapper tile = writer.getDataIfPresent(x, y, z, TileWrapper.class);
if (tile != null) {
journal.capture(x, y, z, TileWrapper.class, tile);
}
}
Identifier identifier = writer.getDataIfPresent(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class);
if (identifier != null || mutation instanceof CustomBlockMutation) {
originals.putIfAbsent(new DataKey(
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class),
identifier == null ? CLEARED : identifier);
if (!journal.captured(x, y, z, Identifier.class)) {
Identifier identifier = writer.getDataIfPresent(x, y, z, Identifier.class);
if (identifier != null || custom) {
journal.capture(x, y, z, Identifier.class, identifier == null ? CLEARED : identifier);
}
}
}
mutation.apply(writer);
if (custom) {
writer.set(x, y, z, (NativeBlockState) value);
} else {
writer.setData(x, y, z, value);
}
}
} catch (Throwable failure) {
rollback(originals, failure);
journal.rollback(failure);
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
@@ -154,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override
public void set(int x, int y, int z, NativeBlockState state) {
set(x, y, z, state, null);
set(x, y, z, state, false, false);
}
private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) {
private void set(int x, int y, int z, NativeBlockState state, boolean replayed, boolean replayedCanopy) {
if (state == null) {
return;
}
@@ -172,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cell.block = baseState;
cell.tile = CLEARED;
cell.identifier = identifier;
mutations.add(replayed != null
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z));
return;
}
setData(x, y, z, state);
@@ -267,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override
public <T> void setData(int x, int y, int z, T data) {
setData(x, y, z, data, null);
setData(x, y, z, data, false, false);
}
private void setData(int x, int y, int z, Object data, SetMutation replayed) {
private void setData(int x, int y, int z, Object data, boolean replayed, boolean replayedCanopy) {
if (data == null || y < 0 || y >= worldHeight
|| protectsSubterrain(x, y, z)) {
return;
@@ -290,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} else {
cell.put(type, data);
}
mutations.add(replayed != null
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z));
}
@Override
@@ -373,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return value == null ? CLEARED : value;
}
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
originals.computeIfAbsent(
key,
candidate -> publishedOrCleared(
candidate.x(),
candidate.y(),
candidate.z(),
candidate.type()
)
);
}
private boolean isDestination(int x, int z) {
return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ;
}
@@ -433,22 +444,44 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return hydrologyGateOpen;
}
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
for (int index = entries.size() - 1; index >= 0; index--) {
Map.Entry<DataKey, Object> entry = entries.get(index);
DataKey key = entry.getKey();
try {
writer.restoreData(key.x(), key.y(), key.z(), key.type(),
entry.getValue() == CLEARED ? null : entry.getValue());
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
private final class CommitJournal {
private final Reference2IntOpenHashMap<Class<?>> typeIds = new Reference2IntOpenHashMap<>();
private final LongOpenHashSet keys = new LongOpenHashSet();
private final ArrayList<Original> originals = new ArrayList<>();
private long key(int x, int y, int z, Class<?> type) {
int typeId = typeIds.computeIfAbsent(type, (Class<?> ignored) -> typeIds.size());
return ((long) typeId << 32) | ((long) y << 8) | ((long) (z & 15) << 4) | (x & 15);
}
private boolean captured(int x, int y, int z, Class<?> type) {
return keys.contains(key(x, y, z, type));
}
private void capture(int x, int y, int z, Class<?> type, Object value) {
if (keys.add(key(x, y, z, type))) {
originals.add(new Original(x, y, z, type, value));
}
}
private void rollback(Throwable failure) {
for (int index = originals.size() - 1; index >= 0; index--) {
Original original = originals.get(index);
try {
writer.restoreData(original.x(), original.y(), original.z(), original.type(),
original.value() == CLEARED ? null : original.value());
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
}
}
}
}
private record Original(int x, int y, int z, Class<?> type, Object value) {
}
private static final class OverlayCell {
private Object block;
private Object hydrology;
@@ -520,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
DataKey key();
boolean canopy();
void apply(IObjectPlacer placer);
}
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
@@ -534,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() {
return key.z();
}
@Override
public void apply(IObjectPlacer placer) {
placer.setData(key.x(), key.y(), key.z(), value);
}
}
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
@@ -551,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() {
return key.z();
}
@Override
public void apply(IObjectPlacer placer) {
placer.set(key.x(), key.y(), key.z(), state);
}
}
}
@@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
final class ObjectSourcePlan {
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
@@ -45,18 +44,21 @@ final class ObjectSourcePlan {
private final int estimatedRetainedBytes;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
this(MutationLog.of(mutations), 0, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = mutations.size();
this(MutationLog.of(mutations), 0, accepted);
}
ObjectSourcePlan(MutationLog log, int from, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = log.size() - from;
long[] mutationDestinations = new long[count];
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
destinationSlots.defaultReturnValue(-1);
LongArrayList firstSeen = new LongArrayList();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4);
mutationDestinations[index] = destination;
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
firstSeen.add(destination);
@@ -80,38 +82,23 @@ final class ObjectSourcePlan {
valueIndexes = new int[count];
ValueTable table = new ValueTable();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
int logged = from + index;
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
positions[index] = position;
ObjectDestinationTransaction.DataKey key = mutation.key();
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
int y = log.y(logged);
if (y < 0 || y > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y);
}
Object value;
int custom;
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
value = Objects.requireNonNull(set.value(), "mutation value");
if (typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
custom = 0;
}
case ObjectDestinationTransaction.CustomBlockMutation block -> {
value = Objects.requireNonNull(block.state(), "custom block state");
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0)
| ((log.z(logged) & 15) << 4) | (log.x(logged) & 15);
valueIndexes[position] = table.indexOf(log.value(logged));
}
values = table.values.toArray();
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), log, from, mutationDestinations, destinationSlots, positions);
if (placement != null) {
crossing.add(placement);
}
@@ -147,12 +134,13 @@ final class ObjectSourcePlan {
int ordinal,
int start,
int end,
List<ObjectDestinationTransaction.Mutation> mutations,
MutationLog log,
int from,
long[] mutationDestinations,
Long2IntOpenHashMap destinationSlots,
int[] positions
) {
if (start < 0 || end > mutations.size() || start >= end) {
if (start < 0 || end > log.size() - from || start >= end) {
throw new IllegalArgumentException("Object placement range is outside the plan");
}
long first = mutationDestinations[start];
@@ -172,11 +160,12 @@ final class ObjectSourcePlan {
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
int mutationX = log.x(from + mutationIndex);
int mutationZ = log.z(from + mutationIndex);
minimumX = Math.min(minimumX, mutationX);
minimumZ = Math.min(minimumZ, mutationZ);
maximumX = Math.max(maximumX, mutationX);
maximumZ = Math.max(maximumZ, mutationZ);
long destination = mutationDestinations[mutationIndex];
int touchedIndex = touchedIndexes.get(destination);
if (touchedIndex < 0) {
@@ -216,6 +205,55 @@ final class ObjectSourcePlan {
return destinations.length == 0;
}
int slotStart(int slot) {
return offsets[slot];
}
int slotEnd(int slot) {
return offsets[slot + 1];
}
int cellAt(int index) {
return cells[index];
}
Object valueAt(int index) {
return values[valueIndexes[index]];
}
static int cellX(int cell) {
return cell & 15;
}
static int cellZ(int cell) {
return (cell >>> 4) & 15;
}
static int cellY(int cell) {
return cell >>> Y_SHIFT;
}
static boolean isCustom(int cell) {
return (cell & CUSTOM_BIT) != 0;
}
static boolean isCanopy(int cell) {
return (cell & CANOPY_BIT) != 0;
}
ObjectDestinationTransaction.Mutation mutationAt(int chunkX, int chunkZ, int index) {
int cell = cells[index];
int x = (chunkX << 4) | cellX(cell);
int z = (chunkZ << 4) | cellZ(cell);
int y = cellY(cell);
Object value = valueAt(index);
return isCustom(cell)
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, isCanopy(cell))
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, isCanopy(cell));
}
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
int slot = destinationSlot(chunkX, chunkZ);
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
@@ -266,19 +304,8 @@ final class ObjectSourcePlan {
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
for (int index = 0; index < length; index++) {
int cell = cells[start + index];
int x = baseX | (cell & 15);
int z = baseZ | ((cell >>> 4) & 15);
int y = cell >>> Y_SHIFT;
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, (cell & CANOPY_BIT) != 0)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
decoded[index] = mutationAt(chunkX, chunkZ, start + index);
}
return List.of(decoded);
}
@@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
@@ -0,0 +1,81 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class MutationLogTest {
@Test
public void logKeepsPositionsValuesAndFlagsInOrder() {
NativeBlockState state = new KeyedBlockState("minecraft:stone");
MutationLog log = MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(3, 70, -2, String.class), "marker", true),
new ObjectDestinationTransaction.CustomBlockMutation(new ObjectDestinationTransaction.DataKey(-17, 4, 40, NativeBlockState.class), state, false)));
for (int index = 0; index < 100; index++) {
log.add(index, index + 1, -index, "grow" + index, (index & 1) == 0, (index & 2) == 0);
}
assertEquals(102, log.size());
assertEquals(3, log.x(0));
assertEquals(70, log.y(0));
assertEquals(-2, log.z(0));
assertEquals("marker", log.value(0));
assertFalse(log.custom(0));
assertTrue(log.canopy(0));
assertSame(String.class, log.type(0));
assertSame(state, log.value(1));
assertTrue(log.custom(1));
assertFalse(log.canopy(1));
assertSame(NativeBlockState.class, log.type(1));
assertEquals(99, log.x(101));
assertEquals("grow99", log.value(101));
assertFalse(log.custom(101));
assertTrue(log.canopy(98));
assertFalse(log.canopy(100));
}
@Test
public void mismatchedMutationTypesAreRejected() {
assertThrows(IllegalArgumentException.class, () -> MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(0, 4, 0, Integer.class), "text", false))));
}
@Test
public void planSlicesMatchThePlanBuiltFromTheSameMutations() {
MutationLog log = new MutationLog();
List<ObjectDestinationTransaction.Mutation> tail = new ArrayList<>();
for (int index = 0; index < 64; index++) {
int x = (index * 7) - 30;
int y = 4 + index;
int z = (index * 5) - 20;
String value = "value" + (index % 9);
boolean canopy = index % 4 == 0;
log.add(x, y, z, value, false, canopy);
if (index >= 10) {
tail.add(new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, y, z, String.class), value, canopy));
}
}
ObjectSourcePlan sliced = new ObjectSourcePlan(log, 10, List.of());
ObjectSourcePlan expected = new ObjectSourcePlan(tail);
assertEquals(expected.destinationCount(), sliced.destinationCount());
int total = 0;
for (int chunkX = -3; chunkX <= 30; chunkX++) {
for (int chunkZ = -3; chunkZ <= 30; chunkZ++) {
List<ObjectDestinationTransaction.Mutation> mutations = sliced.mutationsFor(chunkX, chunkZ);
assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations);
total += mutations.size();
}
}
assertEquals(54, total);
}
}