mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
1 parent
fd47240db9
commit
ad73c33480
6 files changed
+410
-191
No files matched your search
+67
-65
@@ -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]);
|
||||
}
|
||||
}
|
||||
+99
-78
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user