mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +00:00
Merged object bake
- Baked rotated object variants, packed transactions, non-blocking source plan builds
This commit is contained in:
commit
544707f5d6
16 files changed
+2096
-559
No files matched your search
+102
-71
@@ -30,27 +30,41 @@ 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.HashMap;
|
||||
import java.util.Map;
|
||||
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;
|
||||
private static final int POSITION_LIMIT = 1 << (POSITION_BITS - 1);
|
||||
private static final long POSITION_MASK = (1L << POSITION_BITS) - 1L;
|
||||
|
||||
private final IObjectPlacer delegate;
|
||||
private final Engine engine;
|
||||
private final int anchorY;
|
||||
private final int minDepthBelowSurface;
|
||||
private final int worldHeight;
|
||||
private final KList<BufferedMutation> mutations;
|
||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
||||
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;
|
||||
private int blockWrites;
|
||||
private BlockPosition canopyPosition;
|
||||
private boolean originKnown;
|
||||
private int originX;
|
||||
private int originZ;
|
||||
|
||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||
this.delegate = delegate;
|
||||
@@ -58,8 +72,7 @@ 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 HashMap<>();
|
||||
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
|
||||
this.surfaceHeights = new Long2IntOpenHashMap();
|
||||
this.caveCeilings = new Long2IntOpenHashMap();
|
||||
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
||||
@@ -73,28 +86,41 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
}
|
||||
|
||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
boolean previousChecked = false;
|
||||
int previousX = 0;
|
||||
int previousY = 0;
|
||||
int previousZ = 0;
|
||||
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 = x;
|
||||
previousY = y;
|
||||
previousZ = z;
|
||||
}
|
||||
|
||||
applyMutations();
|
||||
@@ -106,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 {
|
||||
@@ -128,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);
|
||||
@@ -164,8 +218,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
buffer(BLOCK, x, y, z, state);
|
||||
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
|
||||
@@ -190,7 +244,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
|
||||
return buffered == null ? delegate.get(x, y, z) : buffered;
|
||||
}
|
||||
|
||||
@@ -211,7 +265,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z));
|
||||
return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered);
|
||||
}
|
||||
|
||||
@@ -242,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,21 +305,20 @@ 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(new PositionKey(x, y, z), state);
|
||||
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
@@ -278,6 +331,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
return engine;
|
||||
}
|
||||
|
||||
private long bufferKey(int x, int y, int z) {
|
||||
if (!originKnown) {
|
||||
originX = x;
|
||||
originZ = z;
|
||||
originKnown = true;
|
||||
}
|
||||
int dx = x - originX;
|
||||
int dz = z - originZ;
|
||||
if (dx < -POSITION_LIMIT || dx >= POSITION_LIMIT || dz < -POSITION_LIMIT || dz >= POSITION_LIMIT
|
||||
|| y < -POSITION_LIMIT || y >= POSITION_LIMIT) {
|
||||
throw new IllegalStateException("Cave object write at " + x + "," + y + "," + z
|
||||
+ " is too far from its first write at " + originX + "," + originZ);
|
||||
}
|
||||
return ((dx & POSITION_MASK) << (POSITION_BITS * 2)) | ((dz & POSITION_MASK) << POSITION_BITS) | (y & POSITION_MASK);
|
||||
}
|
||||
|
||||
private boolean isWithinBounds(int x, int y, int z) {
|
||||
if (engine == null || y < 0 || y >= worldHeight) {
|
||||
return false;
|
||||
@@ -325,42 +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);
|
||||
}
|
||||
}
|
||||
|
||||
private record PositionKey(int x, int y, int z) {
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
|
||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
||||
private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L;
|
||||
private static final int MAXIMUM_TREE_MATERIALS = 8192;
|
||||
private static final Map<NativeBlockState, TreeBlockMaterial> TREE_MATERIALS = new ConcurrentHashMap<>();
|
||||
|
||||
private final Object collisionRuleLock;
|
||||
private final ObjectSourcePlanCache sourcePlans;
|
||||
@@ -124,7 +126,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, NativeBlockState data) {
|
||||
placer.setData(x, y, z, TreeBlockMaterial.of(data));
|
||||
TreeBlockMaterial material = TREE_MATERIALS.get(data);
|
||||
if (material == null) {
|
||||
material = TreeBlockMaterial.of(data);
|
||||
if (TREE_MATERIALS.size() < MAXIMUM_TREE_MATERIALS) {
|
||||
TREE_MATERIALS.putIfAbsent(data, material);
|
||||
}
|
||||
}
|
||||
placer.setData(x, y, z, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||
int radius = getRadius();
|
||||
int sourceChunkRadius = sourceChunkRadius(radius);
|
||||
replaySourceChunks(
|
||||
x,
|
||||
z,
|
||||
radius,
|
||||
(sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
}
|
||||
);
|
||||
int side = (sourceChunkRadius * 2) + 1;
|
||||
ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side];
|
||||
int[] slot = new int[1];
|
||||
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire(
|
||||
sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context)));
|
||||
slot[0] = 0;
|
||||
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = plans[slot[0]++];
|
||||
if (plan == ObjectSourcePlanCache.BUSY) {
|
||||
plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
}
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
});
|
||||
transaction.commit();
|
||||
logSourcePlanStats();
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
|
||||
synchronized (chunk) {
|
||||
Matter matter = chunk.getOrCreate(y >> 4);
|
||||
if ((t instanceof NativeBlockState || t instanceof MatterCavern)
|
||||
&& (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) {
|
||||
&& hasProtectedHydrology(matter, x, y, z)) {
|
||||
return;
|
||||
}
|
||||
if (t instanceof NativeBlockState) {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
+114
-79
@@ -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;
|
||||
@@ -40,13 +40,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private int hydrologyGateChunkX;
|
||||
private int hydrologyGateChunkZ;
|
||||
private boolean hydrologyGateOpen;
|
||||
private boolean subterrainKnown;
|
||||
private int subterrainX;
|
||||
private int subterrainY;
|
||||
private int subterrainZ;
|
||||
private boolean subterrainProtects;
|
||||
|
||||
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||
this.writer = writer;
|
||||
this.destinationChunkX = destinationChunkX;
|
||||
this.destinationChunkZ = destinationChunkZ;
|
||||
this.worldHeight = writer.getMantle().getWorldHeight();
|
||||
this.mutations = new ArrayList<>();
|
||||
this.overlay = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
@@ -55,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<>();
|
||||
@@ -68,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;
|
||||
}
|
||||
@@ -149,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;
|
||||
}
|
||||
@@ -167,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);
|
||||
@@ -262,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;
|
||||
@@ -285,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
|
||||
@@ -368,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;
|
||||
}
|
||||
@@ -396,8 +412,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
|
||||
private boolean protectsSubterrain(int x, int y, int z) {
|
||||
if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) {
|
||||
return subterrainProtects;
|
||||
}
|
||||
Engine engine = getEngine();
|
||||
return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
|
||||
boolean protects = engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z));
|
||||
subterrainX = x;
|
||||
subterrainY = y;
|
||||
subterrainZ = z;
|
||||
subterrainProtects = protects;
|
||||
subterrainKnown = true;
|
||||
return protects;
|
||||
}
|
||||
|
||||
private boolean hasProtectedHydrology(int x, int y, int z) {
|
||||
@@ -419,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;
|
||||
@@ -506,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 {
|
||||
@@ -520,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 {
|
||||
@@ -537,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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
@@ -14,6 +15,7 @@ import java.util.function.Supplier;
|
||||
import static art.arcane.iris.generation.cache.Cache.key;
|
||||
|
||||
final class ObjectSourcePlanCache {
|
||||
static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of());
|
||||
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
||||
private static final int ENTRY_BYTES = 64;
|
||||
|
||||
@@ -68,6 +70,19 @@ final class ObjectSourcePlanCache {
|
||||
return plan;
|
||||
}
|
||||
|
||||
ObjectSourcePlan tryAcquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
|
||||
int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
|
||||
State current = state;
|
||||
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
|
||||
if (current.plans.getIfPresent(source) == null) {
|
||||
Pending pending = current.pending.get(source);
|
||||
if (pending != null && pending.owner != Thread.currentThread()) {
|
||||
return BUSY;
|
||||
}
|
||||
}
|
||||
return acquire(sourceChunkX, sourceChunkZ, destinationChunkX, destinationChunkZ, sourceChunkRadius, builder);
|
||||
}
|
||||
|
||||
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
|
||||
Supplier<ObjectSourcePlan> builder) {
|
||||
int slot = entry.slot(destinationChunkX, destinationChunkZ);
|
||||
|
||||
@@ -61,6 +61,7 @@ import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
|
||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
||||
transient final ConcurrentHashMap<ObjectPlacementBake.Key, ObjectPlacementBake> placementBakes = new ConcurrentHashMap<>();
|
||||
@Getter
|
||||
@Setter
|
||||
private transient TreeGrounding treeGrounding;
|
||||
|
||||
+304
-346
@@ -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;
|
||||
@@ -52,6 +51,7 @@ import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -475,127 +475,53 @@ final class IrisObjectPlacementRunner {
|
||||
int vacuumLowest = Integer.MAX_VALUE;
|
||||
int vacuumHighest = Integer.MIN_VALUE;
|
||||
self.readLock.lock();
|
||||
|
||||
KMap<IrisBlockVector, String> markers = null;
|
||||
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
VectorMap<TileData> states = self.states;
|
||||
TreeGrounding tree = self.getTreeGrounding();
|
||||
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
|
||||
boolean hasStates = !states.isEmpty();
|
||||
ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null);
|
||||
PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz,
|
||||
preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials);
|
||||
double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null;
|
||||
String[] markers = config.getMarkers().isNotEmpty() && placer.getEngine() != null
|
||||
? resolveMarkers(config, rng, x, yv, z, rdata, bake) : null;
|
||||
int[] tileBlocks = bake.tileBlocks();
|
||||
TileData[] tileValues = bake.tileValues();
|
||||
int tileCursor = 0;
|
||||
int count = bake.count();
|
||||
|
||||
if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) {
|
||||
markers = new KMap<>();
|
||||
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
|
||||
.collect(KList.collector());
|
||||
// Marker selection persists into the mantle, so it must be seed-deterministic.
|
||||
// Derive a side stream keyed on the placement position instead of consuming the
|
||||
// caller's rng: consuming draws there would shift every later placement in the
|
||||
// chunk and invalidate existing worlds/goldenhashes.
|
||||
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
|
||||
|
||||
int markerIndex = 0;
|
||||
for (IrisObjectMarker j : config.getMarkers()) {
|
||||
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
|
||||
int markerSalt = markerIndex++;
|
||||
|
||||
if (marker == null || marker.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int max = j.getMaximumMarkers();
|
||||
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
NativeBlockState data = blocks.get(i);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (NativeBlockState k : j.getMark(rdata)) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
|
||||
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
|
||||
|
||||
if (!marker.isEmptyAbove() || (a && fff)) {
|
||||
markers.put(i, j.getMarker());
|
||||
max--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector g = cursor.key();
|
||||
NativeBlockState d;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int entry = bake.paletteIndex(index);
|
||||
TileData tile = null;
|
||||
if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) {
|
||||
tile = tileValues[tileCursor++];
|
||||
}
|
||||
|
||||
try {
|
||||
d = cursor.value();
|
||||
if (hasStates) {
|
||||
tile = states.get(g);
|
||||
if (stilting && palette.stiltLayer(entry)) {
|
||||
int layer = bake.layerY(index);
|
||||
if (layer < lowest) {
|
||||
lowest = layer;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)");
|
||||
d = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (d == null) {
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)");
|
||||
d = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) {
|
||||
d = IrisObject.States.vairDebug();
|
||||
}
|
||||
|
||||
NativeBlockState data = d;
|
||||
IrisBlockVector i = g.clone();
|
||||
spin.rotate(i);
|
||||
if (ceilingHang) {
|
||||
i.setY(-i.getBlockY());
|
||||
}
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
|
||||
if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) {
|
||||
if (i.getBlockY() < lowest) {
|
||||
lowest = i.getBlockY();
|
||||
}
|
||||
if (i.getBlockY() > topLayer) {
|
||||
topLayer = i.getBlockY();
|
||||
if (layer > topLayer) {
|
||||
topLayer = layer;
|
||||
}
|
||||
}
|
||||
|
||||
if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) {
|
||||
data = data.withProperty("persistent", "true");
|
||||
}
|
||||
|
||||
NativeBlockState data = palette.decayed(entry);
|
||||
boolean edited = false;
|
||||
if (hasEdits) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
for (IrisObjectReplace j : config.getEdit()) {
|
||||
if (rng.chance(j.getChance())) {
|
||||
for (NativeBlockState k : j.getFind(rdata)) {
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
|
||||
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
|
||||
|
||||
boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data));
|
||||
if (sameMaterial && !(newData.isCustom() || data.isCustom()))
|
||||
data = BlockDataMergeSupport.merge(data, newData);
|
||||
else
|
||||
data = newData;
|
||||
edited = true;
|
||||
|
||||
Optional<TileData> t = j.getReplace().getTile(rng, x, y, z, rdata);
|
||||
if (t.isPresent()) {
|
||||
@@ -609,18 +535,21 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
data = config.getRotation().rotate(data, spinx, spiny, spinz);
|
||||
if (data == null) {
|
||||
StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry);
|
||||
NativeBlockState rotated = facts.rotated();
|
||||
if (rotated == null) {
|
||||
continue;
|
||||
}
|
||||
xx = x + (int) Math.round(i.getX());
|
||||
|
||||
int yy = y + (int) Math.round(i.getY());
|
||||
zz = z + (int) Math.round(i.getZ());
|
||||
xx = x + bake.offsetX(index);
|
||||
int yy = y + bake.offsetY(index);
|
||||
zz = z + bake.offsetZ(index);
|
||||
|
||||
if (warped) {
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
|
||||
}
|
||||
|
||||
if (paint) {
|
||||
@@ -628,8 +557,8 @@ final class IrisObjectPlacementRunner {
|
||||
if (surface == IrisPaintSurfaceProjection.MISSING) {
|
||||
continue;
|
||||
}
|
||||
if (!B.isVineBlock(data)) {
|
||||
yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand;
|
||||
if (!B.isVineBlock(rotated)) {
|
||||
yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,13 +566,18 @@ final class IrisObjectPlacementRunner {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ());
|
||||
boolean canopy = bake.canopy(index);
|
||||
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
|
||||
data = rotated;
|
||||
if (facts.waterloggable()) {
|
||||
data = waterlogCandidate || facts.authoredWaterlogged()
|
||||
? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate)
|
||||
: facts.dry();
|
||||
}
|
||||
|
||||
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||
data = attachVineFaces(placer, data, xx, yy, zz);
|
||||
@@ -652,9 +586,7 @@ final class IrisObjectPlacementRunner {
|
||||
// Short-circuit order matters for cost only: the mantle read is paid solely
|
||||
// for vine blocks. Both operands are pure, so the value is unchanged.
|
||||
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||
String material = IrisObjectShaping.materialKey(data);
|
||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace);
|
||||
|
||||
if (data.isCustom() || place) {
|
||||
placer.set(xx, yy, zz, data);
|
||||
@@ -668,8 +600,8 @@ final class IrisObjectPlacementRunner {
|
||||
if (tile != null) {
|
||||
placer.setTile(xx, yy, zz, tile);
|
||||
}
|
||||
if (markers != null && markers.containsKey(g)) {
|
||||
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
|
||||
if (markers != null && markers[index] != null) {
|
||||
placer.setData(xx, yy, zz, new MatterMarker(markers[index]));
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(xx, yy, zz), data);
|
||||
@@ -687,27 +619,21 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
if (grounding != null) {
|
||||
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
|
||||
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
|
||||
extension.x(), rootY, extension.z(), waterlogCandidate);
|
||||
placer.set(extension.x(), rootY, extension.z(), rootState);
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
|
||||
if (grounding != null) {
|
||||
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
|
||||
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
|
||||
extension.x(), rootY, extension.z(), waterlogCandidate);
|
||||
placer.set(extension.x(), rootY, extension.z(), rootState);
|
||||
if (listener != null) {
|
||||
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stilting) {
|
||||
self.readLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
if (stilting) {
|
||||
IrisStiltSettings settings = stiltSettings;
|
||||
|
||||
double erodeCentroidX = 0;
|
||||
@@ -715,93 +641,51 @@ final class IrisObjectPlacementRunner {
|
||||
double erodeMaxDist = 1;
|
||||
if (eroding) {
|
||||
int centroidCount = 0;
|
||||
VectorMap<NativeBlockState>.Cursor centroidCursor = blocks.cursor();
|
||||
while (centroidCursor.next()) {
|
||||
IrisBlockVector rot = centroidCursor.key().clone();
|
||||
spin.rotate(rot);
|
||||
if (translating) {
|
||||
rot.add(translateOffset);
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
NativeBlockState bd = centroidCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
erodeCentroidX += rot.getX();
|
||||
erodeCentroidZ += rot.getZ();
|
||||
centroidCount++;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||
erodeCentroidX += exact[index * 3];
|
||||
erodeCentroidZ += exact[index * 3 + 2];
|
||||
centroidCount++;
|
||||
}
|
||||
}
|
||||
if (centroidCount > 0) {
|
||||
erodeCentroidX /= centroidCount;
|
||||
erodeCentroidZ /= centroidCount;
|
||||
}
|
||||
VectorMap<NativeBlockState>.Cursor spreadCursor = blocks.cursor();
|
||||
while (spreadCursor.next()) {
|
||||
IrisBlockVector rot = spreadCursor.key().clone();
|
||||
spin.rotate(rot);
|
||||
if (translating) {
|
||||
rot.add(translateOffset);
|
||||
}
|
||||
if (rot.getBlockY() == lowest) {
|
||||
NativeBlockState bd = spreadCursor.value();
|
||||
if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) {
|
||||
double dx = rot.getX() - erodeCentroidX;
|
||||
double dz = rot.getZ() - erodeCentroidZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > erodeMaxDist) {
|
||||
erodeMaxDist = dist;
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) {
|
||||
double dx = exact[index * 3] - erodeCentroidX;
|
||||
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > erodeMaxDist) {
|
||||
erodeMaxDist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorMap<NativeBlockState>.Cursor stiltCursor = blocks.cursor();
|
||||
while (stiltCursor.next()) {
|
||||
IrisBlockVector g = stiltCursor.key();
|
||||
NativeBlockState sourceData;
|
||||
try {
|
||||
sourceData = stiltCursor.value();
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)");
|
||||
sourceData = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (sourceData == null) {
|
||||
IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)");
|
||||
sourceData = IrisObject.States.air();
|
||||
}
|
||||
|
||||
if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) {
|
||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int entry = bake.paletteIndex(index);
|
||||
if (!palette.shouldStilt(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NativeBlockState d = sourceData;
|
||||
NativeBlockState d;
|
||||
if (settings != null && settings.getPalette() != null) {
|
||||
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
|
||||
} else {
|
||||
String mat = IrisObjectShaping.materialKey(d);
|
||||
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
|
||||
d = IrisObject.States.dirt();
|
||||
}
|
||||
d = palette.stiltDefault(entry);
|
||||
}
|
||||
|
||||
IrisBlockVector i = g.clone();
|
||||
spin.rotate(i);
|
||||
if (ceilingHang) {
|
||||
i.setY(-i.getBlockY());
|
||||
}
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||
if (i.getBlockY() != targetLayer)
|
||||
if (bake.layerY(index) != targetLayer)
|
||||
continue;
|
||||
d = config.getRotation().rotate(d, spinx, spiny, spinz);
|
||||
d = palette.rotate(d);
|
||||
|
||||
if (hasEdits) {
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
for (IrisObjectReplace j : config.getEdit()) {
|
||||
if (rng.chance(j.getChance())) {
|
||||
for (NativeBlockState k : j.getFind(rdata)) {
|
||||
@@ -809,7 +693,7 @@ final class IrisObjectPlacementRunner {
|
||||
continue;
|
||||
}
|
||||
if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) {
|
||||
NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata);
|
||||
NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata);
|
||||
|
||||
if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) {
|
||||
d = BlockDataMergeSupport.merge(d, newData);
|
||||
@@ -822,15 +706,18 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
}
|
||||
|
||||
if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials))
|
||||
if (d == null || !palette.shouldStiltPlaced(d))
|
||||
continue;
|
||||
|
||||
xx = x + (int) Math.round(i.getX());
|
||||
zz = z + (int) Math.round(i.getZ());
|
||||
xx = x + bake.offsetX(index);
|
||||
zz = z + bake.offsetZ(index);
|
||||
|
||||
if (warped) {
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x);
|
||||
double exactX = exact[index * 3];
|
||||
double exactY = exact[index * 3 + 1];
|
||||
double exactZ = exact[index * 3 + 2];
|
||||
xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z);
|
||||
zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x);
|
||||
}
|
||||
|
||||
if (organic) {
|
||||
@@ -891,8 +778,6 @@ final class IrisObjectPlacementRunner {
|
||||
|
||||
int highest = placer.getHighest(xx, zz, self.getLoader(), true);
|
||||
|
||||
|
||||
|
||||
int lowerBound = highest - 1;
|
||||
if (settings != null) {
|
||||
lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand());
|
||||
@@ -901,8 +786,8 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
|
||||
if (eroding) {
|
||||
double dx = i.getX() - erodeCentroidX;
|
||||
double dz = i.getZ() - erodeCentroidZ;
|
||||
double dx = exact[index * 3] - erodeCentroidX;
|
||||
double dz = exact[index * 3 + 2] - erodeCentroidZ;
|
||||
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
|
||||
normalizedDist = Math.min(normalizedDist, 1.0);
|
||||
int totalDepth = (lowest + y) - lowerBound;
|
||||
@@ -933,11 +818,10 @@ final class IrisObjectPlacementRunner {
|
||||
}
|
||||
placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate));
|
||||
}
|
||||
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
|
||||
if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) {
|
||||
@@ -974,9 +858,76 @@ final class IrisObjectPlacementRunner {
|
||||
if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||
return state;
|
||||
}
|
||||
if (!enabled && !"true".equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) {
|
||||
return IrisProceduralBlocks.normalizeWaterlogging(state, null, false);
|
||||
}
|
||||
return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled);
|
||||
}
|
||||
|
||||
private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata,
|
||||
ObjectPlacementBake bake) {
|
||||
self.readLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||
KMap<IrisBlockVector, String> markers = new KMap<>();
|
||||
KList<IrisBlockVector> list = StreamSupport.stream(blocks.keys().spliterator(), false)
|
||||
.collect(KList.collector());
|
||||
// Marker selection persists into the mantle, so it must be seed-deterministic.
|
||||
// Derive a side stream keyed on the placement position instead of consuming the
|
||||
// caller's rng: consuming draws there would shift every later placement in the
|
||||
// chunk and invalidate existing worlds/goldenhashes.
|
||||
RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv);
|
||||
|
||||
int markerIndex = 0;
|
||||
for (IrisObjectMarker j : config.getMarkers()) {
|
||||
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
|
||||
int markerSalt = markerIndex++;
|
||||
|
||||
if (marker == null || marker.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int max = j.getMaximumMarkers();
|
||||
for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
NativeBlockState data = blocks.get(i);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (NativeBlockState k : j.getMark(rdata)) {
|
||||
if (max <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) {
|
||||
boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0)));
|
||||
boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0)));
|
||||
|
||||
if (!marker.isEmptyAbove() || (a && fff)) {
|
||||
markers.put(i, j.getMarker());
|
||||
max--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String[] resolved = new String[bake.count()];
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (index < resolved.length && cursor.next()) {
|
||||
resolved[index++] = markers.get(cursor.key());
|
||||
}
|
||||
return resolved;
|
||||
} finally {
|
||||
self.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private KList<IrisBlockVector> transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset,
|
||||
CNG warp, double half, int x, int y, int z) {
|
||||
KList<IrisBlockVector> offsets = new KList<>();
|
||||
@@ -1337,152 +1288,159 @@ final class IrisObjectPlacementRunner {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputed vector rotation for a single placement.
|
||||
* <p>
|
||||
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
|
||||
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
|
||||
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
|
||||
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
|
||||
* the value the per-call form would have produced.
|
||||
*/
|
||||
private static final class SpinKernel {
|
||||
private static final int MODE_NONE = 0;
|
||||
private static final int MODE_FLIP = 1;
|
||||
private static final int MODE_QUARTER = 2;
|
||||
private static final int MODE_THREE_QUARTER = 3;
|
||||
private static final int MODE_ANGLE = 4;
|
||||
private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged,
|
||||
NativeBlockState dry, boolean air) {
|
||||
private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false);
|
||||
}
|
||||
|
||||
private final boolean rotates;
|
||||
private final int xMode;
|
||||
private final int yMode;
|
||||
private final int zMode;
|
||||
private final double xCos;
|
||||
private final double xSin;
|
||||
private final double yCos;
|
||||
private final double ySin;
|
||||
private final double zCos;
|
||||
private final double zSin;
|
||||
private static final class PlacementStates {
|
||||
private static final Object ROTATED_AWAY = new Object();
|
||||
private static final byte UNKNOWN = 0;
|
||||
private static final byte NO = 1;
|
||||
private static final byte YES = 2;
|
||||
|
||||
private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
|
||||
rotates = rotation.canRotate();
|
||||
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
|
||||
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
|
||||
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
|
||||
private final NativeBlockState[] palette;
|
||||
private final IrisObjectRotation rotation;
|
||||
private final int spinx;
|
||||
private final int spiny;
|
||||
private final int spinz;
|
||||
private final boolean preventingDecay;
|
||||
private final boolean debugSmartBore;
|
||||
private final Set<String> excludedStiltMaterials;
|
||||
private final NativeBlockState[] decayed;
|
||||
private final StateFacts[] facts;
|
||||
private final byte[] stiltLayers;
|
||||
private final byte[] stilts;
|
||||
private final NativeBlockState[] stiltDefaults;
|
||||
private IdentityHashMap<NativeBlockState, Object> rotations;
|
||||
private IdentityHashMap<NativeBlockState, StateFacts> editedFacts;
|
||||
private IdentityHashMap<NativeBlockState, Boolean> placedStilts;
|
||||
|
||||
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
|
||||
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
|
||||
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
|
||||
xCos = Math.cos(xAngle);
|
||||
xSin = Math.sin(xAngle);
|
||||
zCos = Math.cos(zAngle);
|
||||
zSin = Math.sin(zAngle);
|
||||
yCos = Math.cos(yAngle);
|
||||
ySin = Math.sin(yAngle);
|
||||
private PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz,
|
||||
boolean preventingDecay, boolean debugSmartBore, Set<String> excludedStiltMaterials) {
|
||||
this.palette = palette;
|
||||
this.rotation = rotation;
|
||||
this.spinx = spinx;
|
||||
this.spiny = spiny;
|
||||
this.spinz = spinz;
|
||||
this.preventingDecay = preventingDecay;
|
||||
this.debugSmartBore = debugSmartBore;
|
||||
this.excludedStiltMaterials = excludedStiltMaterials;
|
||||
this.decayed = new NativeBlockState[palette.length];
|
||||
this.facts = new StateFacts[palette.length];
|
||||
this.stiltLayers = new byte[palette.length];
|
||||
this.stilts = new byte[palette.length];
|
||||
this.stiltDefaults = new NativeBlockState[palette.length];
|
||||
}
|
||||
|
||||
private static int modeOf(IrisAxisRotationClamp clamp) {
|
||||
if (!clamp.isLocked()) {
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
if (Math.abs(clamp.getMax()) % 360D == 180D) {
|
||||
return MODE_FLIP;
|
||||
}
|
||||
|
||||
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
|
||||
return MODE_QUARTER;
|
||||
}
|
||||
|
||||
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
|
||||
return MODE_THREE_QUARTER;
|
||||
}
|
||||
|
||||
return MODE_ANGLE;
|
||||
private NativeBlockState source(int entry) {
|
||||
NativeBlockState state = palette[entry];
|
||||
return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
|
||||
*/
|
||||
private void rotate(IrisBlockVector v) {
|
||||
if (!rotates) {
|
||||
return;
|
||||
private boolean stiltLayer(int entry) {
|
||||
byte known = stiltLayers[entry];
|
||||
if (known == UNKNOWN) {
|
||||
known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO;
|
||||
stiltLayers[entry] = known;
|
||||
}
|
||||
return known == YES;
|
||||
}
|
||||
|
||||
switch (xMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setZ(-v.getZ());
|
||||
v.setY(-v.getY());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(v.getY());
|
||||
v.setY(-z);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(-v.getY());
|
||||
v.setY(z);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double y = xCos * v.getY() - xSin * v.getZ();
|
||||
double z = xSin * v.getY() + xCos * v.getZ();
|
||||
v.setY(y);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private NativeBlockState decayed(int entry) {
|
||||
NativeBlockState resolved = decayed[entry];
|
||||
if (resolved == null) {
|
||||
NativeBlockState data = source(entry);
|
||||
resolved = preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance")
|
||||
&& "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))
|
||||
? data.withProperty("persistent", "true") : data;
|
||||
decayed[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
switch (zMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setY(-v.getY());
|
||||
v.setX(-v.getX());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(v.getX());
|
||||
v.setX(-y);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(-v.getX());
|
||||
v.setX(y);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = zCos * v.getX() - zSin * v.getY();
|
||||
double y = zSin * v.getX() + zCos * v.getY();
|
||||
v.setX(x);
|
||||
v.setY(y);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private StateFacts facts(int entry) {
|
||||
StateFacts resolved = facts[entry];
|
||||
if (resolved == null) {
|
||||
resolved = computeFacts(decayed(entry));
|
||||
facts[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
switch (yMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setX(-v.getX());
|
||||
v.setZ(-v.getZ());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(v.getZ());
|
||||
v.setZ(-x);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(-v.getZ());
|
||||
v.setZ(x);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = yCos * v.getX() + ySin * v.getZ();
|
||||
double z = -ySin * v.getX() + yCos * v.getZ();
|
||||
v.setX(x);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
private StateFacts factsOf(NativeBlockState state) {
|
||||
if (editedFacts == null) {
|
||||
editedFacts = new IdentityHashMap<>();
|
||||
}
|
||||
StateFacts resolved = editedFacts.get(state);
|
||||
if (resolved == null) {
|
||||
resolved = computeFacts(state);
|
||||
editedFacts.put(state, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private StateFacts computeFacts(NativeBlockState state) {
|
||||
NativeBlockState rotated = rotate(state);
|
||||
if (rotated == null) {
|
||||
return StateFacts.ROTATED_AWAY;
|
||||
}
|
||||
boolean waterloggable = IrisProceduralBlocks.hasProperty(rotated, "waterlogged");
|
||||
boolean authored = waterloggable && "true".equals(IrisProceduralBlocks.propertyValue(rotated, "waterlogged"));
|
||||
NativeBlockState dry = waterloggable ? IrisProceduralBlocks.normalizeWaterlogging(rotated, null, false) : rotated;
|
||||
String material = IrisObjectShaping.materialKey(rotated);
|
||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||
return new StateFacts(rotated, waterloggable, authored, dry, air);
|
||||
}
|
||||
|
||||
private NativeBlockState rotate(NativeBlockState state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if (rotations == null) {
|
||||
rotations = new IdentityHashMap<>();
|
||||
}
|
||||
Object memo = rotations.get(state);
|
||||
if (memo == null) {
|
||||
NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz);
|
||||
memo = rotated == null ? ROTATED_AWAY : rotated;
|
||||
rotations.put(state, memo);
|
||||
}
|
||||
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
|
||||
}
|
||||
|
||||
private boolean shouldStilt(int entry) {
|
||||
byte known = stilts[entry];
|
||||
if (known == UNKNOWN) {
|
||||
known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO;
|
||||
stilts[entry] = known;
|
||||
}
|
||||
return known == YES;
|
||||
}
|
||||
|
||||
private NativeBlockState stiltDefault(int entry) {
|
||||
NativeBlockState resolved = stiltDefaults[entry];
|
||||
if (resolved == null) {
|
||||
NativeBlockState state = palette[entry];
|
||||
String material = IrisObjectShaping.materialKey(state);
|
||||
resolved = material.equals("minecraft:grass_block") || material.equals("minecraft:mycelium")
|
||||
|| material.equals("minecraft:podzol") || material.equals("minecraft:dirt_path")
|
||||
? IrisObject.States.dirt() : state;
|
||||
stiltDefaults[entry] = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private boolean shouldStiltPlaced(NativeBlockState state) {
|
||||
if (placedStilts == null) {
|
||||
placedStilts = new IdentityHashMap<>();
|
||||
}
|
||||
Boolean known = placedStilts.get(state);
|
||||
if (known == null) {
|
||||
known = IrisObjectShaping.shouldStilt(state, excludedStiltMaterials);
|
||||
placedStilts.put(state, known);
|
||||
}
|
||||
return known;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.block.VectorMap;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class ObjectPlacementBake {
|
||||
private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32;
|
||||
|
||||
private final long blocksRevision;
|
||||
private final long statesRevision;
|
||||
private final TreeGrounding grounding;
|
||||
private final int count;
|
||||
private final int[] offsetX;
|
||||
private final int[] offsetY;
|
||||
private final int[] offsetZ;
|
||||
private final int[] layerY;
|
||||
private final short[] paletteIndexes;
|
||||
private final NativeBlockState[] palette;
|
||||
private final int[] tileBlocks;
|
||||
private final TileData[] tileValues;
|
||||
private final long[] canopy;
|
||||
private final Key key;
|
||||
private volatile double[] exact;
|
||||
|
||||
private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count,
|
||||
int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes,
|
||||
NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) {
|
||||
this.key = key;
|
||||
this.blocksRevision = blocksRevision;
|
||||
this.statesRevision = statesRevision;
|
||||
this.grounding = grounding;
|
||||
this.count = count;
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
this.offsetZ = offsetZ;
|
||||
this.layerY = layerY;
|
||||
this.paletteIndexes = paletteIndexes;
|
||||
this.palette = palette;
|
||||
this.tileBlocks = tileBlocks;
|
||||
this.tileValues = tileValues;
|
||||
this.canopy = canopy;
|
||||
}
|
||||
|
||||
static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
Key key = Key.of(spin, ceilingHang, translateOffset);
|
||||
ConcurrentHashMap<Key, ObjectPlacementBake> variants = object.placementBakes;
|
||||
ObjectPlacementBake cached = variants.get(key);
|
||||
if (cached != null && cached.matches(object)) {
|
||||
return cached;
|
||||
}
|
||||
ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset);
|
||||
if (built.matches(object)) {
|
||||
if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) {
|
||||
variants.clear();
|
||||
}
|
||||
variants.put(key, built);
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
object.readLock.lock();
|
||||
try {
|
||||
VectorMap<NativeBlockState> blocks = object.blocks;
|
||||
VectorMap<TileData> states = object.states;
|
||||
long blocksRevision = blocks.modificationRevision();
|
||||
long statesRevision = states.modificationRevision();
|
||||
TreeGrounding grounding = object.getTreeGrounding();
|
||||
boolean hasStates = !states.isEmpty();
|
||||
IntArrayList x = new IntArrayList();
|
||||
IntArrayList y = new IntArrayList();
|
||||
IntArrayList z = new IntArrayList();
|
||||
IntArrayList layer = new IntArrayList();
|
||||
IntArrayList indexes = new IntArrayList();
|
||||
IntArrayList tileBlocks = new IntArrayList();
|
||||
List<TileData> tileValues = new ArrayList<>();
|
||||
List<NativeBlockState> palette = new ArrayList<>();
|
||||
IdentityHashMap<NativeBlockState, Integer> paletteSlots = new IdentityHashMap<>();
|
||||
IntArrayList canopyBlocks = new IntArrayList();
|
||||
boolean layerDiffers = false;
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next()) {
|
||||
IrisBlockVector source = cursor.key();
|
||||
NativeBlockState state = cursor.value();
|
||||
if (state == null) {
|
||||
state = IrisObject.States.air();
|
||||
}
|
||||
transform(source, rotated, spin, ceilingHang, translateOffset);
|
||||
int roundedY = (int) Math.round(rotated.getY());
|
||||
int floorY = rotated.getBlockY();
|
||||
x.add((int) Math.round(rotated.getX()));
|
||||
y.add(roundedY);
|
||||
z.add((int) Math.round(rotated.getZ()));
|
||||
layer.add(floorY);
|
||||
layerDiffers |= floorY != roundedY;
|
||||
Integer slot = paletteSlots.get(state);
|
||||
if (slot == null) {
|
||||
slot = palette.size();
|
||||
palette.add(state);
|
||||
paletteSlots.put(state, slot);
|
||||
}
|
||||
indexes.add(slot);
|
||||
if (hasStates) {
|
||||
TileData tile = states.get(source);
|
||||
if (tile != null) {
|
||||
tileBlocks.add(index);
|
||||
tileValues.add(tile);
|
||||
}
|
||||
}
|
||||
if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||
canopyBlocks.add(index);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (palette.size() > Short.MAX_VALUE) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " has more than "
|
||||
+ Short.MAX_VALUE + " distinct block states");
|
||||
}
|
||||
short[] paletteIndexes = new short[index];
|
||||
for (int block = 0; block < index; block++) {
|
||||
paletteIndexes[block] = (short) indexes.getInt(block);
|
||||
}
|
||||
long[] canopy = null;
|
||||
if (!canopyBlocks.isEmpty()) {
|
||||
canopy = new long[(index + 63) >>> 6];
|
||||
for (int block : canopyBlocks) {
|
||||
canopy[block >>> 6] |= 1L << (block & 63);
|
||||
}
|
||||
}
|
||||
return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index,
|
||||
x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null,
|
||||
paletteIndexes, palette.toArray(new NativeBlockState[0]),
|
||||
tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(),
|
||||
tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy);
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang,
|
||||
IrisBlockVector translateOffset) {
|
||||
target.setX(source.getX());
|
||||
target.setY(source.getY());
|
||||
target.setZ(source.getZ());
|
||||
spin.rotate(target);
|
||||
if (ceilingHang) {
|
||||
target.setY(-target.getBlockY());
|
||||
}
|
||||
if (translateOffset != null) {
|
||||
target.add(translateOffset);
|
||||
}
|
||||
}
|
||||
|
||||
boolean matches(IrisObject object) {
|
||||
return object.blocks.modificationRevision() == blocksRevision
|
||||
&& object.states.modificationRevision() == statesRevision
|
||||
&& object.getTreeGrounding() == grounding;
|
||||
}
|
||||
|
||||
int count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
int offsetX(int index) {
|
||||
return offsetX[index];
|
||||
}
|
||||
|
||||
int offsetY(int index) {
|
||||
return offsetY[index];
|
||||
}
|
||||
|
||||
int offsetZ(int index) {
|
||||
return offsetZ[index];
|
||||
}
|
||||
|
||||
int layerY(int index) {
|
||||
return layerY == null ? offsetY[index] : layerY[index];
|
||||
}
|
||||
|
||||
int paletteIndex(int index) {
|
||||
return paletteIndexes[index];
|
||||
}
|
||||
|
||||
NativeBlockState[] palette() {
|
||||
return palette;
|
||||
}
|
||||
|
||||
int[] tileBlocks() {
|
||||
return tileBlocks;
|
||||
}
|
||||
|
||||
TileData[] tileValues() {
|
||||
return tileValues;
|
||||
}
|
||||
|
||||
boolean canopy(int index) {
|
||||
return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L;
|
||||
}
|
||||
|
||||
double[] exact(IrisObject object, SpinKernel spin) {
|
||||
double[] resolved = exact;
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
object.readLock.lock();
|
||||
try {
|
||||
resolved = new double[count * 3];
|
||||
IrisBlockVector rotated = new IrisBlockVector(0, 0, 0);
|
||||
VectorMap<NativeBlockState>.Cursor cursor = object.blocks.cursor();
|
||||
int index = 0;
|
||||
while (cursor.next() && index < count) {
|
||||
transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset());
|
||||
resolved[index * 3] = rotated.getX();
|
||||
resolved[index * 3 + 1] = rotated.getY();
|
||||
resolved[index * 3 + 2] = rotated.getZ();
|
||||
index++;
|
||||
}
|
||||
if (index != count) {
|
||||
throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read");
|
||||
}
|
||||
} finally {
|
||||
object.readLock.unlock();
|
||||
}
|
||||
exact = resolved;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin,
|
||||
long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY,
|
||||
long translateZ) {
|
||||
static Key of(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) {
|
||||
boolean translating = translateOffset != null;
|
||||
return new Key(spin.rotates, spin.xMode, spin.yMode, spin.zMode,
|
||||
Double.doubleToLongBits(spin.xCos), Double.doubleToLongBits(spin.xSin),
|
||||
Double.doubleToLongBits(spin.yCos), Double.doubleToLongBits(spin.ySin),
|
||||
Double.doubleToLongBits(spin.zCos), Double.doubleToLongBits(spin.zSin), ceilingHang, translating,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getX()) : 0L,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getY()) : 0L,
|
||||
translating ? Double.doubleToLongBits(translateOffset.getZ()) : 0L);
|
||||
}
|
||||
|
||||
IrisBlockVector translateOffset() {
|
||||
return translating ? new IrisBlockVector(Double.longBitsToDouble(translateX),
|
||||
Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
|
||||
/**
|
||||
* Precomputed vector rotation for a single placement.
|
||||
* <p>
|
||||
* This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two
|
||||
* differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine
|
||||
* and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the
|
||||
* spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to
|
||||
* the value the per-call form would have produced.
|
||||
*/
|
||||
final class SpinKernel {
|
||||
static final int MODE_NONE = 0;
|
||||
static final int MODE_FLIP = 1;
|
||||
static final int MODE_QUARTER = 2;
|
||||
static final int MODE_THREE_QUARTER = 3;
|
||||
static final int MODE_ANGLE = 4;
|
||||
|
||||
final boolean rotates;
|
||||
final int xMode;
|
||||
final int yMode;
|
||||
final int zMode;
|
||||
final double xCos;
|
||||
final double xSin;
|
||||
final double yCos;
|
||||
final double ySin;
|
||||
final double zCos;
|
||||
final double zSin;
|
||||
|
||||
SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) {
|
||||
rotates = rotation.canRotate();
|
||||
xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE;
|
||||
zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE;
|
||||
yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE;
|
||||
|
||||
double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0;
|
||||
double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0;
|
||||
double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0;
|
||||
xCos = Math.cos(xAngle);
|
||||
xSin = Math.sin(xAngle);
|
||||
zCos = Math.cos(zAngle);
|
||||
zSin = Math.sin(zAngle);
|
||||
yCos = Math.cos(yAngle);
|
||||
ySin = Math.sin(yAngle);
|
||||
}
|
||||
|
||||
private static int modeOf(IrisAxisRotationClamp clamp) {
|
||||
if (!clamp.isLocked()) {
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
if (Math.abs(clamp.getMax()) % 360D == 180D) {
|
||||
return MODE_FLIP;
|
||||
}
|
||||
|
||||
if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) {
|
||||
return MODE_QUARTER;
|
||||
}
|
||||
|
||||
if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) {
|
||||
return MODE_THREE_QUARTER;
|
||||
}
|
||||
|
||||
return MODE_ANGLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates in place. Axis order (X, then Z, then Y) is load bearing.
|
||||
*/
|
||||
void rotate(IrisBlockVector v) {
|
||||
if (!rotates) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (xMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setZ(-v.getZ());
|
||||
v.setY(-v.getY());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(v.getY());
|
||||
v.setY(-z);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double z = v.getZ();
|
||||
v.setZ(-v.getY());
|
||||
v.setY(z);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double y = xCos * v.getY() - xSin * v.getZ();
|
||||
double z = xSin * v.getY() + xCos * v.getZ();
|
||||
v.setY(y);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (zMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setY(-v.getY());
|
||||
v.setX(-v.getX());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(v.getX());
|
||||
v.setX(-y);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double y = v.getY();
|
||||
v.setY(-v.getX());
|
||||
v.setX(y);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = zCos * v.getX() - zSin * v.getY();
|
||||
double y = zSin * v.getX() + zCos * v.getY();
|
||||
v.setX(x);
|
||||
v.setY(y);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
switch (yMode) {
|
||||
case MODE_FLIP -> {
|
||||
v.setX(-v.getX());
|
||||
v.setZ(-v.getZ());
|
||||
}
|
||||
case MODE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(v.getZ());
|
||||
v.setZ(-x);
|
||||
}
|
||||
case MODE_THREE_QUARTER -> {
|
||||
double x = v.getX();
|
||||
v.setX(-v.getZ());
|
||||
v.setZ(x);
|
||||
}
|
||||
case MODE_ANGLE -> {
|
||||
double x = yCos * v.getX() + ySin * v.getZ();
|
||||
double z = -ySin * v.getX() + yCos * v.getZ();
|
||||
v.setX(x);
|
||||
v.setZ(z);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user