mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Merge L1-objects
This commit is contained in:
commit
08353e3bdd
18 files changed
+1043
-254
No files matched your search
@@ -79,9 +79,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
private static final byte LIQUID_FORCED_AIR = 3;
|
||||
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 final Object collisionRuleLock;
|
||||
private final ObjectSourcePlanCache sourcePlans;
|
||||
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
|
||||
private volatile int collisionRuleState;
|
||||
|
||||
public MantleObjectComponent(EngineMantle engineMantle) {
|
||||
@@ -138,18 +140,41 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||
int radius = getRadius();
|
||||
int sourceChunkRadius = sourceChunkRadius(radius);
|
||||
replaySourceChunks(
|
||||
x,
|
||||
z,
|
||||
getRadius(),
|
||||
radius,
|
||||
(sourceX, sourceZ) -> {
|
||||
ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
|
||||
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
if (plan != null) {
|
||||
transaction.apply(plan);
|
||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
||||
}
|
||||
}
|
||||
);
|
||||
transaction.commit();
|
||||
logSourcePlanStats();
|
||||
}
|
||||
|
||||
private void logSourcePlanStats() {
|
||||
long now = System.currentTimeMillis();
|
||||
long last = lastSourcePlanStatsLog.get();
|
||||
if (now - last < SOURCE_PLAN_STATS_INTERVAL_MS || !lastSourcePlanStatsLog.compareAndSet(last, now)) {
|
||||
return;
|
||||
}
|
||||
ObjectSourcePlanCache.Stats stats = sourcePlans.stats();
|
||||
IrisLogging.debug("Object source plans: lookups=" + stats.lookups()
|
||||
+ " builds=" + stats.builds()
|
||||
+ " waits=" + stats.waits()
|
||||
+ " replays=" + stats.replays()
|
||||
+ " drained=" + stats.drained()
|
||||
+ " retired=" + stats.retired()
|
||||
+ " retained=" + stats.retained()
|
||||
+ " retainedMiB=" + (stats.retainedBytes() >> 20)
|
||||
+ " budgetMiB=" + (stats.budgetBytes() >> 20));
|
||||
}
|
||||
|
||||
private ObjectSourcePlan buildSourcePlan(
|
||||
|
||||
+3
-3
@@ -59,10 +59,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
if (checkpoint < 0 || checkpoint > mutations.size()) {
|
||||
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
|
||||
}
|
||||
ArrayList<List<Mutation>> accepted = new ArrayList<>();
|
||||
ArrayList<PlacementRange> accepted = new ArrayList<>();
|
||||
for (PlacementRange placement : placements) {
|
||||
if (placement.start() >= checkpoint) {
|
||||
accepted.add(List.copyOf(mutations.subList(placement.start(), placement.end())));
|
||||
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
|
||||
}
|
||||
}
|
||||
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
|
||||
@@ -451,7 +451,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
}
|
||||
|
||||
private record PlacementRange(int start, int end) {
|
||||
record PlacementRange(int start, int end) {
|
||||
}
|
||||
|
||||
record DataKey(int x, int y, int z, Class<?> type) {
|
||||
|
||||
@@ -4,93 +4,242 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class ObjectSourcePlan {
|
||||
private static final int PLAN_BYTES = 256;
|
||||
private static final int DESTINATION_INDEX_BYTES = 96;
|
||||
private static final int MUTATION_BYTES = 96;
|
||||
private static final int PLACEMENT_BYTES = 256;
|
||||
private static final int PLACEMENT_DESTINATION_BYTES = 128;
|
||||
private static final int REFERENCE_BYTES = 8;
|
||||
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
|
||||
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
|
||||
private static final int PLAN_BYTES = 160;
|
||||
private static final int DESTINATION_BYTES = 12;
|
||||
private static final int MUTATION_BYTES = 8;
|
||||
private static final int VALUE_BYTES = 8;
|
||||
private static final int PLACEMENT_BYTES = 128;
|
||||
private static final int PLACEMENT_DESTINATION_BYTES = 40;
|
||||
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
|
||||
private static final int CUSTOM_BIT = 1 << 8;
|
||||
private static final int Y_SHIFT = 9;
|
||||
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
|
||||
|
||||
private final long[] destinations;
|
||||
private final int[] offsets;
|
||||
private final int[] cells;
|
||||
private final int[] valueIndexes;
|
||||
private final Object[] values;
|
||||
private final Placement[] placements;
|
||||
private final int estimatedRetainedBytes;
|
||||
private final List<Placement> placements;
|
||||
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
|
||||
this(mutations, List.of());
|
||||
}
|
||||
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<List<ObjectDestinationTransaction.Mutation>> placements) {
|
||||
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
|
||||
int count = mutations.size();
|
||||
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);
|
||||
mutationDestinations[index] = destination;
|
||||
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
|
||||
firstSeen.add(destination);
|
||||
}
|
||||
}
|
||||
destinations = firstSeen.toLongArray();
|
||||
Arrays.sort(destinations);
|
||||
for (int slot = 0; slot < destinations.length; slot++) {
|
||||
destinationSlots.put(destinations[slot], slot);
|
||||
}
|
||||
offsets = new int[destinations.length + 1];
|
||||
for (int index = 0; index < count; index++) {
|
||||
offsets[destinationSlots.get(mutationDestinations[index]) + 1]++;
|
||||
}
|
||||
for (int slot = 0; slot < destinations.length; slot++) {
|
||||
offsets[slot + 1] += offsets[slot];
|
||||
}
|
||||
int[] cursor = Arrays.copyOf(offsets, destinations.length);
|
||||
int[] positions = new int[count];
|
||||
cells = new int[count];
|
||||
valueIndexes = new int[count];
|
||||
ValueTable table = new ValueTable();
|
||||
for (int index = 0; index < count; index++) {
|
||||
ObjectDestinationTransaction.Mutation mutation = mutations.get(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());
|
||||
}
|
||||
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 | ((key.z() & 15) << 4) | (key.x() & 15);
|
||||
valueIndexes[position] = table.indexOf(value);
|
||||
}
|
||||
values = table.values.toArray();
|
||||
|
||||
ArrayList<Placement> crossing = new ArrayList<>();
|
||||
for (int ordinal = 0; ordinal < placements.size(); ordinal++) {
|
||||
List<ObjectDestinationTransaction.Mutation> placement = placements.get(ordinal);
|
||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments = group(placement);
|
||||
if (fragments.size() > 1) {
|
||||
crossing.add(new Placement(ordinal, bounds(placement), List.copyOf(fragments.keySet()), fragments));
|
||||
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);
|
||||
if (placement != null) {
|
||||
crossing.add(placement);
|
||||
}
|
||||
}
|
||||
this.placements = List.copyOf(crossing);
|
||||
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
|
||||
long weight = PLAN_BYTES;
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
long key = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
|
||||
ArrayList<ObjectDestinationTransaction.Mutation> local = grouped.get(key);
|
||||
if (local == null) {
|
||||
local = new ArrayList<>();
|
||||
grouped.put(key, local);
|
||||
weight += DESTINATION_INDEX_BYTES;
|
||||
}
|
||||
local.add(mutation);
|
||||
Object payload = switch (mutation) {
|
||||
case ObjectDestinationTransaction.SetMutation set -> set.value();
|
||||
case ObjectDestinationTransaction.CustomBlockMutation custom -> custom.state();
|
||||
};
|
||||
weight = Math.min(Integer.MAX_VALUE, weight + MUTATION_BYTES + payloadBytes(payload));
|
||||
}
|
||||
destinations = new Long2ObjectOpenHashMap<>(grouped.size());
|
||||
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
|
||||
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
|
||||
}
|
||||
for (Placement placement : this.placements) {
|
||||
placements = crossing.toArray(new Placement[0]);
|
||||
|
||||
long weight = PLAN_BYTES
|
||||
+ (long) destinations.length * DESTINATION_BYTES
|
||||
+ (long) count * MUTATION_BYTES
|
||||
+ table.payloadBytes
|
||||
+ (long) values.length * VALUE_BYTES;
|
||||
for (Placement placement : placements) {
|
||||
weight += PLACEMENT_BYTES + (long) placement.touched().size() * PLACEMENT_DESTINATION_BYTES;
|
||||
for (List<ObjectDestinationTransaction.Mutation> fragment : placement.fragments().values()) {
|
||||
long retainedCapacity = Math.max(10L, fragment.size() + (fragment.size() + 1L) / 2L);
|
||||
weight += retainedCapacity * REFERENCE_BYTES;
|
||||
}
|
||||
estimatedRetainedBytes = (int) Math.min(Integer.MAX_VALUE, weight);
|
||||
}
|
||||
|
||||
private ObjectSourcePlan(long[] destinations) {
|
||||
this.destinations = destinations;
|
||||
this.offsets = new int[destinations.length + 1];
|
||||
this.cells = new int[0];
|
||||
this.valueIndexes = new int[0];
|
||||
this.values = new Object[0];
|
||||
this.placements = new Placement[0];
|
||||
this.estimatedRetainedBytes = PLAN_BYTES + destinations.length * DESTINATION_BYTES;
|
||||
}
|
||||
|
||||
ObjectSourcePlan destinationIndex() {
|
||||
return new ObjectSourcePlan(destinations);
|
||||
}
|
||||
|
||||
private static Placement crossingPlacement(
|
||||
int ordinal,
|
||||
int start,
|
||||
int end,
|
||||
List<ObjectDestinationTransaction.Mutation> mutations,
|
||||
long[] mutationDestinations,
|
||||
Long2IntOpenHashMap destinationSlots,
|
||||
int[] positions
|
||||
) {
|
||||
if (start < 0 || end > mutations.size() || start >= end) {
|
||||
throw new IllegalArgumentException("Object placement range is outside the plan");
|
||||
}
|
||||
long first = mutationDestinations[start];
|
||||
int index = start + 1;
|
||||
while (index < end && mutationDestinations[index] == first) {
|
||||
index++;
|
||||
}
|
||||
if (index == end) {
|
||||
return null;
|
||||
}
|
||||
LongArrayList touched = new LongArrayList(4);
|
||||
Long2IntOpenHashMap touchedIndexes = new Long2IntOpenHashMap(4);
|
||||
touchedIndexes.defaultReturnValue(-1);
|
||||
ArrayList<int[]> runs = new ArrayList<>(4);
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
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());
|
||||
long destination = mutationDestinations[mutationIndex];
|
||||
int touchedIndex = touchedIndexes.get(destination);
|
||||
if (touchedIndex < 0) {
|
||||
touchedIndexes.put(destination, touched.size());
|
||||
touched.add(destination);
|
||||
runs.add(new int[]{destinationSlots.get(destination), positions[mutationIndex], 1});
|
||||
} else {
|
||||
runs.get(touchedIndex)[2]++;
|
||||
}
|
||||
}
|
||||
this.estimatedRetainedBytes = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
|
||||
int size = touched.size();
|
||||
ArrayList<ObjectContinuationBundle.ChunkPosition> chunks = new ArrayList<>(size);
|
||||
int[] slots = new int[size];
|
||||
int[] runStarts = new int[size];
|
||||
int[] runLengths = new int[size];
|
||||
for (int touchedIndex = 0; touchedIndex < size; touchedIndex++) {
|
||||
long destination = touched.getLong(touchedIndex);
|
||||
chunks.add(new ObjectContinuationBundle.ChunkPosition(destinationX(destination), destinationZ(destination)));
|
||||
int[] run = runs.get(touchedIndex);
|
||||
slots[touchedIndex] = run[0];
|
||||
runStarts[touchedIndex] = run[1];
|
||||
runLengths[touchedIndex] = run[2];
|
||||
}
|
||||
return new Placement(ordinal, new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ),
|
||||
List.copyOf(chunks), slots, runStarts, runLengths);
|
||||
}
|
||||
|
||||
int destinationSlot(int chunkX, int chunkZ) {
|
||||
return Arrays.binarySearch(destinations, destinationKey(chunkX, chunkZ));
|
||||
}
|
||||
|
||||
int destinationCount() {
|
||||
return destinations.length;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return destinations.length == 0;
|
||||
}
|
||||
|
||||
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = destinations.get(destinationKey(chunkX, chunkZ));
|
||||
return mutations == null ? List.of() : mutations;
|
||||
int slot = destinationSlot(chunkX, chunkZ);
|
||||
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
|
||||
}
|
||||
|
||||
void persistContinuations(MantleWriter writer, int sourceX, int sourceZ, int destinationX, int destinationZ) {
|
||||
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(destinationX, destinationZ);
|
||||
if (placements.length == 0) {
|
||||
return;
|
||||
}
|
||||
int destinationSlot = destinationSlot(destinationX, destinationZ);
|
||||
if (destinationSlot < 0) {
|
||||
return;
|
||||
}
|
||||
for (Placement placement : placements) {
|
||||
if (!placement.fragments().containsKey(destination)) {
|
||||
if (!placement.touches(destinationSlot)) {
|
||||
continue;
|
||||
}
|
||||
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, sourceX, sourceZ, placement.ordinal());
|
||||
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> entry : placement.fragments().entrySet()) {
|
||||
ObjectContinuationBundle.ChunkPosition target = entry.getKey();
|
||||
if (target.equals(destination)) {
|
||||
for (int touchedIndex = 0; touchedIndex < placement.slots().length; touchedIndex++) {
|
||||
if (placement.slots()[touchedIndex] == destinationSlot) {
|
||||
continue;
|
||||
}
|
||||
ObjectContinuationBundle.ChunkPosition target = placement.touched().get(touchedIndex);
|
||||
int runStart = placement.runStarts()[touchedIndex];
|
||||
int runLength = placement.runLengths()[touchedIndex];
|
||||
writer.withChunkFence(target.x(), target.z(), () -> {
|
||||
MantleChunk<Matter> chunk = writer.acquireChunk(target.x(), target.z());
|
||||
if (chunk.isFlagged(MantleFlag.REAL)) {
|
||||
@@ -107,13 +256,32 @@ final class ObjectSourcePlan {
|
||||
}
|
||||
}
|
||||
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(key, placement.bounds(), placement.touched(),
|
||||
encode(entry.getValue(), writer.getMantle().getWorldHeight()));
|
||||
encode(decode(target.x(), target.z(), runStart, runLength), writer.getMantle().getWorldHeight()));
|
||||
writer.setData(x, 0, z, bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
: new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
|
||||
}
|
||||
return List.of(decoded);
|
||||
}
|
||||
|
||||
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter payload = new IrisMatter(16, height, 16);
|
||||
@@ -134,54 +302,41 @@ final class ObjectSourcePlan {
|
||||
return ObjectContinuationBundle.encode(payload);
|
||||
}
|
||||
|
||||
private static LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
|
||||
List<ObjectDestinationTransaction.Mutation> mutations
|
||||
) {
|
||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
|
||||
grouped.computeIfAbsent(destination, ignored -> new ArrayList<>()).add(mutation);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private static ObjectContinuationBundle.Bounds bounds(List<ObjectDestinationTransaction.Mutation> mutations) {
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
minimumX = Math.min(minimumX, mutation.x());
|
||||
minimumZ = Math.min(minimumZ, mutation.z());
|
||||
maximumX = Math.max(maximumX, mutation.x());
|
||||
maximumZ = Math.max(maximumZ, mutation.z());
|
||||
}
|
||||
return new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ);
|
||||
}
|
||||
|
||||
int estimatedRetainedBytes() {
|
||||
return estimatedRetainedBytes;
|
||||
}
|
||||
|
||||
static Class<?> typeOf(Object value) {
|
||||
return value instanceof NativeBlockState ? NativeBlockState.class : value.getClass();
|
||||
}
|
||||
|
||||
static long destinationKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static int destinationX(long destination) {
|
||||
return (int) (destination >> 32);
|
||||
}
|
||||
|
||||
private static int destinationZ(long destination) {
|
||||
return (int) destination;
|
||||
}
|
||||
|
||||
private static long payloadBytes(Object value) {
|
||||
return switch (value) {
|
||||
case null -> 0L;
|
||||
case String text -> stringBytes(text);
|
||||
case TreeBlockMaterial material -> 24L + stringBytes(material.materialKey());
|
||||
case Identifier identifier -> 32L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
|
||||
case NativeBlockState ignored -> 64L;
|
||||
case MatterCavern cavern -> 32L + stringBytes(cavern.getCustomBiome());
|
||||
case HydrologyCaveCell cell -> 64L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
|
||||
case Byte ignored -> 24L;
|
||||
case Short ignored -> 24L;
|
||||
case Integer ignored -> 24L;
|
||||
case Long ignored -> 24L;
|
||||
case Float ignored -> 24L;
|
||||
case Double ignored -> 24L;
|
||||
case Boolean ignored -> 24L;
|
||||
case Character ignored -> 24L;
|
||||
case TreeBlockMaterial material -> 16L + stringBytes(material.materialKey());
|
||||
case Identifier identifier -> 24L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
|
||||
case NativeBlockState ignored -> 0L;
|
||||
case MatterCavern cavern -> 24L + stringBytes(cavern.getCustomBiome());
|
||||
case HydrologyCaveCell cell -> 48L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
|
||||
case MatterMarker marker -> 16L + stringBytes(marker.getTag());
|
||||
case TileWrapper ignored -> 4L * UNKNOWN_PAYLOAD_BYTES;
|
||||
case Enum<?> ignored -> 0L;
|
||||
default -> Integer.MAX_VALUE;
|
||||
case Number ignored -> 16L;
|
||||
case Boolean ignored -> 16L;
|
||||
case Character ignored -> 16L;
|
||||
default -> UNKNOWN_PAYLOAD_BYTES;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -189,12 +344,45 @@ final class ObjectSourcePlan {
|
||||
return value == null ? 0L : 48L + 2L * value.length();
|
||||
}
|
||||
|
||||
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
|
||||
List<ObjectContinuationBundle.ChunkPosition> touched,
|
||||
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments) {
|
||||
private static final class ValueTable {
|
||||
private final ArrayList<Object> values = new ArrayList<>();
|
||||
private final Object2IntOpenHashMap<Object> equalValues = new Object2IntOpenHashMap<>();
|
||||
private final Reference2IntOpenHashMap<Object> identityValues = new Reference2IntOpenHashMap<>();
|
||||
private long payloadBytes;
|
||||
|
||||
private ValueTable() {
|
||||
equalValues.defaultReturnValue(-1);
|
||||
identityValues.defaultReturnValue(-1);
|
||||
}
|
||||
|
||||
private int indexOf(Object value) {
|
||||
boolean byValue = value instanceof String || value instanceof TreeBlockMaterial;
|
||||
int index = byValue ? equalValues.getInt(value) : identityValues.getInt(value);
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
index = values.size();
|
||||
values.add(value);
|
||||
payloadBytes += payloadBytes(value);
|
||||
if (byValue) {
|
||||
equalValues.put(value, index);
|
||||
} else {
|
||||
identityValues.put(value, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
private static long destinationKey(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
|
||||
List<ObjectContinuationBundle.ChunkPosition> touched,
|
||||
int[] slots, int[] runStarts, int[] runLengths) {
|
||||
private boolean touches(int slot) {
|
||||
for (int touchedSlot : slots) {
|
||||
if (touchedSlot == slot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,22 @@ import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static art.arcane.iris.generation.cache.Cache.key;
|
||||
|
||||
final class ObjectSourcePlanCache {
|
||||
private static final long MAXIMUM_RETAINED_BYTES = 256L * 1024L * 1024L;
|
||||
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
||||
private static final int ENTRY_BYTES = 64;
|
||||
|
||||
private final long maximumRetainedBytes;
|
||||
private final LongAdder lookups = new LongAdder();
|
||||
private final LongAdder builds = new LongAdder();
|
||||
private final LongAdder waits = new LongAdder();
|
||||
private final LongAdder replays = new LongAdder();
|
||||
private final LongAdder drained = new LongAdder();
|
||||
private final LongAdder retired = new LongAdder();
|
||||
private volatile State state;
|
||||
|
||||
ObjectSourcePlanCache() {
|
||||
@@ -31,33 +39,84 @@ final class ObjectSourcePlanCache {
|
||||
}
|
||||
|
||||
static long retainedByteBudget(long maximumHeapBytes) {
|
||||
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 64L));
|
||||
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 32L));
|
||||
}
|
||||
|
||||
ObjectSourcePlan get(int sourceChunkX, int sourceChunkZ, Supplier<ObjectSourcePlan> builder) {
|
||||
/**
|
||||
* Returns the plan whose mutations the destination still has to replay, or null when the source writes nothing
|
||||
* there. Each destination consumes a retained plan once; a plan whose destinations were all consumed keeps only
|
||||
* its destination index, so a repeated consumption rebuilds it instead of trusting released state. The entry is
|
||||
* dropped once every destination within the source chunk radius has asked for it.
|
||||
*/
|
||||
ObjectSourcePlan acquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
|
||||
int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
|
||||
Objects.requireNonNull(builder, "Source plan builder");
|
||||
lookups.increment();
|
||||
State current = state;
|
||||
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
|
||||
ObjectSourcePlan cached = current.plans.getIfPresent(source);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
Entry entry = current.plans.getIfPresent(source);
|
||||
if (entry == null) {
|
||||
entry = load(current, source, sourceChunkRadius, builder);
|
||||
}
|
||||
boolean lastVisitor = entry.visitors.visit(destinationChunkX - sourceChunkX, destinationChunkZ - sourceChunkZ);
|
||||
ObjectSourcePlan plan = claim(current, source, entry, destinationChunkX, destinationChunkZ, builder);
|
||||
if (lastVisitor) {
|
||||
retired.increment();
|
||||
Visitors visitors = entry.visitors;
|
||||
current.plans.asMap().computeIfPresent(source, (Long key, Entry mapped) -> mapped.visitors == visitors ? null : mapped);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
|
||||
Supplier<ObjectSourcePlan> builder) {
|
||||
int slot = entry.slot(destinationChunkX, destinationChunkZ);
|
||||
if (slot < 0) {
|
||||
return null;
|
||||
}
|
||||
Claim claim = entry.claim(slot);
|
||||
if (claim == null) {
|
||||
replays.increment();
|
||||
return replay(current, source, entry, destinationChunkX, destinationChunkZ, builder);
|
||||
}
|
||||
if (claim.last()) {
|
||||
drained.increment();
|
||||
current.plans.asMap().replace(source, entry, entry.released());
|
||||
}
|
||||
return claim.plan();
|
||||
}
|
||||
|
||||
private ObjectSourcePlan replay(State current, long source, Entry consumed, int destinationChunkX, int destinationChunkZ,
|
||||
Supplier<ObjectSourcePlan> builder) {
|
||||
Entry rebuilt = new Entry(build(builder), consumed.visitors);
|
||||
int slot = rebuilt.slot(destinationChunkX, destinationChunkZ);
|
||||
Claim claim = slot < 0 ? null : rebuilt.claim(slot);
|
||||
if (claim == null) {
|
||||
return null;
|
||||
}
|
||||
if (!claim.last() && rebuilt.weight() <= maximumRetainedBytes) {
|
||||
current.plans.asMap().replace(source, consumed, rebuilt);
|
||||
}
|
||||
return claim.plan();
|
||||
}
|
||||
|
||||
private Entry load(State current, long source, int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
|
||||
Pending created = new Pending(Thread.currentThread());
|
||||
Pending pending = current.pending.putIfAbsent(source, created);
|
||||
if (pending != null) {
|
||||
waits.increment();
|
||||
return pending.await();
|
||||
}
|
||||
try {
|
||||
ObjectSourcePlan plan = current.plans.getIfPresent(source);
|
||||
if (plan == null) {
|
||||
plan = builder.get();
|
||||
if (plan != null && plan.estimatedRetainedBytes() < Integer.MAX_VALUE
|
||||
&& plan.estimatedRetainedBytes() <= maximumRetainedBytes) {
|
||||
current.plans.put(source, plan);
|
||||
Entry entry = current.plans.getIfPresent(source);
|
||||
if (entry == null) {
|
||||
entry = new Entry(build(builder), new Visitors(sourceChunkRadius));
|
||||
if (entry.weight() <= maximumRetainedBytes) {
|
||||
current.plans.put(source, entry);
|
||||
}
|
||||
}
|
||||
created.result.complete(plan);
|
||||
return plan;
|
||||
created.result.complete(entry);
|
||||
return entry;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
created.failure = failure;
|
||||
created.result.completeExceptionally(failure);
|
||||
@@ -67,6 +126,11 @@ final class ObjectSourcePlanCache {
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectSourcePlan build(Supplier<ObjectSourcePlan> builder) {
|
||||
builds.increment();
|
||||
return Objects.requireNonNull(builder.get(), "Source plan builder returned no plan");
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = new State(maximumRetainedBytes);
|
||||
}
|
||||
@@ -77,28 +141,134 @@ final class ObjectSourcePlanCache {
|
||||
return current.plans.estimatedSize();
|
||||
}
|
||||
|
||||
Stats stats() {
|
||||
State current = state;
|
||||
current.plans.cleanUp();
|
||||
long retainedBytes = current.plans.policy().eviction()
|
||||
.map(eviction -> eviction.weightedSize().orElse(0L))
|
||||
.orElse(0L);
|
||||
return new Stats(lookups.sum(), builds.sum(), waits.sum(), replays.sum(), drained.sum(), retired.sum(),
|
||||
current.plans.estimatedSize(), retainedBytes, maximumRetainedBytes);
|
||||
}
|
||||
|
||||
record Stats(long lookups, long builds, long waits, long replays, long drained, long retired, long retained,
|
||||
long retainedBytes, long budgetBytes) {
|
||||
}
|
||||
|
||||
private static final class State {
|
||||
private final Cache<Long, ObjectSourcePlan> plans;
|
||||
private final Cache<Long, Entry> plans;
|
||||
private final ConcurrentHashMap<Long, Pending> pending = new ConcurrentHashMap<>();
|
||||
|
||||
private State(long maximumRetainedBytes) {
|
||||
plans = Caffeine.newBuilder()
|
||||
.maximumWeight(maximumRetainedBytes)
|
||||
.weigher((Long key, ObjectSourcePlan plan) -> plan.estimatedRetainedBytes())
|
||||
.weigher((Long key, Entry entry) -> entry.weight())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private record Claim(ObjectSourcePlan plan, boolean last) {
|
||||
}
|
||||
|
||||
private static final class Entry {
|
||||
private final ObjectSourcePlan index;
|
||||
private final boolean[] claimed;
|
||||
private final Visitors visitors;
|
||||
private final int weight;
|
||||
private ObjectSourcePlan plan;
|
||||
private int remaining;
|
||||
|
||||
private Entry(ObjectSourcePlan plan, Visitors visitors) {
|
||||
this.index = plan.isEmpty() ? ObjectSourcePlan.EMPTY : plan;
|
||||
this.plan = plan.isEmpty() ? null : plan;
|
||||
this.claimed = new boolean[plan.destinationCount()];
|
||||
this.visitors = visitors;
|
||||
this.remaining = claimed.length;
|
||||
this.weight = weigh(index, claimed, visitors);
|
||||
}
|
||||
|
||||
private Entry(ObjectSourcePlan index, boolean[] claimed, Visitors visitors) {
|
||||
this.index = index;
|
||||
this.claimed = claimed;
|
||||
this.visitors = visitors;
|
||||
this.weight = weigh(index, claimed, visitors);
|
||||
}
|
||||
|
||||
private int slot(int destinationChunkX, int destinationChunkZ) {
|
||||
return index.destinationSlot(destinationChunkX, destinationChunkZ);
|
||||
}
|
||||
|
||||
private synchronized Claim claim(int slot) {
|
||||
ObjectSourcePlan retained = plan;
|
||||
if (retained == null || claimed[slot]) {
|
||||
return null;
|
||||
}
|
||||
claimed[slot] = true;
|
||||
if (--remaining == 0) {
|
||||
plan = null;
|
||||
return new Claim(retained, true);
|
||||
}
|
||||
return new Claim(retained, false);
|
||||
}
|
||||
|
||||
private Entry released() {
|
||||
return new Entry(index.destinationIndex(), claimed, visitors);
|
||||
}
|
||||
|
||||
private int weight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
private static int weigh(ObjectSourcePlan index, boolean[] claimed, Visitors visitors) {
|
||||
return (int) Math.min(Integer.MAX_VALUE,
|
||||
ENTRY_BYTES + (long) claimed.length + visitors.retainedBytes() + index.estimatedRetainedBytes());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Visitors {
|
||||
private final int radius;
|
||||
private final int side;
|
||||
private final long[] seen;
|
||||
private int remaining;
|
||||
|
||||
private Visitors(int radius) {
|
||||
if (radius < 0) {
|
||||
throw new IllegalArgumentException("Source chunk radius must not be negative");
|
||||
}
|
||||
this.radius = radius;
|
||||
this.side = 2 * radius + 1;
|
||||
this.seen = new long[(side * side + 63) >>> 6];
|
||||
this.remaining = side * side;
|
||||
}
|
||||
|
||||
private synchronized boolean visit(int offsetX, int offsetZ) {
|
||||
if (Math.abs(offsetX) > radius || Math.abs(offsetZ) > radius) {
|
||||
return false;
|
||||
}
|
||||
int bit = (offsetX + radius) * side + offsetZ + radius;
|
||||
long mask = 1L << (bit & 63);
|
||||
if ((seen[bit >>> 6] & mask) != 0L) {
|
||||
return false;
|
||||
}
|
||||
seen[bit >>> 6] |= mask;
|
||||
return --remaining == 0;
|
||||
}
|
||||
|
||||
private long retainedBytes() {
|
||||
return 32L + 8L * seen.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Thread owner;
|
||||
private final CompletableFuture<ObjectSourcePlan> result = new CompletableFuture<>();
|
||||
private final CompletableFuture<Entry> result = new CompletableFuture<>();
|
||||
private Throwable failure;
|
||||
|
||||
private Pending(Thread owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
private ObjectSourcePlan await() {
|
||||
private Entry await() {
|
||||
if (owner == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Recursive source plan construction");
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public final class BukkitBlockState implements NativeBlockState {
|
||||
private byte deepSlate;
|
||||
private byte vineBlock;
|
||||
private byte tileEntity;
|
||||
private volatile ConcurrentHashMap<String, ConcurrentHashMap<String, NativeBlockState>> propertyVariants;
|
||||
|
||||
private BukkitBlockState(BlockData data, String key) {
|
||||
this.data = data;
|
||||
@@ -435,7 +436,18 @@ public final class BukkitBlockState implements NativeBlockState {
|
||||
BlockData resolved = Bukkit.createBlockData(merged);
|
||||
return of(IrisCustomData.of(resolved, custom.getCustom()));
|
||||
}
|
||||
return of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value)));
|
||||
ConcurrentHashMap<String, ConcurrentHashMap<String, NativeBlockState>> variants = propertyVariants;
|
||||
if (variants == null) {
|
||||
variants = new ConcurrentHashMap<>(4);
|
||||
propertyVariants = variants;
|
||||
}
|
||||
ConcurrentHashMap<String, NativeBlockState> values = variants.computeIfAbsent(name, (String ignored) -> new ConcurrentHashMap<>(4));
|
||||
NativeBlockState variant = values.get(value);
|
||||
if (variant == null) {
|
||||
variant = of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value)));
|
||||
values.putIfAbsent(value, variant);
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -313,7 +314,15 @@ public final class NativeStructureVolumeIndex {
|
||||
};
|
||||
}
|
||||
|
||||
static int runtimeChunkHash(int runtimeId, long chunkKey) {
|
||||
return Long.hashCode(CacheKey.mix(chunkKey)) * 31 + runtimeId;
|
||||
}
|
||||
|
||||
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return runtimeChunkHash(runtimeId, chunkKey);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -86,6 +86,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
NativeBlockState stone,
|
||||
NativeBlockState vair,
|
||||
NativeBlockState vairDebug,
|
||||
NativeBlockState dirt,
|
||||
NativeBlockState[] snowLayers) {
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ public class IrisObject extends IrisRegistrant {
|
||||
B.getState("STONE"),
|
||||
B.getState("VOID_AIR"),
|
||||
B.getState("COBWEB"),
|
||||
B.getState("minecraft:dirt"),
|
||||
new NativeBlockState[]{B.getState("minecraft:snow[layers=1]"), B.getState("minecraft:snow[layers=2]"), B.getState("minecraft:snow[layers=3]"), B.getState("minecraft:snow[layers=4]"), B.getState("minecraft:snow[layers=5]"), B.getState("minecraft:snow[layers=6]"), B.getState("minecraft:snow[layers=7]"), B.getState("minecraft:snow[layers=8]")});
|
||||
bound = resolved;
|
||||
return resolved;
|
||||
@@ -123,6 +125,10 @@ public class IrisObject extends IrisRegistrant {
|
||||
return bound().vairDebug();
|
||||
}
|
||||
|
||||
static NativeBlockState dirt() {
|
||||
return bound().dirt();
|
||||
}
|
||||
|
||||
static NativeBlockState snowLayer(int layerIndex) {
|
||||
return bound().snowLayers()[layerIndex];
|
||||
}
|
||||
|
||||
@@ -733,7 +733,7 @@ final class IrisObjectPlacementRunner {
|
||||
} 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 = B.getState("minecraft:dirt");
|
||||
d = IrisObject.States.dirt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,11 +745,10 @@ final class IrisObjectPlacementRunner {
|
||||
if (translating) {
|
||||
i.add(translateOffset);
|
||||
}
|
||||
d = config.getRotation().rotate(d, spinx, spiny, spinz);
|
||||
|
||||
int targetLayer = ceilingHang ? topLayer : lowest;
|
||||
if (i.getBlockY() != targetLayer)
|
||||
continue;
|
||||
d = config.getRotation().rotate(d, spinx, spiny, spinz);
|
||||
|
||||
if (hasEdits) {
|
||||
for (IrisObjectReplace j : config.getEdit()) {
|
||||
|
||||
@@ -31,9 +31,12 @@ import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.bukkit.Axis;
|
||||
import org.bukkit.block.BlockFace;
|
||||
@@ -50,6 +53,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Snippet("object-rotator")
|
||||
@Accessors(chain = true)
|
||||
@@ -59,6 +63,8 @@ import java.util.Set;
|
||||
@Data
|
||||
public class IrisObjectRotation {
|
||||
private static volatile StateRotator PLATFORM_ROTATOR = null;
|
||||
private static final Object ROTATED_AWAY = new Object();
|
||||
private static final int SPIN_STEPS = 5;
|
||||
|
||||
public interface StateRotator {
|
||||
NativeBlockState rotate(IrisObjectRotation rotation, NativeBlockState state, int spinx, int spiny, int spinz);
|
||||
@@ -91,6 +97,10 @@ public class IrisObjectRotation {
|
||||
@Description("The z axis rotation")
|
||||
private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp();
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@ToString.Exclude
|
||||
private final transient ConcurrentHashMap<NativeBlockState, Object[]> stateRotations = new ConcurrentHashMap<>();
|
||||
|
||||
public static IrisObjectRotation xFlip180() {
|
||||
IrisObjectRotation rt = new IrisObjectRotation();
|
||||
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
|
||||
@@ -316,9 +326,19 @@ public class IrisObjectRotation {
|
||||
return state;
|
||||
}
|
||||
|
||||
BlockData raw = original.clone();
|
||||
BlockData rotated = rotate(raw, spinx, spiny, spinz);
|
||||
return rotated == null ? null : BukkitBlockState.of(rotated);
|
||||
Object[] rotations = stateRotations.computeIfAbsent(state, (NativeBlockState ignored) -> new Object[SPIN_STEPS * SPIN_STEPS * SPIN_STEPS]);
|
||||
int index = (spinStep(spinx) * SPIN_STEPS + spinStep(spiny)) * SPIN_STEPS + spinStep(spinz);
|
||||
Object memo = rotations[index];
|
||||
if (memo == null) {
|
||||
BlockData rotated = rotate(original.clone(), spinx, spiny, spinz);
|
||||
memo = rotated == null ? ROTATED_AWAY : BukkitBlockState.of(rotated);
|
||||
rotations[index] = memo;
|
||||
}
|
||||
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
|
||||
}
|
||||
|
||||
private static int spinStep(int spin) {
|
||||
return (int) Math.ceil(Math.abs((spin % 360D) / 90D));
|
||||
}
|
||||
|
||||
private static boolean canRotateBlockData(BlockData data) {
|
||||
|
||||
Reference in new issue
Block a user