diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java index 35123610a..e8a47f754 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java @@ -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 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 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 @Nullable T getData(int x, int y, int z, Class 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); - } - } - } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java b/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java new file mode 100644 index 000000000..2480cf184 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java @@ -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 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]); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java index 0622f536b..546c14200 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java @@ -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 mutations; + private final MutationLog log = new MutationLog(); private final List placements = new ArrayList<>(); private final Long2ObjectOpenHashMap> 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 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 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 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 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 originals, Throwable failure) { - ArrayList> entries = new ArrayList<>(originals.entrySet()); - for (int index = entries.size() - 1; index >= 0; index--) { - Map.Entry 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> typeIds = new Reference2IntOpenHashMap<>(); + private final LongOpenHashSet keys = new LongOpenHashSet(); + private final ArrayList 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); - } } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java index 236c1b90c..ab7e12def 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java @@ -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 mutations) { - this(mutations, List.of()); + this(MutationLog.of(mutations), 0, List.of()); } ObjectSourcePlan(List mutations, List accepted) { - int count = mutations.size(); + this(MutationLog.of(mutations), 0, accepted); + } + + ObjectSourcePlan(MutationLog log, int from, List 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 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 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 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 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); } diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java index af595b13c..36d05ab82 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java @@ -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; diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java new file mode 100644 index 000000000..9a47e059d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java @@ -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 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 mutations = sliced.mutationsFor(chunkX, chunkZ); + assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations); + total += mutations.size(); + } + } + assertEquals(54, total); + } +}